Menu

בנאים ב-C++: לאתחל אובייקטים בדרך הנכונה

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

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

מה זה בנאי

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

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

ה-Counter() בלי פרמטרים נקרא בנאי ברירת מחדל (default constructor): זה הבנאי שמשמש כשיוצרים אובייקט בלי להעביר ארגומנטים.

בנאים עם פרמטרים

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

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

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

רשימות אתחול של חברים

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

התחביר הוא נקודתיים אחרי רשימת הפרמטרים, ואחריהם זוגות member(value):

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

רשימת האתחול היא לא רק אופטימיזציה; היא חובה בשלושה מקרים, כי הגוף יכול רק לבצע השמה, לא לאתחל:

  • חברים const: אי אפשר לבצע השמה ל-const אחרי שהוא קיים.
  • חברים שהם הפניות: הפניה חייבת להיקשר ברגע שהיא נולדת.
  • חברים שלטיפוס שלהם אין בנאי ברירת מחדל.
class Sensor {
    const int id;        // חבר const
    int& slot;           // חבר שהוא הפניה

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // ניסיון לקבוע את id או את slot בתוך הגוף לא היה מתהדר.
};

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

ארגומנטים ברירת מחדל ובנאים מאצילים

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

היזהרו כשמשלבים בנאי עם פרמטרים שיש להם ברירות מחדל יחד עם בנאי ברירת מחדל נפרד Point(): המהדר לא יכול לדעת לאיזה מהם לקרוא עבור Point p; וידווח על עמימות. בחרו גישה אחת.

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

בנאי ההעתקה

כשיוצרים אובייקט כעותק של אובייקט אחר, בהעברה לפי ערך, בהחזרה, או בכתיבת Foo b = a;, רץ בנאי ההעתקה (copy constructor). החתימה שלו מקבלת הפניית const לאותו טיפוס:

ClassName(const ClassName& other);

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

המלכודת הגדולה נמצאת בפרק הבא, על זיכרון: אם המחלקה שלכם מחזיקה מצביע גולמי לזיכרון ב-heap, בנאי ההעתקה שבברירת המחדל מעתיק את המצביע, לא את הנתונים. כך שני אובייקטים מצביעים בסוף על אותו זיכרון, ושניהם ינסו לשחרר אותו. זה באג ה-double-free. כלל האצבע הוא Rule of Three/Five: אם אתם כותבים destructor מותאם אישית, כמעט בוודאות תצטרכו גם בנאי העתקה (והשמת העתקה) מותאמים אישית. ב-C++ מודרנית התיקון הנקי יותר הוא להחזיק std::vector או smart pointer, כך שההעתקה שהמהדר מייצר פשוט עובדת.

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

הבא בתור: Destructors

בנאי מקים אובייקט; destructor מפרק אותו. כשאובייקט יוצא מהטווח או נמחק, ה-destructor שלו רץ אוטומטית: המקום המושלם לשחרר קבצים, חיבורי רשת או זיכרון heap שהאובייקט החזיק. העמוד הבא מסביר איך destructors עובדים, מתי בדיוק הם מופעלים, ואיך הם משתלבים עם בנאים כדי לתת ל-C++ את דפוס ה-RAII החזק שלה.

שאלות נפוצות

מה זה בנאי ב-C++?

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

מה ההבדל בין בנאי ברירת מחדל לבנאי עם פרמטרים?

בנאי ברירת מחדל לא מקבל ארגומנטים, ומשתמשים בו כשיוצרים אובייקט בלי לספק ערכים (Point p;). בנאי עם פרמטרים מקבל ארגומנטים, כך שהקורא יכול לאתחל את האובייקט עם ערכים מסוימים (Point p(3, 4);). למחלקה יכולים להיות שניהם, כי בנאים מועמסים לפי רשימות הפרמטרים שלהם.

למה כדאי להשתמש ברשימת אתחול של חברים ב-C++?

רשימת אתחול של חברים (: name(n), age(a)) מאתחלת את החברים ישירות, לפני שגוף הבנאי רץ. היא חובה עבור חברים const, הפניות וחברים בלי בנאי ברירת מחדל, והיא חוסכת את הבזבוז של בנייה בברירת מחדל ואז השמה, שקורה כשמבצעים השמה בתוך הגוף.

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

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

להתחיל