פונקציות וירטואליות ו־VTable
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 52 מתוך 104.
מילת המפתח virtual פותרת את הבעיה שראינו בשיעור הקודם. כשמכריזים על מתודה כווירטואלית, C++ קובעת איזו גרסה לקרוא על סמך סוג האובייקט בפועל בזמן ריצה, ולא על סמך סוג המצביע.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // פלט: Woof!כעת המתודה הנכונה נקראת! מילת המפתח override היא אופציונלית, אך מומלצת. היא מודיעה למהדר שבכוונתך לדרוס פונקציה וירטואלית, ומאפשרת לזהות שגיאות אם החתימות אינן תואמות.
איך זה עובד? כאשר למחלקה יש פונקציות וירטואליות, המהדר יוצר טבלה וירטואלית (vtable). זוהי טבלת חיפוש מוסתרת שמכילה מצביעים לפונקציות הווירטואליות של המחלקה. כל אובייקט עם פונקציות וירטואליות שומר מצביע מוסתר (vptr) לטבלה הווירטואלית של המחלקה שלו.
כשקוראים לפונקציה וירטואלית דרך מצביע, התוכנית מחפשת את כתובת הפונקציה הנכונה ב־vtable בזמן ריצה. הדבר נקרא שליחה דינמית. הוא מוסיף תקורה קטנה בהשוואה לקריאות רגילות לפונקציות, אך מאפשר התנהגות פולימורפית עוצמתית.
כלל חשוב אחד: אם למחלקה יש פונקציות וירטואליות והיא תשמש כמחלקת בסיס, גם ההורס שלה צריך להיות וירטואלי. כך מובטח ניקוי תקין כשמוחקים אובייקטים נגזרים דרך מצביעים למחלקת הבסיס:
class Animal {
public:
virtual ~Animal() {} // מפרק וירטואלי
virtual void speak() {}
};אתגר
קלבואו נבנה מערכת נגן מדיה שמדגימה את העוצמה של פונקציות וירטואליות ופולימורפיזם בזמן ריצה. תיצרו היררכיה של סוגי מדיה, שבה כל סוג מתנגן באופן שונה, ותראו כיצד פונקציות וירטואליות מאפשרות את ההתנהגות הנכונה גם כשניגשים אליהן דרך מצביעים למחלקת בסיס.
תארגנו את הקוד בשלושה קבצים:
Media.h: הגדירו מחלקת בסיסMediaשמייצגת כל מדיה שניתן לנגן:- חבר
std::string titleמוגן - בנאי שמקבל כותרת ושומר אותה
- מתודת
play()virtual שמדפיסה:Playing media: <title> - מפרק וירטואלי שמדפיס:
Media [<title>] destroyed
- חבר
AudioTrack.h: הגדירו מחלקתAudioTrackשיורשת באופן ציבורי מ-Media:- חבר
std::string artistפרטי - בנאי שמקבל כותרת ושם אמן, מעביר את הכותרת למחלקת הבסיס ושומר את שם האמן
- דרסו את
play()באמצעות מילת המפתחoverrideכדי להדפיס:Playing audio: <title> by <artist> - מפרק שמדפיס:
AudioTrack [<title>] destroyed
- חבר
main.cpp: קראו שלוש קלטים (כל אחד בשורה נפרדת):- כותרת של רצועת שמע (מחרוזת)
- שם האמן (מחרוזת)
- כותרת הסרטון (מחרוזת)
הגדירו ישירות בתוך main.cpp מחלקת
VideoClipשיורשת באופן ציבורי מ-Media:- חבר
int durationפרטי (בשניות) - בנאי שמקבל כותרת ומשך (משך ברירת המחדל הוא 120)
- דרסו את
play()כדי להדפיס:Playing video: <title> (<duration>s) - מפרק שמדפיס:
VideoClip [<title>] destroyed
צרו מערך של שלושה מצביעי
Media*בתוך תחום בלוק. הקצו באופן דינמי:- אובייקט בסיס מסוג
Mediaעם הכותרת "Generic Media" - אובייקט
AudioTrackעם הכותרת ושם האמן מהקלט - אובייקט
VideoClipעם כותרת הסרטון מהקלט
עברו בלולאה על המערך וקראו ל-
play()עבור כל מצביע. לאחר מכן מחקו את כל האובייקטים בסדר הפוך. לאחר הבלוק, הדפיסו:Playback complete!
לדוגמה, עבור הקלטים Bohemian Rhapsody, Queen ו-Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!שימו לב כיצד קריאה ל-play() דרך מצביעי Media* מפעילה את המתודה הנכונה של המחלקה הנגזרת, הודות לפונקציות וירטואליות. שימו לב גם כיצד המפרק הווירטואלי מבטיח ניקוי תקין: גם המפרקים של המחלקה הנגזרת וגם של מחלקת הבסיס מופעלים כשמוחקים דרך מצביע למחלקת בסיס.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: הגדירו כאן מחלקה בשם VideoClip שיורשת באופן ציבורי מ-Media
// - שדה duration פרטי מסוג int (בשניות)
// - הבנאי מקבל כותרת ומשך זמן (ברירת המחדל של משך הזמן היא 120)
// - דרסו את play() כך שתדפיס: Playing video: <title> (<duration>s)
// - הורס המחלקה מדפיס: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: ממשו את הבנאי
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: שמרו את משך הזמן
}
// TODO: דרסו את המתודה play()
void play() override {
// TODO: ממשו את המתודה הזו
}
// TODO: ממשו את הורס המחלקה
~VideoClip() {
// TODO: ממשו את הורס המחלקה הזה
}
};
int main() {
// קראו את הקלטים
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: ליצור תחום בלוק באמצעות סוגריים מסולסלים
// בתוך הבלוק:
// 1. ליצור מערך של שלושה מצביעי Media*
// 2. להקצות באופן דינמי:
// - אובייקט בסיס מסוג Media עם הכותרת "Generic Media"
// - אובייקט מסוג AudioTrack עם הכותרת והאמן שהתקבלו כקלט
// - אובייקט מסוג VideoClip עם כותרת הסרטון שהתקבלה כקלט
// 3. לעבור על המערך ולקרוא ל־play() בכל מצביע
// 4. למחוק את כל האובייקטים בסדר הפוך
// TODO: אחרי הבלוק, להדפיס: Playback complete!
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++ אונליין