قاعدة الثلاثة في C++: ما يجب أن يعرفه كل مطور

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

ما هي قاعدة الثلاثة في C++؟ نشرحها بالأمثلة



إذا سبق لك أن كتبت في C++ كلاسًا يدير الذاكرة الديناميكية (على سبيل المثال، يحتوي على new أو malloc)، فمن المؤكد أنك واجهت قاعدة الثلاثة (Rule of Three). إنها واحدة من أكثر القواعد الأساسية في اللغة، والتي تنقذ من تسرب الذاكرة، والحذف المزدوج، والسلوك غير المحدد. دعنا نشرحها بأمثلة واقعية، حتى لا تحفظ القاعدة فحسب، بل تفهم لماذا تعمل.



تخيل: لقد كتبت كلاسًا يخصص ذاكرة في المنشئ. يقوم المترجم بلطف بتوليد منشئ النسخ وعامل الإسناد لك. لكنه يفعل ذلك بشكل سطحي (shallow copy). وهنا تبدأ المشاكل. تقول قاعدة الثلاثة: إذا كان الكلاس الخاص بك يُعرّف بشكل صريح المدمر، أو منشئ النسخ، أو عامل الإسناد، فمن المحتمل أنك بحاجة إلى تعريف الثلاثة جميعًا. لماذا؟ لأن هذه إشارة إلى أن الكلاس الخاص بك يدير موردًا ما (ذاكرة، ملف، اتصال شبكة)، ويجب أن يكون النسخ عميقًا.



المشكلة: النسخ السطحي (shallow copy)

دعنا نلقي نظرة على مثال نموذجي. كلاس String، الذي يخزن سلسلة C في الذاكرة الديناميكية:



class String {private:    char* data;    size_t size;public:    // المنشئ    String(const char* str) {        size = strlen(str);        data = new char[size + 1];        strcpy(data, str);    }

// المدمر ~String() { delete[] data; }

// منشئ النسخ — تم توليده بواسطة المترجم (سطحي) // عامل الإسناد — تم توليده بواسطة المترجم (سطحي)

void print() const { std::cout << data << std::endl; }};


يبدو كل شيء جيدًا، حتى نحاول نسخ الكائن:



int main() {    String s1("Hello");    String s2 = s1;  // استدعاء منشئ النسخ (المولد)    s1.print();    s2.print();    // سيتعطل البرنامج مع خطأ double free!    return 0;}


ماذا حدث؟ قام المترجم بتوليد منشئ نسخ قام ببساطة بنسخ المؤشر data. الآن s1.data و s2.data يشيران إلى نفس كتلة الذاكرة. عندما تخرج الكائنات عن النطاق، يتم استدعاء مدمراتها. المدمر الأول يحرر الذاكرة، والثاني يحاول تحريرها مرة أخرى — double free. يتعطل البرنامج أو يتصرف بشكل غير متوقع.



الحل: قاعدة الثلاثة في العمل

لإصلاح الموقف، نحتاج إلى تنفيذ جميع دوال الأعضاء الثلاث الخاصة. هذا هو قاعدة الثلاثة. دعنا نكتب النسخة الصحيحة من الكلاس:



class String {private:    char* data;    size_t size;public:    // المنشئ    String(const char* str) {        size = strlen(str);        data = new char[size + 1];        strcpy(data, str);    }

// المدمر ~String() { delete[] data; }

// منشئ النسخ — نسخ عميق String(const String& other) { size = other.size; data = new char[size + 1]; strcpy(data, other.data); }

// عامل الإسناد — نسخ عميق String& operator=(const String& other) { if (this == &other) return *this; // حماية من الإسناد الذاتي

delete[] data; // تحرير الموارد القديمة

size = other.size; data = new char[size + 1]; strcpy(data, other.data);

return *this; }

void print() const { std::cout << data << std::endl; }};


الآن يعمل النسخ بشكل صحيح: كل كائن يمتلك كتلة الذاكرة الخاصة به. الإسناد أيضًا بدون

المدونات

توصيات الكتب