قاعدة الثلاثة / الخمسة / الصفر
جزء من قسم البرمجة كائنية التوجه في رحلة C++ على Coddy. الدرس 25 من 104.
عندما يدير class لديك موارد مثل الذاكرة الديناميكية، تكون قد تعلمت أنك تحتاج إلى destructor مخصص، وcopy constructor، وmove constructor. لكن هناك مبدأً إرشاديًا يساعدك على تحديد أي functions خاصة بالأعضاء ينبغي تنفيذها: قاعدة الثلاثة والخمسة والصفر.
تنص Rule of Three على ما يلي: إذا قمت بتعريف أيٍّ من هذه الثلاثة، فينبغي أن تعرّف الثلاثة جميعًا:
- Destructor
- Copy constructor
- Copy assignment operator
توسّع قاعدة العناصر الخمسة هذا في C++ الحديثة، بإضافة عمليات النقل:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
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. إسناد النقل
};تُعد Rule of Zero النهج الأبسط: إذا لم يكن صفّك يدير الموارد مباشرةً، فلا تعرّف أيًا من هذه الدوال. دع المصرّف ينشئها، أو استخدم المؤشرات الذكية والحاويات القياسية التي تتولى إدارة الموارد نيابةً عنك.
class Player {
std::string name; // std::string يدير ذاكرته الخاصة
std::vector<int> scores; // std::vector يتعامل مع موارده
public:
Player(std::string n) : name(n) {}
// لا حاجة لدوال التدمير أو النسخ أو النقل!
};يمنع اتباع هذه القواعد حدوث أخطاء مثل الحذف المزدوج، وتسربات الذاكرة، والمؤشرات المتدلية التي تحدث عند تعريف بعض العمليات مع فقدان عمليات أخرى.
التحدي
سهللنبنِ فئة TextBuffer تتبع Rule of Five: من خلال تنفيذ جميع special member functions الخمس لإدارة بيانات الأحرف المخصّصة ديناميكيًا بطريقة صحيحة. سيوضح هذا كيف تعمل عمليات copy وmove معًا لإنشاء فئة قوية تدير الموارد.
ستنشئ ملفين لتنظيم code الخاص بك:
TextBuffer.h: Define فئةTextBufferتخزّن النص في مصفوفة أحرف مخصّصة ديناميكيًا. تحتاج فئتك إلى:- أعضاء Private: مؤشر
char*calleddataلمحتوى النص، وsize_t lengthلطول السلسلة النصية (من دون null terminator) - Parameterized constructor يأخذ C-string (
const char*)، ويخصّص الذاكرة، وينسخ content، ويطبع"TextBuffer created: <text>" - destructor يحرّر الذاكرة (إذا لم تكن null) ويطبع
"TextBuffer destroyed" - copy constructor ينفّذ deep copy ويطبع
"TextBuffer copied" - copy assignment operator يعالج self-assignment، وينظّف data الموجودة، وينفّذ deep copy، ويطبع
"TextBuffer copy-assigned". أعد*this - move constructor (الموسوم بـ
noexcept) ينقل الملكية ويطبع"TextBuffer moved". اترك المصدر في حالة فارغة صالحة - move assignment operator (الموسوم بـ
noexcept) يعالج self-assignment، وينظّف data الموجودة، وينقل الملكية، ويطبع"TextBuffer move-assigned". أعد*this - دالة
getText()تُرجع النص المخزّن (أعد سلسلة نصية فارغة""إذا كانت data تساوي null) - دالة
getLength()تُرجع length
- أعضاء Private: مؤشر
main.cpp: اعرض جميع special member functions الخمس أثناء العمل. اقرأ سلسلة نصية من input، ثم:- Create كائن
TextBuffercalledoriginalباستخدام نص input - Create
copiedباستخدام copy constructor منoriginal - Create
anotherباستخدام النص"Temporary" - استخدم copy assignment:
another = original - Create
movedبواسطة move-constructing منoriginalباستخدامstd::move() - Create
targetباستخدام النص"Target" - استخدم move assignment:
target = std::move(copied) - اطبع
"--- Final State ---" - اطبع
"original: <text> (length: <len>)"لكل buffer: original وcopied وmoved وanother وtarget
- Create كائن
بعد عمليات move، يجب أن تعرض كائنات المصدر (original وcopied) نصًا فارغًا مع length يساوي 0، بينما تحتفظ كائنات الوجهة بالبيانات المنقولة. يوضّح هذا Rule of Five أثناء العمل: تعمل functions الخمس معًا لضمان إدارة آمنة للموارد.
ضمّن <cstring> من أجل string functions مثل 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: اطبع حالة كل buffer بهذا التنسيق:
// "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++ عبر الإنترنت