C++/מחלקות: הבדלים בין גרסאות בדף

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי
< C++
תוכן שנמחק תוכן שנוסף
Ybungalobill (שיחה | תרומות)
אין תקציר עריכה
Ybungalobill (שיחה | תרומות)
אין תקציר עריכה
שורה 13: שורה 13:
};
};
</source></div>
</source></div>
המילה public מצינת שחברי המחלקה המוגדרים בהמשך יהיו ציבוריים, כלומר מכל מקום בתוכנית נוכל להשתמש בהם כמו במבנה רגיל. בעצם הגדרת מחלקה זו שקולה להגדרת מבנה דומה (struct במקום class). שלושת משתני המחלקה (d, m, y) מיצגים את התאריך באמצעות יום, חודש ושנה. כיוון שנוכל לשנות אותם באופן ישיר על ידי השמה, עלולות להתעורר שגיאות נסתרות, למשל אם בטעות נכניס ל-m את הערך 13. גישה כזו נוגדת את עקרון תכנות מונחה העצמים.
המילה public מציינת שחברי המחלקה המוגדרים בהמשך יהיו ציבוריים, כלומר מכל מקום בתוכנית נוכל להשתמש בהם כמו במבנה רגיל. בעצם הגדרת מחלקה זו שקולה להגדרת מבנה דומה (struct במקום class). שלושת משתני המחלקה (d, m, y) מיצגים את התאריך באמצעות יום, חודש ושנה. כיוון שנוכל לשנות אותם באופן ישיר על ידי השמה, עלולות להתעורר שגיאות נסתרות, למשל אם בטעות נכניס ל-m את הערך 13. גישה כזו נוגדת את עקרון תכנות מונחה העצמים.


עתה, כדי לעשות את התוכנית שלנו נוחה יותר, ברורה יותר ויציבה יותר, נחסום את הגישה למשתני המחלקה. לצורך זה נשנה את המילה public ל-private, מותר למחוק אותה כלל כיוון שחברי המחלקה הבאים ראשונים לאחר הסוגרים המסולסלים מוגדרים אוטומטית כפרטיים:
עתה, כדי לעשות את התוכנית שלנו נוחה יותר, ברורה יותר ויציבה יותר, נחסום את הגישה למשתני המחלקה. לצורך זה נשנה את המילה public ל-private, מותר למחוק אותה כלל כיוון שחברי המחלקה הבאים ראשונים לאחר הסוגרים המסולסלים מוגדרים אוטומטית כפרטיים:
שורה 23: שורה 23:
};
};
</source></div>
</source></div>
כעת לא נוכל לגשת למשתני המחלקה d, m ו y באופן ישיר, לכן נבנה ממשק נוח לטיפול במשתני מחלקה זו. ממשק זה יוגדר כציבורי והוא יבטיח שהערכים של משתני המחלקה תמיד יהיו תקינים (למשל m לא יהיה גדול מ-12). כמעט כל ממשק צריך לכלול פעולות המשנות את המחלקה ופונקציות לאיחזור נתונים. בדוגמה זו נגדיר תחילה רק את כותרות הפונקציות (אתחל, הוספת יום/חודש/שנה, אחזור יום/חודש/שנה):
כעת לא נוכל לגשת למשתני המחלקה d, m ו y באופן ישיר, לכן נבנה ממשק נוח לטיפול במשתני מחלקה זו. ממשק זה יוגדר כציבורי והוא יבטיח שהערכים של משתני המחלקה תמיד יהיו תקינים (למשל m לא יהיה גדול מ-12). כמעט כל ממשק צריך לכלול פעולות המשנות את המחלקה ופונקציות לאחזור נתונים. בדוגמה זו נצהיר תחילה רק את כותרות הפונקציות (אתחול, הוספת יום/חודש/שנה, אחזור יום/חודש/שנה):
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class Date
class Date
שורה 40: שורה 40:
};
};
</source></div>
</source></div>
''הערה:'' המילה const אחרי כותרת הפונקציה מצינת שהפונקציה לא משנה את משתני המחלקה. אלה פונקציות ממשק המחזירות את הערכים של משתנים אלה.
''הערה:'' המילה const אחרי כותרת הפונקציה מציינת שהפונקציה לא משנה את משתני המחלקה. אלה פונקציות ממשק המחזירות את הערכים של משתנים אלה.


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


