Menu

ירושה ב-C++: מחלקות בסיס ומחלקות נגזרות

איך ירושה ב-C++ מאפשרת למחלקה נגזרת להשתמש מחדש במחלקת בסיס ולהרחיב אותה: התחביר, ירושה public מול private, סדר הבנאים וההורסים, ומלכודות כמו object slicing.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שימוש חוזר במחלקה על ידי הרחבתה

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

המחלקה הקיימת היא מחלקת הבסיס (או ההורה), והחדשה היא המחלקה הנגזרת (או הילד). מחלקה נגזרת מתחילה עם כל הנתונים וההתנהגות של הבסיס, ואז מוסיפה או משנה את מה שמייחד אותה. זה הכלי העיקרי של C++ ליחס "is-a": Dog הוא Animal, ו-SavingsAccount הוא BankAccount.

תחביר בסיסי: class Derived : public Base

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

Dog אף פעם לא מצהיר על name או על eat(), ובכל זאת שניהם עובדים על אובייקט Dog, כי הם עברו בירושה מ-Animal. המחלקה הנגזרת חופשייה להוסיף איברים כמו bark() שהבסיס לא יודע עליהם דבר.

protected: איברים לילדים בלבד

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

חשבו על שלוש הרמות כעל טבעות מעגליות: private הוא "רק המחלקה הזו", protected הוא "המחלקה הזו והצאצאים שלה", ו-public הוא "כולם". ראו את access-specifiers לתמונה המלאה.

סדר הבנאים וההורסים

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

הפלט ממחיש את הסדר:

Animal ctor: Rex   // הבסיס נבנה ראשון
Dog ctor           // ואז החלק הנגזר
Dog dtor           // ההריסה בסדר הפוך...
Animal dtor: Rex   // ...הבסיס אחרון

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

דריסה: הגדרה מחדש של מתודת בסיס

מחלקה נגזרת יכולה להחליף מתודה שעברה בירושה על ידי הצהרה על מתודה עם אותה חתימה. עדיין אפשר להגיע למקורית דרך Base::method().

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

היזהרו מ-object slicing

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

a הוא Animal אמיתי, לא Dog עם תווית של בסיס, ולכן breed פשוט לא קיים בו. כדי לעבוד עם אובייקטים נגזרים באופן פולימורפי חייבים להשתמש בהפניה (Animal&) או במצביע (Animal*) מטיפוס הבסיס, ואף פעם לא בערך מטיפוס הבסיס. ה-slicing שקט: הקוד מתקמפל בלי בעיה ופשוט משמיט נתונים בשקט, ולכן זה אחד מבאגי הירושה שהכי קל לשחרר לייצור.

טעויות נפוצות שכדאי להימנע מהן

  • לצפות שאיברי private של הבסיס יהיו נגישים בילד. הם לא. השתמשו ב-protected לנתונים שהמחלקה הנגזרת באמת צריכה, והשאירו מצב פנימי באמת כ-private.
  • לשכוח להעביר ארגומנטים לבנאי הבסיס. אם לבסיס אין בנאי ברירת מחדל, חייבים לקרוא לו במפורש ברשימת האתחול של הבנאי הנגזר (: Base(args)).
  • לחתוך אובייקט נגזר לערך מטיפוס הבסיס. העתקה של Dog ל-Animal משמיטה את כל מה שייחודי ל-Dog. העבירו ושמרו במקום זאת הפניות או מצביעים לבסיס.
  • שימוש מופרז בירושה לשם שימוש חוזר בקוד. ירושה ממדלת "is-a". אם היחס הוא בעצם "has-a" (ל-Car יש Engine), העדיפו הרכבה (composition), כלומר אובייקט איבר, על פני ירושה.

הבא בתור: פונקציות וירטואליות

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

שאלות נפוצות

מהי ירושה ב-C++?

ירושה מאפשרת להגדיר מחלקה חדשה (המחלקה הנגזרת) במונחים של מחלקה קיימת (מחלקת הבסיס). המחלקה הנגזרת מקבלת אוטומטית את משתני האיברים ואת פונקציות האיברים של הבסיס, ויכולה להוסיף חדשים או להחליף התנהגות קיימת. היא ממדלת יחס "is-a" (Dog הוא Animal), והיא הדרך העיקרית ב-C++ להשתמש מחדש בקוד ולהרחיב אותו לאורך היררכיות של מחלקות.

מה ההבדל בין ירושה public לירושה private ב-C++?

בירושה public (class Dog : public Animal) הממשק הציבורי של הבסיס נשאר ציבורי במחלקה הנגזרת, כך ש-Dog הוא Animal ואפשר להשתמש בו בכל מקום שבו מצופה Animal. בירושה private האיברים שעוברים בירושה הופכים לפרטיים: המחלקה הנגזרת משתמשת במימוש של הבסיס אבל לא יכולה לבוא במקומו. ירושה ציבורית היא המקרה הנפוץ בהרבה, ובירושה פרטית משתמשים רק לשימוש חוזר מסוג "implemented-in-terms-of".

באיזה סדר רצים בנאים והורסים בירושה ב-C++?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל