C++: البانيات المتقدمة

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

في الدرس 28، تعلمنا أساسيات البانيات.

لكن في السيناريوهات الواقعية، قد تحتاج إلى نسخ الكائنات، نقل الكائنات، استخدام قوائم التهيئة للكفاءة...

في هذا الدرس، سنتعلم الاستخدام المتقدم للبانيات.


1. الباني الافتراضي

(1) 1.1 ما هو الباني الافتراضي؟

الباني الافتراضي هو باني لا يأخذ معاملات.

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // باني افتراضي (بدون معاملات)
 Student() {
 name = "Unknown";
 age = 0;
 std::cout << "Default constructor called" << std::endl;
 }
};

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

💡 نقطة مهمة: إذا لم تكتب أي باني، يُنشئ المترجم تلقائياً بانيًا افتراضياً (لا يفعل شيئاً). لكن إذا كتبت أي باني، فلن يعود المترجم يُنشئ بانيًا افتراضيًا تلقائياً.


(2) 1.2 المشكلة: بعد كتابة باني بمعاملات، يفشل البناء الافتراضي

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // كتبت بانيًا بمعاملات فقط
 Student(const std::string& n, int a) {
 name = n;
 age = a;
 }
};

int main() {
 Student s1("Alice", 20); // ✅ صحيح
 Student s2; // ❌ خطأ: لا يوجد باني افتراضي
 return 0;
}

الحل: اكتب بانيًا افتراضيًا صراحةً، أو استخدم = default (C++11).

TEXT 📖 للعرض فقط
// الطريقة 1: كتابته صراحةً
Student() {
 name = "Unknown";
 age = 0;
}

// الطريقة 2: استخدام = default (C++11، موصى بها)
Student() = default;


2. قائمة التهيئة

(1) 2.1 ما هي قائمة التهيئة؟

قائمة التهيئة تُهيئ الأعضاء مباشرة بعد نقطتي الباني، بدلاً من تعيين القيم في جسم الدالة.

الطريقة التقليدية (التعيين):

CPP
Student(const std::string& n, int a) {
 name = n; // هذا تعيين، وليس تهيئة
 age = a;
}

طريقة قائمة التهيئة (موصى بها):

TEXT 📖 للعرض فقط
Student(const std::string& n, int a) : name(n), age(a) {
 // يمكن أن يكون جسم الدالة فارغاً
}

💡 المزايا:

  1. أكثر كفاءة (تهيئة مباشرة، وليس بناءًا افتراضياً يتبعه تعيين)
  2. الحالات التي تتطلب قائمة التهيئة:

(2) 2.2 الحالات التي تتطلب قائمة تهيئة

الحالة 1: أعضاء const

CPP
#include <iostream>
#include <string>

class Student {
private:
 const std::string id; // عضو const
 std::string name;
 
public:
 // ❌ خطأ: لا يمكن تعيين أعضاء const في جسم الدالة
 // Student(const std::string& i, const std::string& n) {
 // id = i; // ❌ عضو const لا يمكن تعيينه
 // name = n;
 // }
 
 // ✅ صحيح: استخدم قائمة التهيئة
 Student(const std::string& i, const std::string& n) : id(i), name(n) {
 }
};

int main() {
 Student s1("20240001", "Alice");
 return 0;
}


3. باني النسخ

(1) 3.1 ما هو باني النسخ؟

يُستخدم باني النسخ لتهيئة كائن من كائن آخر.

متى يُستدعى:

  1. تهيئة كائن من كائن آخر
  2. تمرير كائن بالقيمة إلى دالة (يُنسخ)
  3. إرجاع كائن من دالة (قد يُنسخ)

▶ مثال 1: باني النسخ الافتراضي (الصعوبة ⭐)

CPP
#include <iostream>
#include <string>

class Student {
public:
 std::string name;
 int age;
 
 // يُنشئ المترجم تلقائياً باني نسخ (نسخ عضو بعضو)
};