* '''בתוך בלוק המחלקה.''' דרך זו מקובלת יותר לפונקציות inline. לדוגמה, נגדיר את הפונקציה ()year:
* '''בתוך בלוק המחלקה.''' פונקציות אלה יהיו inline. לדוגמה, נגדיר את הפונקציה ()year:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class Date
class Date
שורה 61: שורה 61:
</source></div>
</source></div>


* '''מחוץ לבלוק המחלקה.''' בדרך זו נגדיר את רוב הפונקציות. הסיבה לכך היא שנרצה להגדיר אותן בד"כ בקובץ cpp ואילו הגדרת המחלקה עצמה תמצא בקובץ h. כדי לציין שהפונקציה שאנו מגדירים שיכת למחלקה מסוימת, נוסיף את שם המחלקה וארבע נקודות (::) לפני שמה, בזהה למרחבי שם. כדוגמה נגדיר את הפעולה ()init:
* '''מחוץ לבלוק המחלקה.''' בדרך זו נגדיר את שאר הפונקציות. הסיבה לכך היא שנרצה להגדיר אותן בד"כ בקובץ cpp ואילו הגדרת המחלקה עצמה תמצא בקובץ h. כדי לציין שהפונקציה שאנו מגדירים שייכת למחלקה מסוימת, נוסיף את שם המחלקה וארבע נקודות (::) לפני שמה, בזהה למרחבי שם. כדוגמה נגדיר את הפעולה ()init:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class Date
class Date
שורה 76: שורה 76:
</source></div>
</source></div>


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


== שימוש בטיפוס שהוגדר ==
== שימוש בטיפוס שהוגדר ==


לאחר שהגדרנו מחלקה ומימשנו את כל הפונקציות שלה (לא נעשה את זה כאן), נוכל להשתמש בה בדומה לטיפוסים בסיסיים של C++. לצורך זה עלינו להצהיר על משתנה מטיפוס זה (מופע של מחלקה) ולגשת לפונקציות ומשתני המחלקה הציבורים בדומה למבנים:
לאחר שהגדרנו מחלקה ומימשנו את כל הפונקציות שלה (לא נעשה את זה כאן), נוכל להשתמש בה בדומה לטיפוסים בסיסיים של C++. לצורך זה עלינו להצהיר על משתנה מטיפוס זה (מופע של מחלקה) ולגשת לפונקציות ומשתני המחלקה הציבוריים בדומה למבנים:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
Date dat;
Date dat;
שורה 99: שורה 99:
== הבדל בין מבנים למחלקות ==
== הבדל בין מבנים למחלקות ==


