C++: التحكم في النسخ

آخر تحديث: 2026-08-26

في الدرس 29، تعلمنا البانيات المتقدمة.

لكن إذا أردت نسخ الكائنات، تعيين الكائنات، أو نقل الكائنات، فإن C++ لديه مجموعة من قواعد التحكم في النسخ.

فهم هذه القواعد ضروري لكتابة فئات صحيحة.


1. قاعدة الثلاثة/الخمسة

(1) 1.1 ما هي قاعدة الثلاثة/الخمسة؟

إذا كانت فئتك تحتاج إلى تطبيق مخصص لأي من الدوال التالية، فيجب تطبيق جميع الدوال ذات الصلة بشكل مخصص:

الإصدار الدوال التي يجب إدارتها الوصف
قاعدة الثلاثة (C++98) 1. المدمرbr2. باني النسخbr3. عامل تعيين النسخ مطلوبة عند وجود ذاكرة ديناميكية
قاعدة الخمسة (C++11) قاعدة الثلاثة +br4. باني النقلbr5. عامل تعيين النقل أدخلت C++11 دلالات النقل

(2) 1.2 لماذا قاعدة الثلاثة/الخمسة؟

إذا احتجت إلى تخصيص إحداها، فهذا يعني أن الفئة لديها موارد تتطلب معاملة خاصة (مثل الذاكرة الديناميكية، مقابض الملفات).

مثال: عواقب عدم اتباع قاعدة الثلاثة (الصعوبة ⭐⭐)

▶ مثال 2: مثال برمجي (الصعوبة ⭐)

CPP
#include <iostream>
#include <cstring>

class BadString {
private:
 char* data;
 
public:
 // باني
 BadString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // ❌ لا يوجد باني نسخ مخصص، أو عامل تعيين نسخ مخصص، أو مدمر مخصص
 // سينشئ المترجم افتراضيات (نسخة سطحية)
};

int main() {
 BadString s1("Hello");
 BadString s2 = s1; // ❌ نسخة سطحية: s1.data و s2.data يشيران إلى نفس الذاكرة
 
 // عند انتهاء main، يُدمَّر s2 أولاً (يُحرر data)
 // ثم يُدمَّر s1 (يُحرر data مرة أخرى، لكنها حُرِّرت بالفعل) → انهيار!
 
 return 0;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
Hello
Hello

العاقبة: انهيار البرنامج (تحرير مزدوج لنفس الذاكرة).

الحل: اتبع قاعدة الثلاثة - طبّق جميع الدوال الثلاث بشكل مخصص.



2. عامل تعيين النسخ*

(1) 2.1 الاستخدام الأساسي

CPP
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // باني
 MyString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // باني نسخ
 MyString(const MyString& other) {
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 
 // عامل تعيين النسخ
 MyString& operator=(const MyString& other) {
 if (this != &other) { // منع التعيين الذاتي
 delete data; // تحرير الذاكرة القديمة
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 return *this;
 }
 
 // مدمر
 ~MyString() {
 delete data;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2("World");
 
 s2 = s1; // ✅ يستدعي عامل تعيين النسخ
 
 return 0;
}

💡 نقطة مهمة: يجب أن يتحقق عامل تعيين النسخ من التعيين الذاتي (if (this != &other)).



3. دلالات النقل (C++11)*

(1) 3.1 لماذا نحتاج النقل؟

إذا كان لديك كائن مؤقت (على وشك التدمير)، فإن نسخ محتواه مضيعة - من الأفضل فقط "سرقة" موارده.

مثال: استخدام النقل لتجنب النسخ غير الضرورية (الصعوبة ⭐⭐)

TEXT 📖 للعرض فقط
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // باني نقل (C++11)
 MyString(MyString&& other) noexcept {
 data = other.data; // سرقة المورد
 other.data = nullptr; // تعيين الكائن الآخر إلى قيمة فارغة
 }
 
 // عامل تعيين النقل (C++11)
 MyString& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data; // تحرير المورد القديم
 data = other.data; // سرقة المورد
 other.data = nullptr; // تعيين الكائن الآخر إلى قيمة فارغة
 }
 return *this;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = std::move(s1); // ✅ يستدعي باني النقل
 // الآن s1.data هي nullptr، و s2.data تشير إلى الذاكرة الأصلية
 
 return 0;
}

💡 نصيحة: يحوّل std::move() قيمة يسارية إلى مرجع قيمة يمنى، مما يُفعّل باني النقل أو عامل تعيين النقل.



4. = default و = delete*

(1) 4.1 استخدام = default للسماح للمترجم بتوليد التطبيقات الافتراضية*

إذا كانت فئتك لا تحتاج إلى منطق نسخ خاص، يمكنك استخدام = default للسماح للمترجم بتوليد الافتراضيات.

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // دع المترجم يُنشئ باني نسخ افتراضي، وعامل تعيين نسخ افتراضي، ومدمراً افتراضياً
 Student(const Student& other) = default;
 Student& operator=(const Student& other) = default;
 ~Student() = default;
};

int main() {
 Student s1;
 Student s2 = s1; // ✅ يستدعي باني النسخ الافتراضي
 
 return 0;
}

💡 نصيحة: إذا كانت الفئة تحتوي فقط على أنواع STL (std::string، std::vector، إلخ)، فإن استخدام = default كافٍ.


