الوراثة الافتراضية
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 54 من 104.
تحدث مشكلة الماس (diamond problem) عندما ترث فئة من فئتين تشتركان في فئة أساسية واحدة. وبدون معالجة خاصة، تنتهي الفئة المشتقة بوجود نسختين من الفئة الأساسية المشتركة، مما يسبب غموضاً وهدراً في الذاكرة.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // خطأ: غامض - أي 'age'؟يحتوي الصنف Bat على كائنين فرعيين منفصلين من النوع Animal: أحدهما عبر Mammal والآخر عبر Bird. وهذا ينشئ مخطط الوراثة الماسي الذي يعطي المشكلة اسمها.
الوراثة الافتراضية تحل هذه المشكلة من خلال ضمان وجود نسخة واحدة فقط من القاعدة المشتركة. أضف الكلمة المفتاحية virtual عند الوراثة من القاعدة المشتركة:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // يعمل! يوجد 'age' واحد فقطلاحظ أن Bat يجب أن يقوم بتهيئة Animal بشكل مباشر في المنشئ الخاص به. مع الوراثة الافتراضية (virtual inheritance)، تكون الفئة الأكثر اشتقاقاً هي المسؤولة عن بناء القاعدة الافتراضية، بغض النظر عن الفئات الوسيطة.
التحدي
سهللنقم ببناء نظام لإدارة الموظفين يوضح كيف يحل التوريث الافتراضي (virtual inheritance) مشكلة الألماسة (diamond problem). ستقوم بإنشاء تسلسل هرمي حيث يرث TeamLead من كل من Developer و Manager، واللذان يشتركان في فئة أساسية عامة هي Employee.
ستقوم بتنظيم الكود الخاص بك عبر أربعة ملفات:
Employee.h: قم بتعريف الفئة الأساسية العامةEmployeeمع:- عضوين محميين (protected) هما
std::string nameوint id - منشئ (constructor) يأخذ القيمتين ويطبع:
Employee [<name>] hired with ID <id> - دالة عامة
getInfo()تطبع:Employee: <name> (ID: <id>) - مدمر افتراضي (virtual destructor) يطبع:
Employee [<name>] record closed
- عضوين محميين (protected) هما
Developer.h: قم بتعريف فئةDeveloperتستخدم التوريث العام الافتراضي (virtual public inheritance) منEmployee:- عضو محمي
std::string language - منشئ يأخذ الاسم، والمعرف، واللغة — يمرر الاسم والمعرف إلى
Employee، ويخزن اللغة، ويطبع:Developer [<name>] specializes in <language> - دالة عامة
code()تطبع:<name> is coding in <language> - مدمر يطبع:
Developer [<name>] signed off
- عضو محمي
Manager.h: قم بتعريف فئةManagerتستخدم التوريث العام الافتراضي (virtual public inheritance) منEmployee:- عضو محمي
int teamSize - منشئ يأخذ الاسم، والمعرف، وحجم الفريق — يمرر الاسم والمعرف إلى
Employee، ويخزن حجم الفريق، ويطبع:Manager [<name>] leads a team of <teamSize> - دالة عامة
manage()تطبع:<name> is managing <teamSize> people - مدمر يطبع:
Manager [<name>] stepped down
- عضو محمي
main.cpp: اقرأ أربعة مدخلات (كل منها في سطر منفصل):- الاسم (string)
- معرف الموظف (integer)
- لغة البرمجة (string)
- حجم الفريق (integer)
قم بتعريف فئة
TeamLeadترث علنًا من كل منDeveloperوManager:- منشئ يأخذ جميع المعاملات الأربعة ويجب أن يقوم بتهيئة
Employee(القاعدة الافتراضية) مباشرة، ثمDeveloperوManager - يجب أن يطبع المنشئ:
TeamLead [<name>] ready to lead and code! - دالة
showRole()تستدعيgetInfo()، وcode()، وmanage()بهذا الترتيب - مدمر يطبع:
TeamLead [<name>] promoted out
قم بإنشاء كائن
TeamLeadداخل نطاق محدد (block scope)، واستدعِshowRole()، ثم اتركه يخرج عن النطاق. بعد انتهاء النطاق، اطبع:Organization restructured!
على سبيل المثال، مع المدخلات Alice، 101، C++، و 5:
Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!لاحظ كيف يوجد استدعاء واحد فقط لمنشئ Employee واستدعاء واحد فقط لمدمر Employee — يضمن التوريث الافتراضي وجود نسخة واحدة فقط من القاعدة المشتركة. يجب على TeamLead تهيئة Employee مباشرة لأنه مع التوريث الافتراضي، تكون الفئة الأكثر اشتقاقًا (most derived class) هي المسؤولة عن بناء القاعدة الافتراضية.
ورقة مرجعية
تحدث مشكلة الألماسة (diamond problem) عندما ترث فئة (class) من فئتين تشتركان في فئة أساسية واحدة، مما يؤدي إلى وجود نسختين من الفئة الأساسية ويسبب غموضاً.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Error: ambiguous - which 'age'?تحل الوراثة الافتراضية (Virtual inheritance) مشكلة الألماسة من خلال ضمان وجود نسخة واحدة فقط من الفئة الأساسية المشتركة. استخدم الكلمة المفتاحية virtual عند الوراثة:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // Works! Only one 'age' existsمع الوراثة الافتراضية، يجب على الفئة الأكثر اشتقاقاً (most derived class) تهيئة الفئة الأساسية الافتراضية مباشرة في منشئها (constructor)، بغض النظر عن الفئات الوسيطة.
جرّب بنفسك
#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"
using namespace std;
// TODO: قم بتعريف فئة TeamLead التي ترث علنًا من كل من Developer و Manager
// تذكر: مع الوراثة الافتراضية (virtual inheritance)، يجب على TeamLead تهيئة Employee (القاعدة الافتراضية) بشكل مباشر
class TeamLead : public Developer, public Manager {
public:
// TODO: قم بتنفيذ المنشئ (constructor) الذي يأخذ name و id و language و teamSize
// يجب تهيئة: Employee أولاً (القاعدة الافتراضية)، ثم Developer، ثم Manager
// يجب أن يطبع: TeamLead [<name>] ready to lead and code!
TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
: Employee(name, id),
Developer(name, id, language),
Manager(name, id, teamSize) {
// TODO: اطبع رسالة المنشئ
}
// TODO: قم بتنفيذ دالة showRole()
// يجب أن تستدعي getInfo() و code() و manage() بهذا الترتيب
void showRole() {
// TODO: استدعِ الدوال الثلاث
}
// TODO: قم بتنفيذ المدمر (destructor)
// يجب أن يطبع: TeamLead [<name>] promoted out
~TeamLead() {
// TODO: اطبع رسالة المدمر
}
};
int main() {
// قراءة المدخلات
string name;
int id;
string language;
int teamSize;
getline(cin, name);
cin >> id;
cin.ignore();
getline(cin, language);
cin >> teamSize;
// TODO: قم بإنشاء كائن TeamLead داخل نطاق كتلة (block scope)
// استدعِ showRole()، ثم اتركه يخرج عن النطاق
{
// TODO: قم بإنشاء TeamLead واستدعِ showRole()
}
// اطبع الرسالة النهائية بعد الكتلة البرمجية
cout << "Organization restructured!" << endl;
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مراجعة - الحاويات العامة