ב++C אין הבדל משמעותי בין מבנים לבין מחלקות. כל מחלקה נוכל לשכתב בקלות למבנה וכל מבנה נוכל לשכתב בקלות למחלקה. ההבדל היחידי הוא שחברי המחלקה הם פרטיים כברירת מחדל ואילו חברי המבנה הם ציבוריים כברירת מחדל. משמעות הדבר שנוכל לגדיר גם משתני המבנה כפרטיים ולהוסיף פונקציות לטיפול בהם. שתי ההגדרות הבאות שקולות זו לזו:
ב++C אין הבדל משמעותי בין מבנים לבין מחלקות. כל מחלקה נוכל לשכתב בקלות למבנה וכל מבנה נוכל לשכתב בקלות למחלקה. ההבדל היחידי הוא שחברי המחלקה הם פרטיים כברירת מחדל ואילו חברי המבנה הם ציבוריים כברירת מחדל. משמעות הדבר שנוכל להגדיר גם משתני המבנה כפרטיים ולהוסיף פונקציות לטיפול בהם. שתי ההגדרות הבאות שקולות זו לזו:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class myType { // ...
class myType { // ...
שורה 106: שורה 106:
הבדל זה חל גם על כללי ההורשה (למד בהמשך).
הבדל זה חל גם על כללי ההורשה (למד בהמשך).


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


== בנאים ==
== בנאים ==


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


במחלקה אנו כתבנו בפרק עלינו לקרוא ל-init בכל פעם שנרצה לאתחל את המופע. דבר זה גורר שגיאות, למשל אם נשכח לקרוא לפונקציה זו לפני השימוש הראשון במשתנה Date. בדומה לאי-איתחול של כל משתנה אחר, גם במשתני המחלקה יהיו ערכים לא מוגדרים - 'זבל'. הבנאים יאפשרו לנו לכתוב פונקציה שתאתחל את המופע של המחלקה באופן אוטומטי באמצעות ערך חוקי כלשהו. הנה בנאי המאתחל את כל התאריכים ל-01/01/1970 (תוכלו לשנות בנאי זה שיאתחל את התאריך באמצעות התאריך של היום, ע"י שימוש בפונקציות מתאימות מהסיפריה ctime):
מחלקה אותה כתבנו בפרק, עלינו לקרוא ל-init בכל פעם שנרצה לאתחל את המופע שלה. דבר זה גורר שגיאות, למשל אם נשכח לקרוא לפונקציה זו לפני השימוש הראשון במשתנה Date. בדומה לאי-אתחול של כל משתנה אחר, גם במשתני המחלקה יהיו ערכים לא מוגדרים - 'זבל'. הבנאים יאפשרו לנו לכתוב פונקציה שתאתחל את המופע של המחלקה באופן אוטומטי באמצעות ערך חוקי כלשהו. הנה בנאי המאתחל את כל התאריכים ל-01/01/1970 (תוכלו לשנות בנאי זה שיאתחל את התאריך באמצעות התאריך של היום, ע"י שימוש בפונקציות מתאימות מהסיפריה ctime):
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class Date
class Date
שורה 126: שורה 126:
};
};
</source></div>
</source></div>
הבנאים של כל מחלקה יקראו על שם המחלקה, במקרה שלנו Date, אך הם אף פעם לא יחזירו ערך ולכן גם לא נכתוב את טיפוס הערך המוחזר, גם לא void. דבר זה מובן מאילו כאשר נתבונן בדרך בה אנו נפעילים את הבנאי, הרי זו הצהרה רגילה על משתנה past (הבנאי מופעל אוטומטית):
הבנאים של כל מחלקה יקראו על שם המחלקה, במקרה שלנו Date, אך הם אף פעם לא יחזירו ערך ולכן גם לא נכתוב את טיפוס הערך המוחזר, גם לא void. דבר זה מובן מאילו כאשר נתבונן בדרך בה אנו מפעילים את הבנאי, הרי זו הצהרה רגילה על משתנה past (הבנאי מופעל אוטומטית):
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
Date past; // כאן מופעל הבנאי
Date past; // כאן מופעל הבנאי
cout << past.day() << '.' << past.month() << '.' << past.year();
cout << past.day() << '.' << past.month() << '.' << past.year();
</source></div>
</source></div>
בדוגמה זו יופיע תמיד בפלט 1.1.1970.
בדוגמה זו יופיע תמיד בפלט 1.1.1970. ניתן להתייחס לבנאי כלפונקציה שמחזירה את האובייקט החדש ולכן, גם מנקודת מבט זו, לא נציין את הטיפוס המוחזר.


''הערה:'' לא נוכל לקרוא לבנאי באופן ישיר {{קוד בשורה|past.Date()}} דבר זה נחשב לשגיאה, כי כל אובייקט מאותחל רק פעם אחת.
''הערה:'' לא נוכל לקרוא לבנאי באופן ישיר {{קוד בשורה|past.Date()}} דבר זה נחשב לשגיאה, כי כל אובייקט מאותחל אך ורק פעם אחת.


נוכל להוסיף פרמטרים לבנאים שאנו כותבים, נזכור שהעמסת בנאים מותרת כמו העמסת שאר הפונקציות. לכן נוכל להגדיר בנאי המאתחל את האוביקט באמצעות הערכים הנמסרים לו כפרמטרים, הוא יחליף לנו את הפונקציה init. בדוגמה הבאה יש להוסיף את בדיקת תקינות הפרמטרים ולהחליט על דרך פעולה כלשהי כאשר הפרמטרים לא תקינים (למשל לזרוק חריגה):
נוכל להוסיף פרמטרים לבנאים שאנו כותבים, נזכור שהעמסת בנאים מותרת כמו העמסת שאר הפונקציות. לכן נוכל להגדיר בנאי המאתחל את האובייקט באמצעות הערכים הנמסרים לו כפרמטרים, הוא יחליף לנו את הפונקציה init. בדוגמה הבאה יש להוסיף את בדיקת תקינות הפרמטרים ולהחליט על דרך פעולה כלשהי כאשר הפרמטרים לא תקינים (למשל לזרוק חריגה):
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
class Date
class Date
שורה 149: שורה 149:
};
};
</source></div>
</source></div>
כאן העמסנו בנאי נוסף לבנאי הקודם. כדי למסור פרמטרים לבנאי כלשהו בעת אתחול המשתנה, נרשום אותם בסוגרים עגולים לאחר שם המשתנה (''הערה:'' אם אין פרמטרים אז אין לרשום סוגרים כלל):
כאן העמסנו בנאי נוסף לבנאי הקודם. כדי למסור פרמטרים לבנאי כלשהו בעת אתחול המשתנה, נרשום אותם בסוגריים עגולים לאחר שם המשתנה (''הערה:'' אם אין פרמטרים אז אין לרשום סוגריים כלל):
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
Date future(1,1,2970); // בנאי עם פרמטרים
Date future(1,1,2970); // בנאי עם פרמטרים
שורה 157: שורה 157:
== מפרקים ==
== מפרקים ==


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


