כלל השלושה / החמישה / האפס
חלק מהיחידה תכנות מונחה עצמים במסלול ה-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. אופרטור השמה בהעברה
};שני אופרטורי ההשמה פועלים לפי דפוס אחד: בדיקה שאין השמה עצמית, שחרור המשאב הישן, לקיחת המשאב החדש והחזרת *this:
Buffer& Buffer::operator=(const Buffer& other) { // השמה בהעתקה
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
Buffer& Buffer::operator=(Buffer&& other) noexcept { // השמה בהעברה
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}כלל האפס הוא הגישה הפשוטה ביותר: אם המחלקה שלך לא מנהלת משאבים ישירות, אל תגדיר אף אחת מהפונקציות האלה. תן למהדר ליצור אותן, או השתמש במצביעים חכמים ובמכולות סטנדרטיות שמטפלות במשאבים עבורך.
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++ אונליין