int main() {
 Student s1;
 s1.name = "Alice";
 s1.age = 20;
 
 Student s2 = s1; // ✅ يستدعي باني النسخ
 std::cout << "s2 Name: " << s2.name << std::endl; // Alice
 std::cout << "s2 Age: " << s2.age << std::endl; // 20
 
 return 0;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
s2 Name: Alice
s2 Age: 20

💡 نقطة مهمة: إذا لم تكتب باني نسخ، يُنشئ المترجم تلقائياً واحداً (نسخ عضو ببعضو).


(2) 3.2 باني نسخ مخصص

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // باني عادي
 Student(const std::string& n, int a) : name(n), age(a) {
 std::cout << "Regular constructor called:" << name << std::endl;
 }
 
 // باني نسخ
 Student(const Student& other) : name(other.name), age(other.age) {
 std::cout << "Copy constructor called: " << name << std::endl;
 }
};

int main() {
 Student s1("Alice", 20);
 Student s2 = s1; // يستدعي باني النسخ
 
 return 0;
}


4. النسخة السطحية مقابل النسخة العميقة

(1) 4.1 المشكلة: النسخ الافتراضي هو "نسخة سطحية"

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

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

class MyClass {
public:
 char* data;
 
 MyClass() {
 data = new char[5];
 std::strcpy(data, "Hello");
 }
 
 // ❌ لا يوجد باني نسخ مخصص (سيستخدم النسخة السطحية الافتراضية)
 
 ~MyClass() {
 delete data;
 }
};

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

💡 هذه هي مشكلة "النسخة السطحية" — مؤشرات كائنين تشير إلى نفس الذاكرة، مما يسبب تحريراً مزدوجاً عند التدمير.


(2) 4.2 الحل: نسخة عميقة مخصصة

CPP
#include <iostream>
#include <cstring>

class MyClass {
public:
 char* data;
 
 MyClass() {
 data = new char[5];
 std::strcpy(data, "Hello");
 }
 
 // باني نسخ مخصص (نسخة عميقة)
 MyClass(const MyClass& other) {
 data = new char[std::strlen(other.data) + 1];
 std::strcpy(data, other.data);
 }
 
 ~MyClass() {
 delete data;
 }
};

int main() {
 MyClass obj1;
 MyClass obj2 = obj1; // نسخة عميقة: obj2.data يشير إلى ذاكرة مُخصصة حديثاً
 
 std::cout << "obj1.data = " << obj1.data << std::endl;
 std::cout << "obj2.data = " << obj2.data << std::endl;
 
 return 0;
}

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



5. باني النقل (C++11، اختياري)

(1) 5.1 ما هو باني النقل؟

باني النقل هو ميزة أُدخلت في C++11 — "يسرق" الموارد من كائن آخر بدلاً من نسخها.

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

CPP
#include <iostream>
#include <string>

class MyClass {
private:
 std::string* data;
 
public:
 // باني عادي
 MyClass(const std::string& s) {
 data = new std::string(s);
 std::cout << "Regular constructor" << std::endl;
 }
 
 // باني نقل (C++11)
 MyClass(MyClass&& other) noexcept : data(other.data) {
 other.data = nullptr; // اضبط مؤشر الكائن الآخر على قيمة فارغة (منع التحرير عند التدمير)
 std::cout << "Move constructionfunction" << std::endl;
 }
 
 ~MyClass() {
 delete data;
 }
};

int main() {
 MyClass obj1("Hello");
 MyClass obj2 = std::move(obj1); // يستدعي باني النقل
 
 return 0;
}

💡 نصيحة: باني النقل موضوع متقدم — يمكن للمبتدئين التعرف على المفهوم أولاً والتعمق لاحقاً.



6. تطبيق: فئة طالب كاملة

