تخطَّ إلى المحتوى

الوراثة والواجهات في C++‎: الدوال الافتراضية الخالصة

الدرس 9 من 11 · 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
دائرة: 314
  • virtual 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
ضغطة رقم 2
  • class Button : public Drawable, public Clickable: C++‎ تسمح بالوراثة المتعددة، فالكلاس ينفّذ أي عدد من الواجهات.
  • المرجع Clickable& لا يعرف عن الزر إلا أنه «شيء يُضغط»، ومع ذلك يستدعي نسخة الزر من click.
  • كل واجهة لها مُدمِّر virtual كما تعلمت في درس الكلاسات.

الواجهة مثل مقبس الكهرباء: أي جهاز له القابس المناسب يعمل، ولا يهم المقبس هل هو مصباح أم ثلاجة.

الكلاس الوارث يستطيع أن يبني على دالة أبيه بدل استبدالها كلياً، بكتابة اسم الأب ثم :::

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;
الناتج
400
1100

dynamic_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 بعد اسم الكلاس تمنع الوراثة منه، وبعد دالة تمنع إعادة تعريفها في الأبناء:

class Token final {
public:
string value() const {
return "abc123";
}
};

كتابة class MyToken : public Token بعد ذلك خطأ ترجمة.

  1. أنشئ كلاساً مجرداً Vehicle فيه حقل name ودالة خالصة wheels() ودالة عادية info().
  2. أنشئ Car و Bike يرثان منه، وضعهما في vector<unique_ptr<Vehicle>> واطبع معلوماتهما.
  3. أنشئ واجهة Electric فيها الدالة الخالصة charge()، وكلاس ElectricCar يرث من Vehicle و Electric معاً.
  4. تحدٍّ: مرّ على المركبات، واشحن فقط ما ينفّذ Electric باستخدام dynamic_cast.
  • إنشاء كائن من كلاس مجرد: ستقرأ: cannot declare variable to be of abstract type. أنشئ كائناً من أحد الأبناء.
  • نسيان تنفيذ دالة خالصة: الابن الذي لا ينفّذ كل الدوال الخالصة يبقى مجرداً هو الآخر، ويظهر الخطأ نفسه عند إنشاء كائن منه.
  • نسيان const في التوقيع: double area() و double area() const دالتان مختلفتان! كلمة override تكشف هذا الخطأ فوراً، فلا تهملها.
  • تقطيع الكائن (Slicing): وضع ابن في متغير من نوع الأب بالقيمة مثل Employee e = m; يقصّ الجزء الخاص بالابن. استخدم المراجع أو المؤشرات مع تعدد الأشكال.

سؤال

ماذا تعني الكتابة = 0 في نهاية تعريف دالة virtual؟

سؤال

كيف نبني واجهة في C++‎؟

  • الدالة الافتراضية الخالصة = 0 بلا جسم، وتجعل كلاسها مجرداً لا يُنشأ منه كائن.
  • الواجهة في C++‎ كلاس كل دواله خالصة، والكلاس يرث من أي عدد من الواجهات.
  • Base::method() تستدعي نسخة الأب من الدالة.
  • dynamic_cast تفحص النوع الحقيقي وتُرجع nullptr عند الفشل.
  • final تمنع الوراثة، واستخدم المراجع والمؤشرات لتفادي تقطيع الكائن.