למפרק ניתן את שם המחלקה שלפניו נוסיף זרקא (~). סימן זה בא לציין שמפרק הוא "לא בנאי", הזרקא בC++ היא אופרטור הפיכת הסיביות (Bitwise NOT). כיוון שהמפרק נקרא אוטומטית הוא לא יקבל פרמטרים ולא יחזיר ערך. לדוגמה, הנה חלק מממוש אפשרי למחלקת מחרוזות:
{{קצרמר}}
<div style="direction: ltr;"><source lang="cpp">
class myString
{
char *buf;
int length;

public:
// בנאי העושה עותק של מחרוזת קיימת
myString(const myString& str)
{
length = str.length;
buf = new char[length+1];
for(int i = 0; i <= length; i++)
buf[i] = str.buf[i];
}

~myString()
{
// יש לשחרר את הזיכרון שהוקצא
delete[] buf;
}

// פונקציות ממשק לעבודה עם מחרוזות ...
};
</source></div>

''הערה:'' ניתן לקרוא למפרק באופן ישיר מבלי לפנות את הזיכרון: {{קוד בשורה|str.~myString()}} אך זהו נושא מתקדם העוסק בעבודה עם זיכרון לא מאותחל.

== מצביע this ==

לחלק מהקוראים כנראה התעוררה השאלה: כיצד הפונקציה של המחלקה יודעת עבור איזה מופע של המחלקה קראו לה? בהתבוננות בקוד האסמבלי הנוצר ע"י המהדר נראה שכל פונקציה כזאת מקבלת כפרמטר נוסף את כתובת האובייקט עבורו היא נקראת. באותה דרך הצטרכו מתכנתי C לשלוח באופן ידני את המצביעים על המבנים לפונקציות העובדות איתם. לעיתים נרצה להשתמש במצביע זה גם בC++. ניתקל בבעייה זו כאשר נרצה לקשר בין אובייקטים, למשל בעבודה עם רשימות מקושרות, GUI וכד'... מצביע זה נקרא this והטיפוס שלו הוא {{קוד בשורה|(X *const)}} כאשר X הוא שם המחלקה. משמע הדבר שהמצביע עצמו הוא קבוע ולא נוכל לשנותו. (ראה דוגמה בהמשך הפרק)

== העתקת אובייקטים ==

בC++ ישנו בנאי אחד ואופרטור אחד המוגדרים כברירת מחדל עבור כל מחלקה. תפקיד הבנאי הוא לאתחל את המופע החדש ע"י אובייקט קיים ואילו תפקיד האופרטור הוא להעתיק את ערכו של אובייקט אחד לאובייקט שני. עבור טיפוס X כותרת הבנאי תהיה {{קוד בשורה|X::X(const X&)}} וכותרת האופרטור תהיה {{קוד בשורה|<nowiki>X& X::operator = (const X&)</nowiki>}} (על העמסת אופרטורים למד בהמשך). גם הבנאי וגם האופרטור, שניהם מעתיקים אחד אחד את משתני המבנה/המחלקה, דבר זה מאפשר לנו בקלות לכתוב כך:
<div style="direction: ltr;"><source lang="cpp">
Date myBirthday(18, 6, 1991);
Date tmp(myBirthday); // העתקה באמצעות בנאי
tmp.addDays(7);
tmp = myBirthday; // העתקה באמצעות אופרטור
</source></div>

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

