Menu
CoddyTech

כלל השלושה / החמישה / האפס

חלק מהיחידה תכנות מונחה עצמים במסלול ה-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) {}
    // אין צורך בפונקציות להשמדה, להעתקה או להעברה!
};

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

challenge icon

אתגר

קל

בואו נבנה מחלקה בשם 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;
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: קומפיילר C++ אונליין