قاعدة الثلاثة / الخمسة / الصفر
جزء من قسم البرمجة كائنية التوجه في رحلة C++ على Coddy. الدرس 25 من 104.
عندما يدير صنفك موارد مثل الذاكرة الديناميكية، تكون قد تعلمت أنك تحتاج إلى مُدمِّر مخصّص، ومُنشئ نسخ، ومُنشئ نقل. لكن هناك مبدأً إرشاديًا يساعدك على تحديد دوال الأعضاء الخاصة التي ينبغي تنفيذها: قاعدة الثلاثة والخمسة والصفر.
تنص قاعدة الثلاثة على ما يلي: إذا عرّفت أيًّا من هذه الثلاثة، فينبغي أن تعرّفها جميعًا:
- المُدمِّر
- مُنشئ النسخ
- عامل إسناد النسخ
توسّع قاعدة الخمسة هذا المفهوم في C++ الحديثة، بإضافة عمليات النقل:
- المُدمِّر
- مُنشئ النسخ
- عامل إسناد النسخ
- مُنشئ النقل
- عامل إسناد النقل
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; } // 1. المدمّر
Buffer(const Buffer& other); // 2. باني النسخ
Buffer& operator=(const Buffer& other); // 3. إسناد النسخ
Buffer(Buffer&& other) noexcept; // 4. باني النقل
Buffer& operator=(Buffer&& other) noexcept; // 5. إسناد النقل
};يتبع معاملا الإسناد نمطًا واحدًا: التحقق من عدم الإسناد إلى الكائن نفسه، وتحرير المورد القديم، وأخذ المورد الجديد، ثم إرجاع *this:
Buffer& Buffer::operator=(const Buffer& other) { // الإسناد بالنسخ
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
Buffer& Buffer::operator=(Buffer&& other) noexcept { // الإسناد بالنقل
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}قاعدة الصفر هي أبسط نهج: إذا كان صنفك لا يدير الموارد مباشرةً، فلا تعرّف أيًا من هذه الدوال. دع المترجم ينشئها، أو استخدم المؤشرات الذكية والحاويات القياسية التي تتولى إدارة الموارد نيابةً عنك.
class Player {
std::string name; // تدير std::string ذاكرتها بنفسها
std::vector<int> scores; // تدير std::vector مواردها بنفسها
public:
Player(std::string n) : name(n) {}
// لا حاجة إلى دوال التدمير أو النسخ أو النقل!
};اتباع هذه القواعد يمنع أخطاء مثل الحذف المزدوج، وتسرب الذاكرة، والمؤشرات المعلّقة التي تحدث عندما تكون بعض العمليات معرّفة بينما تكون عمليات أخرى مفقودة.
التحدي
سهللننشئ صنفًا باسم TextBuffer يتبع قاعدة الخمسة: تنفيذ جميع دوال الأعضاء الخاصة الخمس لإدارة بيانات الأحرف المخصصة ديناميكيًا على نحو سليم. سيوضح هذا كيف تعمل عمليتا النسخ والنقل معًا لإنشاء صنف متين يدير الموارد.
ستُنشئ ملفين لتنظيم الشيفرة:
TextBuffer.h: عرّف صنفًا باسمTextBufferيخزّن النص في مصفوفة أحرف مخصصة ديناميكيًا. يحتاج صنفك إلى:- أعضاء خاصين: مؤشر
char*باسمdataلمحتوى النص، وsize_t lengthلطول السلسلة (من دون احتساب المحرف الصفري الختامي) - باني ذي معاملات يأخذ سلسلة C (
const char*)، ويخصص الذاكرة، وينسخ المحتوى، ويطبع"TextBuffer created: <text>" - مُدمّر يحرر الذاكرة (إذا لم تكن فارغة) ويطبع
"TextBuffer destroyed" - باني نسخ ينفذ نسخة عميقة ويطبع
"TextBuffer copied" - عامل إسناد النسخ يتعامل مع الإسناد الذاتي، وينظف البيانات الموجودة، وينفذ نسخة عميقة، ويطبع
"TextBuffer copy-assigned". أعد*this - باني نقل (معلّم بـ
noexcept) ينقل الملكية ويطبع"TextBuffer moved". اترك المصدر في حالة فارغة صالحة - عامل إسناد النقل (معلّم بـ
noexcept) يتعامل مع الإسناد الذاتي، وينظف البيانات الموجودة، وينقل الملكية، ويطبع"TextBuffer move-assigned". أعد*this - دالة
getText()تعيد النص المخزّن (أعد سلسلة فارغة""إذا كانتdataفارغة) - دالة
getLength()تعيد الطول
- أعضاء خاصين: مؤشر
main.cpp: وضّح عمل دوال الأعضاء الخاصة الخمس جميعها. اقرأ سلسلة نصية من الإدخال، ثم:- أنشئ كائنًا من
TextBufferباسمoriginal، وضع فيه نص الإدخال - أنشئ
copiedباستخدام باني النسخ منoriginal - أنشئ
anotherبالنص"Temporary" - استخدم إسناد النسخ:
another = original - أنشئ
movedباستخدام باني النقل منoriginalعبرstd::move() - أنشئ
targetبالنص"Target" - استخدم إسناد النقل:
target = std::move(copied) - اطبع
"--- Final State ---" - اطبع
"original: <text> (length: <len>)"لكل مخزن: original وcopied وmoved وanother وtarget
- أنشئ كائنًا من
بعد عمليات النقل، ينبغي أن يعرض كائنا المصدر (original وcopied) نصًا فارغًا بطول 0، بينما تحتفظ الكائنات الوجهة بالبيانات المنقولة. يوضح هذا قاعدة الخمسة عمليًا: تعمل الدوال الخمس معًا لضمان إدارة آمنة للموارد.
ضمّن <cstring> لاستخدام دوال السلاسل مثل strlen وstrcpy، و<utility> لاستخدام std::move().
جرّب بنفسك
#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"
using namespace std;
int main() {
string input;
getline(cin, input);
// TODO: أنشئ TextBuffer باسم 'original' يحتوي على النص المُدخل
// TODO: أنشئ 'copied' باستخدام مُنشئ النسخ من 'original'
// TODO: أنشئ 'another' بالنص "Temporary"
// TODO: استخدم إسناد النسخ: another = original
// TODO: أنشئ 'moved' باستخدام مُنشئ النقل من 'original' عبر std::move()
// TODO: أنشئ 'target' بالنص "Target"
// TODO: استخدم إسناد النقل: target = std::move(copied)
// TODO: اطبع "--- Final State ---"
// TODO: اطبع حالة كل مخزن بهذا التنسيق:
// "original: <text> (length: <len>)"
// اطبع لكلٍّ من: original, copied, moved, another, target
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البرمجة كائنية التوجه
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مراجعة - الحاويات العامةتدرّب بنفسك: مترجم C++ عبر الإنترنت