Smart-World Surf

יחידה 5: פולימורפיזם

גמישות ודינמיות באמצעות התנהגות שונה לאובייקטים מאותו סוג.
פונקציות וירטואליותמחלקה מופשטת (Abstract Class)ממשקים (Interfaces)קשירה דינמית (Dynamic Binding)

ברוכים הבאים ליחידת הלימוד על פולימורפיזם, אחד מעקרונות הליבה החשובים ביותר בתכנות מונחה עצמים. יחידה זו תעסוק ביכולת המערכת להציג התנהגויות שונות לאובייקטים מאותו "סוג" (או ממשק משותף), ובכך להעניק גמישות ודינמיות רבה לקוד. נחקור את המנגנונים המאפשרים זאת, בדגש על פונקציות וירטואליות, מחלקות מופשטות, ממשקים וקשירה דינמית, תוך התייחסות לאופי הבחינות בטכניון.

מהו פולימורפיזם? עקרונות יסוד

פולימורפיזם (מיוונית: "ריבוי צורות") הוא העיקרון המאפשר לאובייקטים מסוגים שונים להגיב לאותה קריאה למתודה בצורה שונה, בהתאם לסוגם האמיתי בזמן ריצה. בבסיסו עומדת היכולת להתייחס לאובייקטים נגזרים באמצעות מצביע או רפרנס למחלקת הבסיס שלהם, ולגרום לקריאה למתודה המתאימה לסוג האובייקט בפועל.

פולימורפיזם: היכולת של אובייקטים מסוגים שונים להגיב לאותה קריאה למתודה בצורה שונה, בהתאם לסוגם האמיתי בזמן ריצה. הוא מאפשר לכתוב קוד גנרי וגמיש יותר, המטפל באובייקטים מרובים בצורה אחידה.

עמודי התווך של הפולימורפיזם ב-C++

פונקציות וירטואליות (Virtual Functions)

ב-C++, פונקציות וירטואליות הן המנגנון העיקרי המאפשר פולימורפיזם. כאשר פונקציה מוצהרת כ-virtual במחלקת בסיס, היא מאפשרת למחלקות נגזרות לדרוס (override) את המימוש שלה, ולקריאה למתודה להתבצע בהתאם לסוג האובייקט בזמן ריצה, ולא לסוג המצביע או הרפרנס.

פונקציה וירטואלית: מתודה המוצהרת במחלקת בסיס עם המילה השמורה virtual. היא מאפשרת למתודות במחלקות נגזרות לדרוס אותה, כך שהקריאה למתודה תתבצע בהתאם לסוג האובייקט בזמן ריצה (קשירה דינמית).

קשירה דינמית (Dynamic Binding)

קשירה דינמית היא התהליך שבו ההחלטה איזו מתודה ספציפית תופעל נקבעת בזמן ריצה. היא מתרחשת כאשר קוראים לפונקציה וירטואלית דרך מצביע או רפרנס למחלקת בסיס, והמערכת "מסתכלת" על הסוג האמיתי של האובייקט כדי לבחור את המימוש הנכון. זאת בניגוד לקשירה סטטית (Static Binding), שבה ההחלטה מתקבלת בזמן קומפילציה.

קשירה דינמית (Dynamic Binding): תהליך קביעת המתודה שתופעל בזמן ריצה, בהתבסס על הסוג האמיתי של האובייקט ולא על סוג המצביע או הרפרנס דרכו קוראים למתודה. זהו המנגנון המאפשר פולימורפיזם.

מחלקה מופשטת (Abstract Class)

מחלקה מופשטת היא מחלקת בסיס המכילה לפחות פונקציה וירטואלית טהורה אחת (pure virtual function). פונקציה וירטואלית טהורה מוצהרת על ידי = 0 בסוף הצהרתה, ואין לה מימוש במחלקת הבסיס. מחלקה מופשטת אינה ניתנת ליצירת מופעים ישירות; היא נועדה לשמש כבסיס למחלקות נגזרות, אשר חייבות לממש את כל הפונקציות הווירטואליות הטהורות שלה כדי שיהיו ניתנות ליצירת מופעים.

מחלקה מופשטת (Abstract Class): מחלקת בסיס המכילה לפחות פונקציה וירטואלית טהורה אחת. לא ניתן ליצור מופעים ישירות ממחלקה מופשטת; היא נועדה להגדיר ממשק משותף למחלקות נגזרות.

ממשקים (Interfaces)

ב-C++, אין מילת מפתח מפורשת עבור "ממשק" כמו בשפות אחרות (Java, C#). במקום זאת, ממשק ממומש לרוב כמחלקה מופשטת לחלוטין – כלומר, מחלקה שכל פונקציותיה וירטואליות טהורות ואין לה שדות נתונים או מימוש כלשהו. מטרתו הבלעדית היא להגדיר חוזה התנהגותי שאובייקטים המממשים אותו חייבים לציית לו.

ממשק (Interface): ב-C++, לרוב ממומש כמחלקה מופשטת שכל פונקציותיה וירטואליות טהורות ואין לה שדות נתונים. מטרתו היא להגדיר חוזה התנהגותי (מה אובייקט יכול לעשות), ללא כל מימוש.

מחלקה מופשטת (Abstract Class)

יכולה להכיל גם מימוש של פונקציות רגילות, שדות נתונים, וקונסטרוקטורים. היא מספקת בסיס משותף עם התנהגות חלקית או מלאה, וגם מגדירה חוזה באמצעות פונקציות וירטואליות טהורות. משמשת לרוב כאשר יש התנהגות משותפת כלשהי.

ממשק (Interface) ב-C++

ממומש כמחלקה מופשטת שכל פונקציותיה וירטואליות טהורות ואין לה שדות נתונים. מטרתו הבלעדית היא להגדיר חוזה התנהגותי טהור (מה האובייקט יכול לעשות), ללא כל מימוש. משמש לרוב כאשר רוצים להגדיר יכולת ללא קשר להיררכיית ירושה.

יישומים, אתגרים ודגשים לבחינה

פולימורפיזם חיוני לבניית מערכות גמישות וניתנות להרחבה. הוא מאפשר, למשל, לפתח מסגרות עבודה (frameworks) שבהן קוד הבסיס יכול לקיים אינטראקציה עם אובייקטים מסוגים שונים שנוספו מאוחר יותר, מבלי לשנות את קוד הבסיס. דוגמאות נפוצות כוללות מערכות GUI, מערכות קבצים, ועיצוב תבניות כמו Strategy Pattern.

דגש לבחינה: דסטרוקטור וירטואלי: זהו נושא קריטי בבחינות בטכניון. חובה להצהיר על דסטרוקטור וירטואלי במחלקת בסיס אם יש בה פונקציות וירטואליות כלשהן, ואם קיים סיכוי שאובייקטים נגזרים יימחקו דרך מצביע למחלקת הבסיס. אי-עשייה זו תוביל לדליפת זיכרון (memory leak) עקב קריאה לדסטרוקטור הלא-נכון (של מחלקת הבסיס בלבד, ללא קריאה לדסטרוקטור של המחלקה הנגזרת).

אתגרים וטעויות נפוצות:

  • שכחת המילה virtual: קריאה לפונקציה דרך מצביע בסיס תגרום לקשירה סטטית ותפעיל את מימוש מחלקת הבסיס, גם אם האובייקט הוא מסוג נגזר.
  • ניסיון ליצור מופע ממחלקה מופשטת: זוהי שגיאת קומפילציה.
  • בעיית "חיתוך" (Slicing Problem): כאשר אובייקט נגזר מוקצה לאובייקט בסיס (לא למצביע/רפרנס), רק חלק מחלקת הבסיס מועתק, והמידע הספציפי למחלקה הנגזרת אובד.
  • חתימה שונה ב-override: אם חתימת הפונקציה במחלקה הנגזרת אינה זהה לחלוטין לזו של הפונקציה הווירטואלית במחלקת הבסיס, לא תתבצע דריסה אלא הסתרה (hiding), והפולימורפיזם לא יפעל כמצופה. שימוש ב-override (C++11 ואילך) יכול לעזור לזהות שגיאות כאלה בזמן קומפילציה.

שאלות לדיון

  • הסבירו מדוע פולימורפיזם נחשב לאחד מעקרונות הליבה של תכנות מונחה עצמים, וכיצד הוא תורם לגמישות הקוד.
  • מה ההבדל המהותי בין קשירה סטטית לקשירה דינמית? תארו מצב שבו כל אחת מהן מתרחשת.
  • מתי תבחרו להשתמש במחלקה מופשטת לעומת מחלקת בסיס קונקרטית (שאינה מופשטת)? תנו דוגמה.
  • הסבירו את החשיבות של דסטרוקטור וירטואלי במחלקות בסיס המכילות פונקציות וירטואליות. תארו תרחיש שבו היעדרו יוביל לבעיה.
  • כיצד ממשקים (כפי שהם ממומשים ב-C++) מאפשרים "התנהגות מרובה" לאובייקטים שאינם בהכרח באותה היררכיית ירושה ישירה?

נקודות לתשובת מודל

  • פולימורפיזם: מאפשר טיפול אחיד באובייקטים שונים דרך ממשק משותף, תוך התאמת ההתנהגות לסוג האמיתי של האובייקט בזמן ריצה. משפר גמישות, הרחבה ותחזוקתיות.
  • פונקציות וירטואליות: המנגנון ב-C++ המאפשר קשירה דינמית. מוגדרות עם המילה virtual במחלקת הבסיס.
  • קשירה דינמית: קביעת המתודה שתופעל בזמן ריצה, על בסיס סוג האובייקט בפועל. מתרחשת עם פונקציות וירטואליות דרך מצביעים/רפרנסים למחלקת בסיס.
  • מחלקה מופשטת: מחלקת בסיס עם לפחות פונקציה וירטואלית טהורה (= 0). לא ניתנת ליצירת מופעים, משמשת להגדרת ממשק חלקי או מלא למחלקות נגזרות.
  • ממשק (ב-C++): מחלקה מופשטת לחלוטין (כל הפונקציות וירטואליות טהורות, ללא שדות נתונים). מגדיר חוזה התנהגותי בלבד.
  • דסטרוקטור וירטואלי: חיוני למניעת דליפות זיכרון בעת מחיקת אובייקט נגזר דרך מצביע למחלקת בסיס, כדי להבטיח קריאה נכונה לדסטרוקטור של המחלקה הנגזרת.
  • יתרונות: קוד גנרי, קל להרחבה (ניתן להוסיף סוגים חדשים מבלי לשנות קוד קיים), הפרדת אחריויות, תחזוקה קלה יותר.
  • דגש בחינה: הבנה מעמיקה של זרימת הבקרה בקוד פולימורפי, השפעת מילת המפתח virtual, והשלכות זיכרון (כמו טבלת פונקציות וירטואליות - vtable).
מצאתם טעות או שחסר משהו?
→ הקודמת
ירושה
הבאה ←
העמסת אופרטורים