רב־צורתיות בזמן קומפילציה לעומת בזמן ריצה
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 56 מתוך 104.
פולימורפיזם פירושו "צורות רבות", והוא מושג מרכזי בתכנות מונחה עצמים (OOP) שמאפשר להתייחס לאובייקטים באופן אחיד, אף שהם מתנהגים באופן שונה. C++ תומכת בשני סוגים נפרדים של פולימורפיזם, שכל אחד מהם נפתר בשלב אחר של ביצוע התוכנית.
פולימורפיזם בזמן הידור (נקרא גם פולימורפיזם סטטי) מוכרע על ידי המהדר לפני שהתוכנית רצה. המהדר קובע בדיוק לאיזו פונקציה לקרוא על סמך חתימת הפונקציה. הדבר כולל העמסת פונקציות ותבניות:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // המהדר בוחר ב־print(int)
print(3.14); // המהדר בוחר ב־print(double)פולימורפיזם בזמן ריצה (נקרא גם פולימורפיזם דינמי) נקבע בזמן שהתוכנית פועלת. ההחלטה איזו פונקציה לקרוא תלויה בסוג האובייקט בפועל, ולא בסוג המצביע או ההפניה. הדבר מתאפשר באמצעות פונקציות וירטואליות:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // נקבע בזמן ריצה: "Drawing circle"הפשרה המרכזית: לפולימורפיזם בזמן קומפילציה אין תקורה בזמן ריצה, מכיוון שההחלטות מתקבלות במהלך הקומפילציה, ואילו פולימורפיזם בזמן ריצה מוסיף עלות קטנה (חיפוש בטבלת vtable), אך מספק גמישות רבה יותר בעבודה עם אובייקטים שהטיפוסים שלהם אינם ידועים עד לשלב הביצוע.
אתגר
קלבוא נבנה מערכת מחשבון שמדגימה את שני סוגי הפולימורפיזם זה לצד זה. תיצור מערכת שבה פולימורפיזם בזמן קומפילציה מטפל בסוגי קלט שונים באמצעות העמסת פונקציות, ואילו פולימורפיזם בזמן ריצה מאפשר להחליף אסטרטגיות חישוב שונות באופן דינמי.
תארגן את הקוד שלך בשלושה קבצים:
Calculator.h: הגדר מחלקת בסיסCalculatorשמייצגת כל אסטרטגיית חישוב:- מתודה וירטואלית
calculate(int a, int b)שמחזירהintומדפיסה:Base calculation: <a> ? <b>(מחזירה 0) - הורס וירטואלי
- מתודה וירטואלית
Operations.h: הגדר שתי מחלקות מחשבון נגזרות שדורסות את התנהגות החישוב:Adder: דרוס אתcalculate()כך שתדפיסAdding: <a> + <b>ותחזיר את הסכוםMultiplier: דרוס אתcalculate()כך שתדפיסMultiplying: <a> * <b>ותחזיר את המכפלה
override.main.cpp: צור מערכת שמדגימה את שני סוגי הפולימורפיזם. קרא שני קלטים של מספרים שלמים (כל אחד בשורה נפרדת).תחילה, הדגם פולימורפיזם בזמן קומפילציה על ידי יצירת שלוש פונקציות
display()מועמסות:display(int x)מדפיסה:Integer value: <x>display(double x)מדפיסה:Double value: <x>display(const std::string& x)מדפיסה:String value: <x>
לאחר מכן, הדגם פולימורפיזם בזמן ריצה על ידי יצירת מערך של מצביעי
Calculator*שמכילCalculatorבסיסי,Adderו-Multiplier. עבור על המערך וקרא ל-calculate()עם ערכי הקלט שלך, והדפס את התוצאה אחרי כל חישוב.בנה את הפלט כך:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>בחלק של זמן הקומפילציה, קרא ל-
display()עם הקלט הראשון כמספר שלם, לאחר מכן כמספר עשרוני (אותו ערך בתוספת .5), ולבסוף עם המחרוזת "Result". בסיום, שחרר את המחשבונים שהוקצו באופן דינמי.
לדוגמה, עם הקלטים 10 ו-3:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30שים לב כיצד המהדר בוחר את העמסת display() הנכונה על סמך סוג הארגומנט (החלטה בזמן קומפילציה), בעוד שהמתודה calculate() הנכונה נקבעת לפי סוג האובייקט בפועל בזמן ריצה, באמצעות מנגנון ה-vtable.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: צרו שלוש פונקציות display() בעלות עומס יתר:
// 1. display(int x) - מדפיסה "Integer value: <x>"
// 2. display(double x) - מדפיסה "Double value: <x>"
// 3. display(const std::string& x) - מדפיסה "String value: <x>"
int main() {
// קראו שני מספרים שלמים מהקלט
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: קראו ל-display() עם:
// - a כמספר שלם
// - a כ-double (הוסיפו לו 0.5)
// - המחרוזת "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: צרו מערך של מצביעים מסוג Calculator* עם 3 איברים:
// - אובייקט בסיסי מסוג Calculator
// - אובייקט מסוג Adder
// - אובייקט מסוג Multiplier
// TODO: עברו על המערך וקראו ל־calculate(a, b) על כל אחד מהאיברים,
// והדפיסו "Result: <return_value>" אחרי כל חישוב
// 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++ אונליין