الوراثة والواجهات في C++: الدوال الافتراضية الخالصة
في درس الكلاسات ورثت Cat من Animal واستخدمت virtual و override. لكن تصميم الألعاب والأنظمة الحقيقية يحتاج أكثر: كلاس أب لا يُنشأ منه كائن أصلاً، وقدرات مشتركة بين كلاسات لا قرابة بينها. في هذا الدرس ستتعلم الكلاسات المجردة، وكيف تُبنى الواجهات في C++ مع أنها لا تملك كلمة interface.
الكلاس المجرد والدالة الافتراضية الخالصة
Section titled “الكلاس المجرد والدالة الافتراضية الخالصة”ما مساحة «شكل»؟ لا إجابة حتى نعرف هل هو دائرة أم مستطيل. الكلاس Shape فكرة عامة، ولا معنى لإنشاء كائن منه:
class Shape {protected: string name;
public: Shape(string n) : name(n) {} virtual ~Shape() = default;
virtual double area() const = 0;
void print() const { cout << name << ": " << area() << endl; }};
class Rectangle : public Shape { double width, height;
public: Rectangle(double w, double h) : Shape("مستطيل"), width(w), height(h) {}
double area() const override { return width * height; }};
class Circle : public Shape { double radius;
public: Circle(double r) : Shape("دائرة"), radius(r) {}
double area() const override { return 3.14 * radius * radius; }};vector<unique_ptr<Shape>> shapes;shapes.push_back(make_unique<Rectangle>(4, 5));shapes.push_back(make_unique<Circle>(10));for (const auto& shape : shapes) { shape->print();}مستطيل: 20دائرة: 314virtual double area() const = 0;: الكتابة= 0تجعلها دالة افتراضية خالصة (Pure Virtual): دالة بلا جسم، وكل كلاس وارث ملزم بكتابتها.- أي كلاس فيه دالة خالصة واحدة يصبح كلاساً مجرداً: لا يمكن إنشاء كائن منه، والسطر
Shape s("شكل");خطأ ترجمة. - الكلاس المجرد يحمل مع ذلك حقولاً ودوالاً عادية مثل
print. printمكتوبة مرة واحدة في الأب، وتستدعي نسخةareaالخاصة بكل شكل. هذا هو تعدد الأشكال (Polymorphism).
الكلاس المجرد مثل قالب كعك بلا حشوة: الشكل العام جاهز، لكن لا تُقدَّم الكعكة حتى يختار كل طاهٍ حشوته.
الواجهة: كلاس كله دوال خالصة
Section titled “الواجهة: كلاس كله دوال خالصة”لغات مثل Java و C# فيها كلمة interface. في C++ نبني الواجهة بكلاس كل دواله خالصة وبلا أي حقول:
class Drawable {public: virtual ~Drawable() = default; virtual void draw() const = 0;};
class Clickable {public: virtual ~Clickable() = default; virtual void click() = 0;};
class Button : public Drawable, public Clickable { string label; int clicks = 0;
public: Button(string l) : label(l) {}
void draw() const override { cout << "[ " << label << " ]" << endl; }
void click() override { clicks++; cout << "ضغطة رقم " << clicks << endl; }};Button button("حفظ");button.draw();button.click();
Clickable& target = button;target.click();[ حفظ ]ضغطة رقم 1ضغطة رقم 2class Button : public Drawable, public Clickable: C++ تسمح بالوراثة المتعددة، فالكلاس ينفّذ أي عدد من الواجهات.- المرجع
Clickable&لا يعرف عن الزر إلا أنه «شيء يُضغط»، ومع ذلك يستدعي نسخة الزر منclick. - كل واجهة لها مُدمِّر
virtualكما تعلمت في درس الكلاسات.
الواجهة مثل مقبس الكهرباء: أي جهاز له القابس المناسب يعمل، ولا يهم المقبس هل هو مصباح أم ثلاجة.
استدعاء دالة الأب
Section titled “استدعاء دالة الأب”الكلاس الوارث يستطيع أن يبني على دالة أبيه بدل استبدالها كلياً، بكتابة اسم الأب ثم :::
class Employee {protected: string name; double salary;
public: Employee(string n, double s) : name(n), salary(s) {} virtual ~Employee() = default;
virtual double bonus() const { return salary * 0.10; }};
class Manager : public Employee {public: Manager(string n, double s) : Employee(n, s) {}
double bonus() const override { return Employee::bonus() + 500; }};Employee e("هدى", 4000);Manager m("كريم", 6000);const Employee& ref = m;cout << e.bonus() << endl;cout << ref.bonus() << endl;4001100dynamic_cast: ما النوع الحقيقي؟
Section titled “dynamic_cast: ما النوع الحقيقي؟”مؤشر من نوع الأب قد يشير إلى أي ابن. للتحقق من النوع الحقيقي والوصول لدواله الخاصة نستخدم dynamic_cast:
vector<unique_ptr<Shape>> shapes;shapes.push_back(make_unique<Circle>(1));shapes.push_back(make_unique<Rectangle>(2, 3));
for (const auto& shape : shapes) { if (dynamic_cast<Circle*>(shape.get()) != nullptr) { cout << "هذه دائرة" << endl; } else { cout << "ليست دائرة" << endl; }}هذه دائرةليست دائرةإذا كان الكائن من النوع المطلوب أرجعت dynamic_cast مؤشراً إليه، وإلا أرجعت nullptr. وهي تعمل فقط مع الكلاسات التي فيها دوال virtual.
final: ممنوع الوراثة
Section titled “final: ممنوع الوراثة”كلمة final بعد اسم الكلاس تمنع الوراثة منه، وبعد دالة تمنع إعادة تعريفها في الأبناء:
class Token final {public: string value() const { return "abc123"; }};كتابة class MyToken : public Token بعد ذلك خطأ ترجمة.
جرّب بنفسك
Section titled “جرّب بنفسك”- أنشئ كلاساً مجرداً
Vehicleفيه حقلnameودالة خالصةwheels()ودالة عاديةinfo(). - أنشئ
CarوBikeيرثان منه، وضعهما فيvector<unique_ptr<Vehicle>>واطبع معلوماتهما. - أنشئ واجهة
Electricفيها الدالة الخالصةcharge()، وكلاسElectricCarيرث منVehicleوElectricمعاً. - تحدٍّ: مرّ على المركبات، واشحن فقط ما ينفّذ
Electricباستخدامdynamic_cast.
أخطاء شائعة
Section titled “أخطاء شائعة”- إنشاء كائن من كلاس مجرد: ستقرأ:
cannot declare variable to be of abstract type. أنشئ كائناً من أحد الأبناء. - نسيان تنفيذ دالة خالصة: الابن الذي لا ينفّذ كل الدوال الخالصة يبقى مجرداً هو الآخر، ويظهر الخطأ نفسه عند إنشاء كائن منه.
- نسيان const في التوقيع:
double area()وdouble area() constدالتان مختلفتان! كلمةoverrideتكشف هذا الخطأ فوراً، فلا تهملها. - تقطيع الكائن (Slicing): وضع ابن في متغير من نوع الأب بالقيمة مثل
Employee e = m;يقصّ الجزء الخاص بالابن. استخدم المراجع أو المؤشرات مع تعدد الأشكال.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تعني الكتابة = 0 في نهاية تعريف دالة virtual؟
هذه صيغة الدالة الافتراضية الخالصة، ووجودها يجعل الكلاس مجرداً.
سؤال
كيف نبني واجهة في C++؟
C++ لا تملك كلمة interface، فالواجهة كلاس مجرد خالص يرث منه الكلاس المنفِّذ.
الخلاصة
Section titled “الخلاصة”- الدالة الافتراضية الخالصة
= 0بلا جسم، وتجعل كلاسها مجرداً لا يُنشأ منه كائن. - الواجهة في C++ كلاس كل دواله خالصة، والكلاس يرث من أي عدد من الواجهات.
Base::method()تستدعي نسخة الأب من الدالة.dynamic_castتفحص النوع الحقيقي وتُرجعnullptrعند الفشل.finalتمنع الوراثة، واستخدم المراجع والمؤشرات لتفادي تقطيع الكائن.