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 لطول السلسلة (من دون احتساب المحرف الصفري الختامي)
    • باني ذي معاملات يأخذ سلسلة C (const char*)، ويخصص الذاكرة، وينسخ المحتوى، ويطبع "TextBuffer created: <text>"
    • مُدمّر يحرر الذاكرة (إذا لم تكن فارغة) ويطبع "TextBuffer destroyed"
    • باني نسخ ينفذ نسخة عميقة ويطبع "TextBuffer copied"
    • عامل إسناد النسخ يتعامل مع الإسناد الذاتي، وينظف البيانات الموجودة، وينفذ نسخة عميقة، ويطبع "TextBuffer copy-assigned". أعد *this
    • باني نقل (معلّم بـ noexcept) ينقل الملكية ويطبع "TextBuffer moved". اترك المصدر في حالة فارغة صالحة
    • عامل إسناد النقل (معلّم بـ noexcept) يتعامل مع الإسناد الذاتي، وينظف البيانات الموجودة، وينقل الملكية، ويطبع "TextBuffer move-assigned". أعد *this
    • دالة getText() تعيد النص المخزّن (أعد سلسلة فارغة "" إذا كانت data فارغة)
    • دالة 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++ عبر الإنترنت