(2) 4.2 استخدام = delete لمنع النسخ*

إذا كنت لا تريد أن تكون الفئة قابلة للنسخ (مثلاً نمط Singleton)، يمكنك استخدام = delete لمنع ذلك.

CPP
#include <iostream>

class Singleton {
private:
 Singleton() {} // باني خاص
 
public:
 // منع النسخ
 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;
 
 static Singleton& getInstance() {
 static Singleton instance;
 return instance;
 }
};

int main() {
 Singleton& s = Singleton::getInstance();
 // Singleton s2 = s; // ❌ خطأ ترجمة: باني النسخ محذوف
 
 return 0;
}


5. تطبيق: فئة MyString كاملة*

▶ مثال 1: تطبيق فئة MyString كاملة (الصعوبة ⭐⭐⭐)

CPP 📖 للعرض فقط
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 int length;
 
public:
 // باني
 MyString(const char* s = "") {
 length = strlen(s);
 data = new char[length + 1];
 strcpy(data, s);
 }
 
 // باني نسخ
 MyString(const MyString& other) {
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 }
 
 // باني نقل
 MyString(MyString&& other) noexcept {
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 
 // عامل تعيين النسخ
 MyString& operator=(const MyString& other) {
 if (this != &other) {
 delete data;
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 }
 return *this;
 }
 
 // عامل تعيين النقل
 MyString& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data;
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 return *this;
 }
 
 // مدمر
 ~MyString() {
 delete data;
 }
 
 // دوال أعضاء أخرى
 void print() const {
 if (data) std::cout << data;
 }
 
 int size() const {
 return length;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = s1; // باني نسخ
 
 s1.print(); // Hello
 std::cout << std::endl;
 
 s2.print(); // Hello
 std::cout << std::endl;
 
 return 0;
}
59 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

الخرج:

TEXT 📖 للعرض فقط
(program output)

▶ مثال 3: مثال على النسخة العميقة (الصعوبة ⭐)

CPP
#include <iostream>

class Buffer {
    int* data;
    int size;
public:
    Buffer(int s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }

    // باني نسخ عميق
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        for (int i = 0; i < size; i++) data[i] = other.data[i];
    }

    int& operator[](int i) { return data[i]; }
};

int main() {
    Buffer b1(3);
    b1[0] = 1; b1[1] = 2; b1[2] = 3;

    Buffer b2 = b1; // نسخة عميقة
    b2[0] = 100;

    std::cout << "b1[0]=" << b1[0] << ", b2[0]=" << b2[0] << std::endl;

    return 0;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
b1[0]=1, b2[0]=100
💡 نصيحة: النسخة العميقة تنسخ المحتوى الذي يشير إليه المؤشر، وليس قيمة المؤشر فقط. تعديل النسخة لا يؤثر على الكائن الأصلي.


س: متى أعرّف باني نسخ مخصص؟ ج: عندما تحتوي الفئة على أعضاء مؤشرات (تشير إلى ذاكرة ديناميكية).

إذا كانت الفئة تحتوي فقط على أنواع أساسية (int، double) أو أنواع STL (std::string، std::vectorلا حاجة لباني نسخ مخصص - باني النسخ الذي يُنشئه المترجم يعمل بشكل صحيح (أنواع STL تطبق النسخة العميقة بنفسها).

س: ماذا تفعل =default و =delete؟ ج: > - = default: يسمح للمترجم بتوليد التطبيق الافتراضي (نسخ عضو بعضو) > - = delete: يحظر دالة (مثلاً حظر النسخ)

يُنصح باستخدام = default بدلاً من كتابة منطق النسخ يدوياً (إذا لم يكن هناك حاجة لمنطق خاص).

س: ماذا يحدث إذا لم أكتب باني نسخ؟ ج: يُنشئ المترجم تلقائياً باني نسخ افتراضي (نسخ عضو بعضو).

لكن إذا كانت الفئة تحتوي على أعضاء مؤشرات، فالافتراضي سيُنفذ نسخة سطحية (ينسخ قيمة المؤشر فقط، وليس المحتوى الذي يشير إليه)، مما يسبب تحريراً مزدوجاً للذاكرة.


❓ أسئلة شائعة

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

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): عرّف فئة Point بعضوين: x و y.

  2. اتبع قاعدة الثلاثة - عرّف باني نسخ مخصص، وعامل تعيين نسخ مخصص، ومدمراً مخصصاً

  3. اختبر النسخ في main

  4. متوسط (الصعوبة ⭐⭐): عرّف فئة DynamicArray (مصفوفة ديناميكية) تحتوي على:

  5. int* data (يشير إلى مصفوفة على الكومة)

  6. int size (حجم المصفوفة)

اتبع قاعدة الخمسة - نفّذ جميع دوال الأعضاء الخاصة.

  1. متقدم (الصعوبة ⭐⭐⭐): وسّع فئة DynamicArray بإضافة:
  2. void push_back(int x): أضف عنصراً إلى نهاية المصفوفة (إذا لم تكن المساحة كافية، ضاعف السعة)
  3. void pop_back(): احذف العنصر الأخير

6. 🚀 الخطوات التالية*

الآن بعد أن تعلمت التحكم في النسخ ودلالات النقل، لننتقل إلى الوراثة (الدرس 31) - إنشاء علاقات "أب-ابن" بين الفئات لإعادة استخدام الكود!

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%