כאשר נשתמש באופרטור ההשמה a = b המצב יהיה גרוע יותר: לאזור אחד בזיכרון, השייך במקור ל-b יצביעו שני מצביעים כמו במקרה הקודם, ובנוסף יהיה לנו אזור שני, השייך במקור ל-a, אליו לא יהיה מצביע כלל. אזור זה לא ישוחרר. נציץ טיפה קדימה לנושא העמסת אופרטורים ונדגים כיצד להגדיר אופרטור השמה למחלקה שלנו, בדוגמה זו נשתמש במצביע this כדי לחשוף את המקרים בהם אנו משימים את המשתנה לעצמו (a = a):
<div style="direction: ltr;"><source lang="cpp">
class myString
{
// ...
public:
myString& operator = (const myString& str)
{
if (&str != this) // משימים אובייקט שונה
{
delete[] buf; // לשחרר את הזיכרון הישן

length = str.length;
buf = new char[length+1];
for(int i = 0; i <= length; i++)
buf[i] = str.buf[i];
}
return *this; // להחזיר את המופע של המחלקה כדי שנוכל לשרשר השמות
}
// ...
};
</source></div>

''הערה:'' חיזרו לדוגמה זו כשתלמדו על העמסת אופרטורים.

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

גרסה מ־20:06, 21 במאי 2007

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

הגדרת מחלקה

נגדיר את המחלקה בדומה למבנה. נרשום את המילה class ואחריה את שם המחלקה:

class Date
{
public:
    int d, m, y;
};

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

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

class Date
{
private: // שורה זו ניתן למחוק
    int d, m, y;
};

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

class Date
{
    int d, m, y;

public:
    void init(int dd, int mm, int yy);
    void add_day(int n);
    void add_month(int n);
    void add_year(int n);

    int day() const;
    int month() const;
    int year() const;
};

הערה: המילה const אחרי כותרת הפונקציה מציינת שהפונקציה לא משנה את משתני המחלקה. אלה פונקציות ממשק המחזירות את הערכים של משתנים אלה.

הגדרת גופי פונקציות המחלקה

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

  • בתוך בלוק המחלקה. פונקציות אלה יהיו inline. לדוגמה, נגדיר את הפונקציה ()year:
class Date
{
    int d, m, y;

public:
    // ...

    int year() const 
    {
        return y;
    }    
};
  • מחוץ לבלוק המחלקה. בדרך זו נגדיר את שאר הפונקציות. הסיבה לכך היא שנרצה להגדיר אותן בד"כ בקובץ cpp ואילו הגדרת המחלקה עצמה תמצא בקובץ h. כדי לציין שהפונקציה שאנו מגדירים שייכת למחלקה מסוימת, נוסיף את שם המחלקה וארבע נקודות (::) לפני שמה, בזהה למרחבי שם. כדוגמה נגדיר את הפעולה ()init:
class Date
{
    // ...
};

void Date::init(int dd, int mm, int yy)
{
    d = dd;
    m = mm;
    y = yy;
}

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

שימוש בטיפוס שהוגדר

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

Date dat;
dat.init(18, 6, 2007);

int n;
cin >> n;

dat.add_day(n);
cout << dat.day() << '.' << dat.month() << '.' << dat.year();

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

dat.d = 32; // שגיאה

הבדל בין מבנים למחלקות

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

