פונקציות וירטואליות טהורות
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 59 מתוך 104.
פונקציה וירטואלית טהורה היא פונקציה וירטואלית שאין לה מימוש במחלקת הבסיס. היא מוכרזת באמצעות הצמדת = 0 להצהרת הפונקציה. כך מודיעים למהדר שמחלקות נגזרות חייבות לספק מימוש משלהן.
class Shape {
public:
virtual double area() = 0; // פונקציה וירטואלית טהורה
virtual ~Shape() = default;
};בניגוד לפונקציות וירטואליות רגילות שמספקות התנהגות ברירת מחדל, פונקציות וירטואליות טהורות מגדירות חוזה: כל מחלקה נגזרת קונקרטית חייבת לממש את הפונקציה הזאת כדי שניתן יהיה ליצור ממנה מופעים. מחלקת הבסיס רק מצהירה מה יש לעשות, ולא איך.
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() override {
return width * height;
}
};גם Circle וגם Rectangle חייבות לממש את area(), משום שהיא וירטואלית טהורה ב-Shape. אם מחלקה נגזרת לא מממשת את כל הפונקציות הווירטואליות הטהורות, גם היא הופכת למחלקה אבסטרקטית ואי אפשר ליצור ממנה מופעים.
פונקציות וירטואליות טהורות חיוניות כאשר מחלקת הבסיס אינה יכולה לספק מימוש ברירת מחדל בעל משמעות. לכל צורה יש שטח, אך אין דרך הגיונית לחשב „שטח של צורה כללית” בלי לדעת מהו סוג הצורה הספציפי.
אתגר
קלבואו נבנה מערכת לעיבוד תשלומים שמדגימה כיצד פונקציות וירטואליות טהורות אוכפות חוזה בין שיטות תשלום שונות. תיצרו מחלקת בסיס מופשטת שמגדירה מה כל מעבד תשלומים חייב לעשות, ואז תממשו סוגי תשלום קונקרטיים שמקיימים את החוזה הזה.
תארגנו את הקוד שלכם בשלושה קבצים:
PaymentProcessor.h: הגדירו מחלקה מופשטת בשםPaymentProcessorשתשמש כתבנית לכל שיטות התשלום. למחלקה הזו צריכים להיות:- איבר מוגן
std::string accountId - בנאי שמאתחל את מזהה החשבון
- מתודה וירטואלית טהורה
processPayment(double amount): כל סוג תשלום חייב לממש אותה באופן שונה - מתודה וירטואלית טהורה
getProcessorName()שמחזירהstd::string - הורס וירטואלי
- איבר מוגן
PaymentMethods.h: ממשו שני מעבדי תשלומים קונקרטיים שיורשים מ-PaymentProcessor:CreditCardProcessor:- איבר פרטי
double feePercentage(שיעור עמלת העסקה) - בנאי שמקבל מזהה חשבון ושיעור עמלה
- ממשו את
processPayment()כדי לחשב את העמלה (amount * feePercentage / 100), ואז להדפיס:Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>) - ממשו את
getProcessorName()כך שתחזיר"CreditCard"
BankTransferProcessor:- איבר פרטי
std::string bankName - בנאי שמקבל מזהה חשבון ושם בנק
- ממשו את
processPayment()כדי להדפיס:Bank Transfer [<accountId>] via <bankName>: Transferred $<amount> - ממשו את
getProcessorName()כך שתחזיר"BankTransfer"
- איבר פרטי
main.cpp: קראו ארבעה קלטים (כל אחד בשורה נפרדת):- מזהה חשבון של כרטיס אשראי
- שיעור העמלה של כרטיס האשראי (double)
- מזהה חשבון בנק
- שם הבנק
צרו את שני מעבדי התשלומים ושמרו אותם במערך של מצביעים מסוג
PaymentProcessor*. עבדו תשלום בסך100.0באמצעות כל אחד מהמעבדים, והציגו את שם המעבד לפני כל עסקה:Processing with <processorName>: <processPayment output>הדפיסו שורה ריקה בין המעבדים. בסיום, נקו את האובייקטים שהוקצו דינמית.
לדוגמה, עבור הקלטים CC-4521, 2.5, BA-7890 ו-National Bank:
Processing with CreditCard:
Credit Card [CC-4521]: Charged $100 (Fee: $2.5)
Processing with BankTransfer:
Bank Transfer [BA-7890] via National Bank: Transferred $100מכיוון של-PaymentProcessor יש פונקציות וירטואליות טהורות, אי אפשר ליצור מופע שלה ישירות: אפשר ליצור רק מימושים קונקרטיים שמקיימים את החוזה. כך מובטח שכל שיטת תשלום תספק לוגיקת עיבוד ייחודית משלה.
נסו בעצמכם
#include <iostream>
#include <string>
#include "PaymentMethods.h"
using namespace std;
int main() {
// קראו את הקלטים
string ccAccountId;
double feePercentage;
string bankAccountId;
string bankName;
getline(cin, ccAccountId);
cin >> feePercentage;
cin.ignore();
getline(cin, bankAccountId);
getline(cin, bankName);
// TODO: צרו מערך של מצביעי PaymentProcessor* עם 2 איברים
// TODO: צרו אובייקטים מסוג CreditCardProcessor ו-BankTransferProcessor
// ואחסנו אותם במערך
// TODO: עברו על המערך, ועבור כל מעבד:
// 1. הדפיסו "Processing with <processorName>:"
// 2. קראו ל-processPayment עם הסכום 100.0
// 3. הדפיסו שורה ריקה בין המעבדים (לא אחרי האחרון)
// 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++ אונליין