C++: البانيات المتقدمة
آخر تحديث: 2026-08-26
في الدرس 28، تعلمنا أساسيات البانيات.
لكن في السيناريوهات الواقعية، قد تحتاج إلى نسخ الكائنات، نقل الكائنات، استخدام قوائم التهيئة للكفاءة...
في هذا الدرس، سنتعلم الاستخدام المتقدم للبانيات.
1. الباني الافتراضي
(1) 1.1 ما هو الباني الافتراضي؟
الباني الافتراضي هو باني لا يأخذ معاملات.
#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 المشكلة: بعد كتابة باني بمعاملات، يفشل البناء الافتراضي
#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).
// الطريقة 1: كتابته صراحةً
Student() {
name = "Unknown";
age = 0;
}
// الطريقة 2: استخدام = default (C++11، موصى بها)
Student() = default;
2. قائمة التهيئة
(1) 2.1 ما هي قائمة التهيئة؟
قائمة التهيئة تُهيئ الأعضاء مباشرة بعد نقطتي الباني، بدلاً من تعيين القيم في جسم الدالة.
الطريقة التقليدية (التعيين):
Student(const std::string& n, int a) {
name = n; // هذا تعيين، وليس تهيئة
age = a;
}
طريقة قائمة التهيئة (موصى بها):
Student(const std::string& n, int a) : name(n), age(a) {
// يمكن أن يكون جسم الدالة فارغاً
}
💡 المزايا:
- أكثر كفاءة (تهيئة مباشرة، وليس بناءًا افتراضياً يتبعه تعيين)
- الحالات التي تتطلب قائمة التهيئة:
- أعضاء
const(يجب تهيئتها عند الإنشاء) - أعضاء المرجع (يجب تهيئتها)
- فئات الأعضاء بدون باني افتراضي
(2) 2.2 الحالات التي تتطلب قائمة تهيئة
الحالة 1: أعضاء const
#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: باني النسخ الافتراضي (الصعوبة ⭐)
#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;
}
الخرج:
s2 Name: Alice
s2 Age: 20
💡 نقطة مهمة: إذا لم تكتب باني نسخ، يُنشئ المترجم تلقائياً واحداً (نسخ عضو ببعضو).
(2) 3.2 باني نسخ مخصص
#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 المشكلة: النسخ الافتراضي هو "نسخة سطحية"
إذا كانت الفئة تحتوي على أعضاء مؤشرات (تشير إلى ذاكرة الكومة)، فإن باني النسخ الافتراضي ينسخ قيمة المؤشر فقط (العنوان)، وليس المحتوى الذي يشير إليه المؤشر.
#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 الحل: نسخة عميقة مخصصة
#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 — "يسرق" الموارد من كائن آخر بدلاً من نسخها.
السيناريو: إذا كان لديك كائن مؤقت (على وشك التدمير)، فإن نسخ محتواه مضيعة — من الأفضل فقط "سرقة" موارده.
#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: فئة طالب مع باني نسخ (الصعوبة ⭐⭐)
#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;
}
الخرج:
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: باني مفوَّض (الصعوبة ⭐)
#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;
}
الخرج:
Zhang San, 20 years old
Li Si, 18 years old
Unknown, 18 years old
- البانيات الافتراضية هي بانيات بدون معاملات
- قوائم التهيئة أكثر كفاءة ويمكنها التعامل مع أعضاء
constوأعضاء المرجع - بانيات النسخ تُستخدم لتهيئة كائن من كائن آخر
- إذا كانت الفئة تحتوي على أعضاء مؤشرات، يجب تعريف نسخة عميقة مخصصة
- بانيات النقل (C++11) يمكنها تحسين كفاءة نسخ الكائنات المؤقتة
📖 ملخص
- البانيات المفوَّضة: باني يستدعي بانيًا آخر
- بانيات الوراثة: الفئات المشتقة ترث بانيات الفئة الأساسية
- بناء النقل: ينقل ملكية الموارد، يتجنب النسخة العميقة
- explicit: يمنع التحويل الضمني
📝 تمارين
-
أساسي (الصعوبة ⭐): عرّف فئة
Bookبعضوين: العنوان والمؤلف. اكتب الباني باستخدام قائمة تهيئة. -
متوسط (الصعوبة ⭐⭐): عرّف باني نسخ مخصص لفئة
Bookيطبق النسخة العميقة (افترض أن العنوان مُخزَّن كـchar*، يتطلب تخصيص ذاكرة جديدة). -
متقدم (الصعوبة ⭐⭐⭐): عرّف فئة
DynamicArray(مصفوفة ديناميكية) تحتوي على: -
int* data(يشير إلى ذاكرة الكومة) -
int size(حجم المصفوفة)
نفّذ: 3. باني عادي (يخصص الذاكرة) 4. باني نسخ (نسخة عميقة) 5. مدمر (يُحرر الذاكرة)
اختبر: أنشئ DynamicArray obj1(10)، ثم استخدم obj1 لتهيئة obj2، وتحقق أن data في الكائنين يشير إلى عناوين ذاكرة مختلفة.
- الباني الافتراضي: بدون معاملات أو جميع المعاملات لها قيم افتراضية
- قوائم التهيئة أكثر كفاءة من التعيين في جسم الباني
- البانيات المفوَّضة: باني يستدعي بانيًا آخر
- explicit يمنع التحويلات الضمنية للأنواع
- المدمر: يُستدعى تلقائياً عند تدمير كائن، يُحرر الموارد
7. 🚀 الخطوات التالية
الآن بعد أن تعلمت الاستخدام المتقدم للبانيات، لننتقل إلى تحميل العوامل (الدرس 30) — جعل الأنواع المخصصة تعمل مع العوامل +، -، << وغيرها!