class myType { // ...
struct myType { private: // ...

הבדל זה חל גם על כללי ההורשה (למד בהמשך).

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

בנאים

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

מחלקה אותה כתבנו בפרק, עלינו לקרוא ל-init בכל פעם שנרצה לאתחל את המופע שלה. דבר זה גורר שגיאות, למשל אם נשכח לקרוא לפונקציה זו לפני השימוש הראשון במשתנה Date. בדומה לאי-אתחול של כל משתנה אחר, גם במשתני המחלקה יהיו ערכים לא מוגדרים - 'זבל'. הבנאים יאפשרו לנו לכתוב פונקציה שתאתחל את המופע של המחלקה באופן אוטומטי באמצעות ערך חוקי כלשהו. הנה בנאי המאתחל את כל התאריכים ל-01/01/1970 (תוכלו לשנות בנאי זה שיאתחל את התאריך באמצעות התאריך של היום, ע"י שימוש בפונקציות מתאימות מהסיפריה ctime):

class Date
{
    int d, m, y;

public:
    Date()
    {
        d = 1, m = 1, y = 1970;
    }
    // ...
};

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

Date past; // כאן מופעל הבנאי
cout << past.day() << '.' << past.month() << '.' << past.year();

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

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

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

class Date
{
    int d, m, y;

public:
    Date(int dd, int mm, int yy)
    {
        d = dd, m = mm, y = yy;
    }
    // ...
};

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

Date future(1,1,2970); // בנאי עם פרמטרים
Date past; // בנאי ללא פרמטרים

מפרקים

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

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

class myString
{
    char *buf;
    int length;

public:
    // בנאי העושה עותק של מחרוזת קיימת
    myString(const myString& str)
    {
        length = str.length;
        buf = new char[length+1];
        for(int i = 0; i <= length; i++)
            buf[i] = str.buf[i];
    }

    ~myString()
    {
        // יש לשחרר את הזיכרון שהוקצא
        delete[] buf;
    }

    // פונקציות ממשק לעבודה עם מחרוזות ...
};

הערה: ניתן לקרוא למפרק באופן ישיר מבלי לפנות את הזיכרון: str.~myString() אך זהו נושא מתקדם העוסק בעבודה עם זיכרון לא מאותחל.

מצביע this

לחלק מהקוראים כנראה התעוררה השאלה: כיצד הפונקציה של המחלקה יודעת עבור איזה מופע של המחלקה קראו לה? בהתבוננות בקוד האסמבלי הנוצר ע"י המהדר נראה שכל פונקציה כזאת מקבלת כפרמטר נוסף את כתובת האובייקט עבורו היא נקראת. באותה דרך הצטרכו מתכנתי C לשלוח באופן ידני את המצביעים על המבנים לפונקציות העובדות איתם. לעיתים נרצה להשתמש במצביע זה גם בC++. ניתקל בבעייה זו כאשר נרצה לקשר בין אובייקטים, למשל בעבודה עם רשימות מקושרות, GUI וכד'... מצביע זה נקרא this והטיפוס שלו הוא (X *const) כאשר X הוא שם המחלקה. משמע הדבר שהמצביע עצמו הוא קבוע ולא נוכל לשנותו. (ראה דוגמה בהמשך הפרק)

העתקת אובייקטים

בC++ ישנו בנאי אחד ואופרטור אחד המוגדרים כברירת מחדל עבור כל מחלקה. תפקיד הבנאי הוא לאתחל את המופע החדש ע"י אובייקט קיים ואילו תפקיד האופרטור הוא להעתיק את ערכו של אובייקט אחד לאובייקט שני. עבור טיפוס X כותרת הבנאי תהיה X::X(const X&) וכותרת האופרטור תהיה X& X::operator = (const X&) (על העמסת אופרטורים למד בהמשך). גם הבנאי וגם האופרטור, שניהם מעתיקים אחד אחד את משתני המבנה/המחלקה, דבר זה מאפשר לנו בקלות לכתוב כך:

Date myBirthday(18, 6, 1991);
Date tmp(myBirthday); // העתקה באמצעות בנאי
tmp.addDays(7);
tmp = myBirthday; // העתקה באמצעות אופרטור

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

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

class myString
{
    // ...
public:
    myString& operator = (const myString& str)
    {
        if (&str != this) // משימים אובייקט שונה
        {
            delete[] buf; // לשחרר את הזיכרון הישן

            length = str.length;
            buf = new char[length+1];
            for(int i = 0; i <= length; i++)
                buf[i] = str.buf[i];
        }
        return *this; // להחזיר את המופע של המחלקה כדי שנוכל לשרשר השמות
    }
    // ...
};

הערה: חיזרו לדוגמה זו כשתלמדו על העמסת אופרטורים.

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