▶ مثال 2: فئة طالب مع باني نسخ (الصعوبة ⭐⭐)

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // باني عادي
 Student(const std::string& n, int a) : name(n), age(a) {
 std::cout << "Regular Constructor: " << name << std::endl;
 }
 
 // باني نسخ
 Student(const Student& other) : name(other.name), age(other.age) {
 std::cout << "Regular Constructor: " << name << std::endl;
 }
 
 // مدمر
 ~Student() {
 std::cout << "Destructor: " << name << std::endl;
 }
 
 void introduce() {
 std::cout << "My name is " << name << ",this year " << age << " years old。" << std::endl;
 }
};

int main() {
 Student s1("Alice", 20);
 Student s2 = s1; // يستدعي باني النسخ
 
 s1.introduce();
 s2.introduce();
 
 return 0;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
Regular Constructor: Alice
Regular Constructor: Alice
Destructor: Alice
Destructor: Alice
My name is Alice,this year 20 years old。
My name is Alice,this year 20 years old。

❓ أسئلة شائعة

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

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

س: ما مقدار فرق الكفاءة بين قوائم التهيئة والتعيين في جسم الدالة؟ ج: بالنسبة للأنواع الأساسية، ليس كثيراً. لكن بالنسبة لأنواع STL (std::string، std::vector)، كثير —

  • التعيين في جسم الدالة: يُنشئ أولاً كائناً فارغاً بشكل افتراضي، ثم يُعين (قد يخصص ذاكرة وينسخ المحتوى)
  • قائمة التهيئة: تهيئة مباشرة (تخصيص ذاكرة واحد فقط)

التوصية: استخدم دائماً قوائم التهيئة.

س: ما فائدة بانيات النقل؟ ج: تحسين كفاءة نسخ الكائنات المؤقتة.

مثلاً، عندما تُرجع دالة كائناً كبيراً، يمكن لباني النقل أن يتجنب النسخة العميقة (عبر "سرقة" ذاكرة الكائن المؤقت مباشرة).


▶ مثال 3: باني مفوَّض (الصعوبة ⭐)

CPP
#include <iostream>
#include <string>

class Student {
    std::string name;
    int age;
public:
    Student(std::string n, int a) : name(n), age(a) {}
    Student(std::string n) : Student(n, 18) {} // باني مفوَّض
    Student() : Student("Unknown") {}

    void print() { std::cout << name << "," << age << "years old" << std::endl; }
};

int main() {
    Student s1("Zhang San", 20);
    Student s2("Li Si");
    Student s3;

    s1.print();
    s2.print();
    s3.print();

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

الخرج:

TEXT 📖 للعرض فقط
Zhang San, 20 years old
Li Si, 18 years old
Unknown, 18 years old
💡 نصيحة: البانيات المفوَّضة تتيح لباني استدعاء باني آخر، مما يتجنب تكرار الكود.



📖 ملخص

📝 تمارين

  1. أساسي (الصعوبة ⭐): عرّف فئة Book بعضوين: العنوان والمؤلف. اكتب الباني باستخدام قائمة تهيئة.

  2. متوسط (الصعوبة ⭐⭐): عرّف باني نسخ مخصص لفئة Book يطبق النسخة العميقة (افترض أن العنوان مُخزَّن كـ char*، يتطلب تخصيص ذاكرة جديدة).

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

  4. int* data (يشير إلى ذاكرة الكومة)

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

نفّذ: 3. باني عادي (يخصص الذاكرة) 4. باني نسخ (نسخة عميقة) 5. مدمر (يُحرر الذاكرة)

اختبر: أنشئ DynamicArray obj1(10)، ثم استخدم obj1 لتهيئة obj2، وتحقق أن data في الكائنين يشير إلى عناوين ذاكرة مختلفة.


7. 🚀 الخطوات التالية

الآن بعد أن تعلمت الاستخدام المتقدم للبانيات، لننتقل إلى تحميل العوامل (الدرس 30) — جعل الأنواع المخصصة تعمل مع العوامل +، -، << وغيرها!

Web-Tutorial.com

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

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

100%