כלל השלושה / החמישה / האפס
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 25 מתוך 104.
כשמחלקה מנהלת משאבים כמו זיכרון דינמי, למדת שעליך להגדיר בנאי הרס מותאם אישית, בנאי העתקה ובנאי העברה. אבל יש עיקרון מנחה שעוזר לך להחליט אילו פונקציות מיוחדות של איברים לממש: כלל השלושה, החמישה והאפס.
כלל השלושה קובע: אם מגדירים אחד משלושת אלה, יש להגדיר את שלושתם:
- הורס
- בנאי העתקה
- אופרטור השמת העתקה
כלל החמישה מרחיב זאת עבור C++ מודרנית, ומוסיף פעולות העברה:
- הורס
- בנאי העתקה
- אופרטור השמה בהעתקה
- בנאי העברה
- אופרטור השמה בהעברה
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; } // 1. הורס
Buffer(const Buffer& other); // 2. בנאי העתקה
Buffer& operator=(const Buffer& other); // 3. השמת העתקה
Buffer(Buffer&& other) noexcept; // 4. בנאי העברה
Buffer& operator=(Buffer&& other) noexcept; // 5. השמת העברה
};כלל האפס הוא הגישה הפשוטה ביותר: אם המחלקה שלך אינה מנהלת משאבים ישירות, אל תגדיר אף אחת מהפונקציות האלה. תן למהדר ליצור אותן, או השתמש במצביעים חכמים ובמכולות סטנדרטיות שמנהלות עבורך את המשאבים.
class Player {
std::string name; // std::string מנהלת את הזיכרון שלה בעצמה
std::vector<int> scores; // std::vector מנהלת את המשאבים שלה
public:
Player(std::string n) : name(n) {}
// אין צורך בפונקציות הרס, העתקה או העברה!
};הקפדה על הכללים האלה מונעת באגים כמו מחיקה כפולה, דליפות זיכרון ומצביעים תלויים, שמתרחשים כאשר פעולות מסוימות מוגדרות אך אחרות חסרות.
אתגר
קלבואו נבנה מחלקה TextBuffer שפועלת לפי כלל החמישה: נממש את כל חמש פונקציות החבר המיוחדות כדי לנהל כראוי נתוני תווים שהוקצו באופן דינמי. כך נדגים כיצד פעולות העתקה והעברה פועלות יחד ליצירת מחלקה חזקה שמנהלת משאבים.
ניצור שני קבצים כדי לארגן את הקוד:
TextBuffer.h: הגדירו מחלקהTextBufferששומרת טקסט במערך תווים שהוקצה באופן דינמי. המחלקה צריכה לכלול:- חברים פרטיים: מצביע
char*בשםdataעבור תוכן הטקסט, ו-size_t lengthעבור אורך המחרוזת (לא כולל תו הסיום null) - בנאי עם פרמטרים שמקבל מחרוזת C (
const char*), מקצה זיכרון, מעתיק את התוכן ומדפיס"TextBuffer created: <text>" - הורס שמשחרר זיכרון (אם הוא אינו null) ומדפיס
"TextBuffer destroyed" - בנאי העתקה שמבצע העתקה עמוקה ומדפיס
"TextBuffer copied" - אופרטור השמת העתקה שמטפל בהשמה לעצמו, מנקה את הנתונים הקיימים, מבצע העתקה עמוקה ומדפיס
"TextBuffer copy-assigned". החזירו*this - בנאי העברה (מסומן ב-
noexcept) שמעביר בעלות ומדפיס"TextBuffer moved". השאירו את המקור במצב ריק ותקין - אופרטור השמת העברה (מסומן ב-
noexcept) שמטפל בהשמה לעצמו, מנקה את הנתונים הקיימים, מעביר בעלות ומדפיס"TextBuffer move-assigned". החזירו*this - מתודה
getText()שמחזירה את הטקסט השמור (החזירו מחרוזת ריקה""אםdataהוא null) - מתודה
getLength()שמחזירה את האורך
- חברים פרטיים: מצביע
main.cpp: הדגימו בפעולה את כל חמש פונקציות החבר המיוחדות. קראו מחרוזת טקסט מהקלט, ואז:- צרו
TextBufferבשםoriginalעם טקסט הקלט - צרו את
copiedבאמצעות בנאי ההעתקה מתוךoriginal - צרו את
anotherעם הטקסט"Temporary" - השתמשו בהשמת העתקה:
another = original - צרו את
movedבאמצעות בנאי העברה מתוךoriginalבעזרתstd::move() - צרו את
targetעם הטקסט"Target" - השתמשו בהשמת העברה:
target = std::move(copied) - הדפיסו
"--- Final State ---" - הדפיסו
"original: <text> (length: <len>)"עבור כל מאגר: original, copied, moved, another, target
- צרו
לאחר פעולות ההעברה, אובייקטי המקור (original ו-copied) אמורים להציג טקסט ריק עם אורך 0, בעוד שאובייקטי היעד מכילים את הנתונים שהועברו. כך מודגם כלל החמישה בפעולה: כל חמש הפונקציות פועלות יחד כדי להבטיח ניהול משאבים בטוח.
כללו את <cstring> עבור פונקציות מחרוזת כמו strlen ו-strcpy, ואת <utility> עבור std::move().
נסו בעצמכם
#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"
using namespace std;
int main() {
string input;
getline(cin, input);
// TODO: צור TextBuffer בשם 'original' עם טקסט הקלט
// TODO: צור את 'copied' באמצעות בנאי ההעתקה מ-'original'
// TODO: צור את 'another' עם הטקסט "Temporary"
// TODO: השתמש בהשמה בהעתקה: another = original
// TODO: צור את 'moved' באמצעות בנאי העברה מ-'original' בעזרת std::move()
// TODO: צור את 'target' עם הטקסט "Target"
// TODO: השתמש בהשמה בהעברה: target = std::move(copied)
// TODO: הדפס "--- Final State ---"
// TODO: הדפס את המצב של כל מאגר בפורמט הזה:
// "original: <text> (length: <len>)"
// הדפס עבור: original, copied, moved, another, target
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++ אונליין