פונקציות וירטואליות — מבט נוסף
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 58 מתוך 104.
כעת, לאחר שהבנו את ההבדל בין פולימורפיזם בזמן קומפילציה לפולימורפיזם בזמן ריצה, בואו נעמיק בפונקציות וירטואליות ובמילת המפתח override, שהופכת את השימוש בהן לבטוח יותר.
כשמסמנים פונקציה כ־virtual במחלקת בסיס, מחלקות נגזרות יכולות לספק מימוש משלהן. המציין override מציין במפורש למהדר שבכוונתך לדרוס פונקציה וירטואלית:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};השימוש ב־override חיוני, כי הוא מזהה שגיאות בזמן הקומפילציה. אם תאיית בטעות את שם הפונקציה באופן שגוי או תשתמש בפרמטרים שגויים, המהדר יתריע בפניך במקום ליצור פונקציה חדשה בשקט:
class Cat : public Animal {
public:
void speek() override { // שגיאת מהדר: אין פונקציה שניתן לדרוס
std::cout << "Meow!" << std::endl;
}
};המציין final מונע דריסה נוספת. השתמשו בו כאשר מחלקה נגזרת אמורה להיות האחרונה שדורסת פונקציה מסוימת:
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // שגיאה: לא ניתן לדרוס פונקציה שהוגדרה כ-final
};השתמש תמיד ב־override כשאתה דורס פונקציות וירטואליות. כך הכוונה שלך מתועדת, והמהדר יכול לוודא שאתה אכן דורס פונקציה וירטואלית קיימת.
אתגר
קלבואו נבנה מערכת התראות שמדגימה את העוצמה של מילות המפתח override ו-final עם פונקציות וירטואליות. תיצרו היררכיה של מטפלי התראות, שבה ניתן להתאים אישית שיטות מסוימות ואחרות נעולות כדי למנוע שינוי.
תארגנו את הקוד שלכם בשלושה קבצים:
Notifier.h: הגדירו מחלקת בסיסNotifierשמייצגת כל שולח התראות:- חבר
std::string recipientמוגן - בנאי שמקבל ושומר את שם הנמען
- שיטה וירטואלית
send(const std::string& message)שמדפיסה:Notifying <recipient>: <message> - שיטה וירטואלית
getType()שמחזירה את המחרוזת"Generic" - הורס וירטואלי
- חבר
EmailNotifier.h: הגדירו מחלקתEmailNotifierשיורשת מ-Notifier:- חבר
std::string domainפרטי - בנאי שמקבל נמען ודומיין, ומעביר את הנמען למחלקת הבסיס
- דרסו את
send()כדי להדפיס:Emailing <recipient>@<domain>: <message> - דרסו את
getType()וסמנו אותה ב-final. היא אמורה להחזיר"Email"
לאחר מכן הגדירו מחלקת
UrgentEmailNotifierשיורשת מ-EmailNotifier:- בנאי שמקבל נמען ודומיין, ומעביר את שניהם ל-
EmailNotifier - דרסו את
send()כדי להדפיס:[URGENT] Emailing <recipient>@<domain>: <message> - הערה: אי אפשר לדרוס כאן את
getType(), מכיוון שהיא סומנה ב-finalבתוךEmailNotifier
- חבר
main.cpp: קראו שלושה קלטים (כל אחד בשורה נפרדת):- שם הנמען
- דומיין האימייל
- תוכן ההודעה
צרו באופן דינמי שלושה אובייקטים של שולחי התראות:
Notifierבסיסי,EmailNotifierו-UrgentEmailNotifier: כולם משתמשים באותו נמען (ובאותו דומיין, לפי העניין). שמרו אותם במערך של מצביעיNotifier*.עברו בלולאה על המערך ועבור כל שולח התראות, הדפיסו את הסוג שלו באמצעות
getType(), ולאחר מכן קראו ל-send()עם ההודעה שלכם. עצבו כל רשומה כך:Type: <type> <send output>הדפיסו שורה ריקה בין כל שולח התראות. בסיום, שחררו את האובייקטים שהוקצו באופן דינמי.
לדוגמה, עם הקלטים Alice, company.com ו-Meeting at 3pm:
Type: Generic
Notifying Alice: Meeting at 3pm
Type: Email
Emailing Alice@company.com: Meeting at 3pm
Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pmשימו לב איך UrgentEmailNotifier יכולה לדרוס את send() כדי להתאים אישית את פורמט ההודעה, אבל היא יורשת את הסוג "Email" מ-EmailNotifier, מכיוון ש-getType() סומנה ב-final. השתמשו במילת המפתח override בכל השיטות שנדרסו כדי לזהות אי-התאמות בחתימות בזמן הקומפילציה.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
int main() {
// קראו את הקלטים
string recipient;
string domain;
string message;
getline(cin, recipient);
getline(cin, domain);
getline(cin, message);
// TODO: צרו מערך של מצביעי Notifier* עם 3 איברים
// TODO: צרו בהקצאה דינמית:
// - אובייקט Notifier בסיסי עם recipient
// - אובייקט EmailNotifier עם recipient ו-domain
// - אובייקט UrgentEmailNotifier עם recipient ו-domain
// TODO: עברו על המערך ועבור כל אובייקט מסוג Notifier:
// - הדפיסו "Type: " ואחריו את התוצאה של getType()
// - קראו ל-send() עם message
// - הדפיסו שורה ריקה בין האובייקטים (אך לא אחרי האחרון)
// 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++ אונליין