منشئ النسخ
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 20 من 104.
يقوم منشئ النسخ (copy constructor) بإنشاء كائن جديد كنسخة من كائن موجود. يتم استدعاؤه عند تهيئة كائن من كائن آخر من نفس النوع، أو تمرير كائن بالقيمة، أو إرجاع كائن بالقيمة.
يأخذ منشئ النسخ (copy constructor) مرجعاً ثابتاً (const reference) لكائن من نفس الفئة:
class Player {
std::string name;
int health;
public:
Player(std::string n, int h) : name(n), health(h) {}
// منشئ النسخ
Player(const Player& other) {
name = other.name;
health = other.health;
}
};
Player original("Hero", 100);
Player copy = original; // تم استدعاء منشئ النسخ
Player another(original); // يستدعي أيضاً منشئ النسخإذا لم تقم بتعريف منشئ نسخ (copy constructor)، فإن المترجم (compiler) يقوم بإنشاء واحد يقوم بعملية نسخ سطحي (shallow copy) - حيث يقوم بنسخ قيمة كل عضو بشكل مباشر. يعمل هذا بشكل جيد مع الأنواع البسيطة، ولكنه يسبب مشاكل عندما تدير الفئة (class) الخاصة بك ذاكرة ديناميكية.
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new int[size];
}
// مُنشئ النسخ العميق
Buffer(const Buffer& other) : size(other.size) {
data = new int[size]; // تخصيص ذاكرة جديدة
for (size_t i = 0; i < size; i++)
data[i] = other.data[i]; // نسخ المحتويات
}
~Buffer() { delete[] data; }
};بدون منشئ نسخ مخصص (custom copy constructor)، سيشير كلا الكائنين إلى نفس الذاكرة. وعندما يتم تدمير أحدهما، سيبقى لدى الآخر مؤشر معلق (dangling pointer). أما النسخ العميق (deep copy) فيقوم بتخصيص ذاكرة جديدة ونسخ البيانات الفعلية، مما يضمن امتلاك كل كائن لموارده الخاصة.
التحدي
سهللنقم ببناء نظام لسجل الرسائل يوضح أهمية النسخ العميق (deep copying) عندما تدير فئتك (class) ذاكرة ديناميكية. ستقوم بإنشاء فئة MessageLog تخزن الرسائل في مصفوفة مخصصة ديناميكيًا، وتنفذ منشئ نسخ (copy constructor) مناسبًا لضمان أن كل نسخة تمتلك ذاكرتها المستقلة الخاصة بها.
ستقوم بإنشاء ملفين لتنظيم الكود الخاص بك:
MessageLog.h: قم بتعريف فئةMessageLogالتي تدير مجموعة من الرسائل باستخدام الذاكرة الديناميكية. يجب أن تحتوي فئتك على:- أعضاء خاصين (Private members): مؤشر إلى مصفوفة سلاسل نصية (string array) لتخزين الرسائل، و
capacityللحد الأقصى لعدد الرسائل، وcountللعدد الحالي للرسائل. - منشئ بمعاملات (parameterized constructor) يأخذ السعة (capacity)، ويخصص المصفوفة في الذاكرة (heap)، ويهيئ count إلى 0.
- منشئ نسخ (copy constructor) يقوم بعملية نسخ عميق (deep copy) — أي تخصيص ذاكرة جديدة ونسخ جميع الرسائل من الكائن المصدر.
- مدمر (destructor) يحرر الذاكرة المخصصة ويطبع
"MessageLog destroyed". - طريقة
addMessage(string msg)تضيف رسالة إذا كانت هناك مساحة. - طريقة
getCount()تعيد عدد الرسائل الحالي. - طريقة
getMessage(int index)تعيد الرسالة الموجودة في الفهرس المعطى.
- أعضاء خاصين (Private members): مؤشر إلى مصفوفة سلاسل نصية (string array) لتخزين الرسائل، و
main.cpp: أثبت أن منشئ النسخ الخاص بك ينشئ نسخة مستقلة. اقرأ قيمة السعة ورسالتين من المدخلات، ثم:- أنشئ
MessageLogيسمىoriginalبالسعة المدخلة. - أضف كلتا الرسالتين إلى
original. - أنشئ نسخة تسمى
backupباستخدام منشئ النسخ. - أضف رسالة ثالثة
"New message"إلىoriginalفقط. - اطبع
"Original count: <count>". - اطبع
"Backup count: <count>". - اطبع جميع الرسائل من
backup، كل واحدة في سطر.
- أنشئ
تنسيق المدخلات سيكون:
- السطر الأول: السعة (عدد صحيح).
- السطر الثاني: الرسالة الأولى (سلسلة نصية).
- السطر الثالث: الرسالة الثانية (سلسلة نصية).
إذا تم تنفيذ النسخ العميق بشكل صحيح، يجب أن يحتوي backup على رسالتين بينما يحتوي original على 3 رسائل — مما يثبت أنهما يمتلكان ذاكرة مستقلة. بدون منشئ نسخ مناسب، سيشترك كلاهما في نفس المصفوفة وستؤثر التغييرات في أحدهما على الآخر!
قم بتضمين ملف الرأس الخاص بك في main.cpp باستخدام #include "MessageLog.h".
ورقة مرجعية
يقوم منشئ النسخ (copy constructor) بإنشاء كائن جديد كنسخة من كائن موجود. وهو يأخذ مرجعاً ثابتاً (const reference) لكائن من نفس الفئة:
class Player {
std::string name;
int health;
public:
Player(std::string n, int h) : name(n), health(h) {}
// منشئ النسخ
Player(const Player& other) {
name = other.name;
health = other.health;
}
};
Player original("Hero", 100);
Player copy = original; // تم استدعاء منشئ النسخ
Player another(original); // يستدعي أيضاً منشئ النسخإذا لم تقم بتعريف منشئ نسخ، فسيقوم المترجم (compiler) بإنشاء واحد يقوم بعملية نسخ سطحي (shallow copy) - أي نسخ قيمة كل عضو مباشرة. يتسبب هذا في حدوث مشكلات عندما تدير فئتك ذاكرة ديناميكية.
يقوم منشئ النسخ العميق (deep copy constructor) بتخصيص ذاكرة جديدة ونسخ البيانات الفعلية، مما يضمن أن كل كائن يمتلك موارده الخاصة:
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new int[size];
}
// منشئ النسخ العميق
Buffer(const Buffer& other) : size(other.size) {
data = new int[size]; // تخصيص ذاكرة جديدة
for (size_t i = 0; i < size; i++)
data[i] = other.data[i]; // نسخ المحتويات
}
~Buffer() { delete[] data; }
};بدون منشئ نسخ مخصص، سيشير كلا الكائنين إلى نفس الذاكرة، مما يتسبب في ظهور مؤشرات معلقة (dangling pointers) عند تدمير أحدهما.
جرّب بنفسك
#include <iostream>
#include <string>
#include "MessageLog.h"
using namespace std;
int main() {
// قراءة المدخلات
int capacity;
cin >> capacity;
cin.ignore();
string msg1, msg2;
getline(cin, msg1);
getline(cin, msg2);
// TODO: قم بإنشاء MessageLog يسمى 'original' بسعة المدخلات
// TODO: أضف كلتا الرسالتين إلى original
// TODO: قم بإنشاء نسخة تسمى 'backup' باستخدام copy constructor
// TODO: أضف "New message" إلى original فقط
// TODO: اطبع "Original count: <count>"
// TODO: اطبع "Backup count: <count>"
// TODO: اطبع جميع الرسائل من backup، رسالة واحدة في كل سطر
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة