המרה דינמית ו־RTTI
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 62 מתוך 104.
לפעמים, כשעובדים עם פולימורפיזם, צריך לקבוע מהו הטיפוס בפועל של אובייקט בזמן ריצה או להמיר בבטחה מצביע למחלקת בסיס למצביע למחלקה נגזרת. C++ מספקת RTTI (מידע על טיפוסים בזמן ריצה) ואת dynamic_cast למצבים האלה.
dynamic_cast ממיר בבטחה מצביעים או הפניות בתוך היררכיית ירושה. בניגוד ל־static_cast, הוא מבצע בדיקה בזמן ריצה ומחזיר nullptr אם ההמרה אינה תקפה:
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void bark() { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {};
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal); // מצליח: מחזיר מצביע תקין
if (dog) {
dog->bark(); // בטוח לקרוא למתודה הייחודית ל-Dog
}
Cat* cat = dynamic_cast<Cat*>(animal); // נכשל: מחזיר nullptrחשוב: dynamic_cast פועל רק עם טיפוסים פולימורפיים (מחלקות שיש בהן לפחות פונקציה וירטואלית אחת). האופרטור typeid מאפשר לך לברר את הטיפוס בפועל של אובייקט:
#include <typeinfo>
Animal* pet = new Dog();
std::cout << typeid(*pet).name() << std::endl; // מדפיס מידע על הטיפוס של Dogלמרות ש־dynamic_cast שימושי, שימוש תכוף בו מעיד לעיתים קרובות על בעיית תכנון. העדיפו פונקציות וירטואליות כשאפשר, מכיוון שהן מאפשרות לאובייקט לטפל בהתנהגות הספציפית לסוג שלו ללא בדיקת סוג מפורשת.
אתגר
קלבואו נבנה מערכת לבדיקת כלי רכב שמשתמשת ב-dynamic_cast כדי לזהות בבטחה סוגים שונים של כלי רכב ולפעול איתם. תיצרו היררכיה של כלי רכב, שבה בודק צריך לבצע בדיקות ספציפיות לסוג, שרק כלי רכב מסוימים תומכים בהן.
תארגנו את הקוד בשלושה קבצים:
Vehicle.h: הגדירו מחלקת בסיסVehicleשמייצגת כל כלי רכב במערכת:- חבר
std::string licensePlateמוגן - בנאי שמאתחל את לוחית הרישוי
- מתודה וירטואלית
getDescription()שמחזירה"Vehicle: <licensePlate>" - הורס וירטואלי
- חבר
Vehicles.h: הגדירו שלושה סוגי כלי רכב נגזרים:Car:- חבר פרטי
int seatCount - בנאי שמקבל לוחית רישוי ומספר מושבים
- דרסו את
getDescription()כדי שתחזיר"Car: <licensePlate>" - מתודה
inspectSeatbelts()שמדפיסה:Inspecting <seatCount> seatbelts in <licensePlate>
Truck:- חבר פרטי
double cargoCapacity(בטונות) - בנאי שמקבל לוחית רישוי וקיבולת מטען
- דרסו את
getDescription()כדי שתחזיר"Truck: <licensePlate>" - מתודה
inspectCargo()שמדפיסה:Inspecting cargo area (<cargoCapacity> tons) in <licensePlate>
Motorcycle:- חבר פרטי
bool hasSidecar - בנאי שמקבל לוחית רישוי ומציין אם יש סירה צדדית
- דרסו את
getDescription()כדי שתחזיר"Motorcycle: <licensePlate>" - מתודה
inspectHelmetStorage()שמדפיסה:Inspecting helmet storage in <licensePlate>אם יש לו סירה צדדית, אוNo helmet storage in <licensePlate>אם אין לו
- חבר פרטי
main.cpp: קראו שלוש קלטים (כל אחד בשורה נפרדת):- לוחית הרישוי של המכונית
- לוחית הרישוי של המשאית
- לוחית הרישוי של האופנוע
צרו
Carעם 4 מושבים,Truckעם קיבולת של 10.5 טונות, ו-Motorcycleעם סירה צדדית. אחסנו את שלושתם במערך של מצביעיVehicle*.עברו בלולאה על המערך, ולכל כלי רכב:
- הדפיסו את התיאור שלו באמצעות
getDescription() - השתמשו ב-
dynamic_castכדי לנסות להמיר לכל אחד מהסוגים הנגזרים - אם ההמרה ל-
Car*מצליחה, קראו ל-inspectSeatbelts() - אם ההמרה ל-
Truck*מצליחה, קראו ל-inspectCargo() - אם ההמרה ל-
Motorcycle*מצליחה, קראו ל-inspectHelmetStorage()
הדפיסו שורה ריקה בין בדיקות כלי הרכב. בסיום, שחררו את האובייקטים שהוקצו באופן דינמי.
לדוגמה, עבור הקלטים ABC-123, TRK-456 ו-MTR-789:
Car: ABC-123
Inspecting 4 seatbelts in ABC-123
Truck: TRK-456
Inspecting cargo area (10.5 tons) in TRK-456
Motorcycle: MTR-789
Inspecting helmet storage in MTR-789שימו לב ש-dynamic_cast מחזירה מצביע תקף רק כאשר סוג האובייקט בפועל תואם לסוג היעד. עבור כל כלי רכב, רק אחת משלוש ההמרות תצליח, וכך תוכלו לקרוא בבטחה למתודת הבדיקה הספציפית לסוג. זהו כוחה של RTTI: קביעת הסוג בפועל בזמן הריצה ופעולה בהתאם.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Vehicle.h"
#include "Vehicles.h"
using namespace std;
int main() {
// קרא את הקלטים
string carPlate, truckPlate, motorcyclePlate;
cin >> carPlate;
cin >> truckPlate;
cin >> motorcyclePlate;
// TODO: צור Car עם 4 מושבים
// TODO: צור Truck עם כושר נשיאה של 10.5 טון
// TODO: צור Motorcycle עם סירת צד (true)
// TODO: אחסן את שלושתם במערך של מצביעי Vehicle*
// TODO: עבור בלולאה על המערך, ולכל כלי רכב:
// 1. הדפס את התיאור שלו באמצעות getDescription()
// 2. השתמש ב-dynamic_cast כדי לנסות לבצע המרה לכל טיפוס נגזר
// 3. אם ההמרה ל-Car* מצליחה, קרא ל-inspectSeatbelts()
// 4. אם ההמרה ל-Truck* מצליחה, קרא ל-inspectCargo()
// 5. אם ההמרה ל-Motorcycle* מצליחה, קרא ל-inspectHelmetStorage()
// 6. הדפס שורה ריקה בין בדיקה של כלי רכב אחד לבדיקה של הבא
// TODO: שחררו את האובייקטים שהוקצו דינמית
return 0;
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP
קבצים חיצונייםבנייה וקומפילציה של C++קובצי Header וקובצי מקורמרחבי שמות והיקףמבוא ל-OOP ב-C++מחלקות לעומת אובייקטיםמצביע thisמתודות (פונקציות איבר)תכונות (חברי נתונים)יסודות הבנאים וההורסיםסיכום – מחשבון פשוט4מאפייני מחלקה
חברי מופע לעומת חברים סטטייםגטרים וסטריםפונקציות איבר קבועותמילת המפתח mutableמתודות ומשתנים סטטייםפונקציות ומחלקות ידידותסיכום – מנהל חשבונות בנק7ירושה
ירושה בסיסיתרמות גישה בירושהסדר הקריאות לבנאי ולמפרקדריסת מתודותפונקציות וירטואליות ו־VTableירושה מרובהירושה וירטואליתסיכום – היררכיית עובדים2ניהול זיכרון
זיכרון Stack לעומת Heapמצביעים והפניותזיכרון דינמי (new/delete)מצביעים חכמים ב-C++RAII ב-C++סיכום – מנהל מערכים דינמיים5כימוס
מצייני גישה ב-C++מצייני גישה לעומקהסתרת מידעמבנה לעומת מחלקהמחלקות מקוננות ופנימיותסיכום – מערכת רשומות תלמידים8רב־צורתיות
רב־צורתיות בזמן קומפילציה לעומת בזמן ריצההעמסת פונקציותפונקציות וירטואליות — מבט נוסףפונקציות וירטואליות טהורותמחלקות מופשטותתכנון ממשקים ב־C++המרה דינמית ו־RTTIסיכום — מחשבון צורות3בנאים והורסים
בנאי ברירת מחדלבנאי עם פרמטריםבנאי העתקהבנאי העברהרשימות אתחול בבנאיםבנאים מאציליםמעמיקים בנושא הורסיםכלל השלושה / החמישה / האפססיכום – מחלקת String6העמסת אופרטורים
מבוא להעמסת אופרטוריםהעמסת אופרטורים אריתמטייםהעמסת אופרטורי השוואהאופרטורי זרםהעמסת אופרטור השמההעמסת האופרטורים [] ו־()אופרטורים להמרת טיפוסיםסיכום – מחלקת Matrix9תבניות
תבניות פונקציהתבניות מחלקההתמחות תבניותתבניות ארגומנטים משתנותיסודות SFINAE ותכונות טיפוסיםסיכום – מכל גנריתרגלו בעצמכם: קומפיילר C++ אונליין