C++: تعدد الأشكال
آخر تحديث: 2026-08-26
في الدرس 31، تعلمنا عن الوراثة - التي تتيح للفئات المشتقة إعادة استخدام كود الفئة الأساسية.
لكن في السيناريوهات الواقعية، تحتاج إلى استخدام مؤشر الفئة الأساسية لمعالجة كائنات الفئات المشتقة المختلفة بشكل موحد.
هذا هو تعدد الأشكال - واجهة واحدة، تطبيقات متعددة.
1. ما هو تعدد الأشكال؟
(1) 1.1 تعدد الأشكال في الحياة اليومية
| مثال من الحياة اليومية | المكافئ البرمجي |
|---|---|
| تطلب من كلب أن يتحدث، فينبح | استدعاء speak() على مؤشر الفئة الأساسية، لكن يُنفَّذ إصدار الفئة المشتقة |
| تطلب من قطة أن تتحدث، فتموو | نفس استدعاء الدالة يُنتج سلوكاً مختلفاً لكائنات مختلفة |
جوهر تعدد الأشكال: استخدام مؤشر الفئة الأساسية للإشارة إلى كائن الفئة المشتقة، بحيث يستدعى دالة افتراضية ترتبط ديناميكياً بتطبيق الفئة المشتقة.
(2) 1.2 لماذا نحتاج تعدد الأشكال؟
بدون تعدد الأشكال (ممل):
▶ مثال 2: عرض البرمجة كائنية التوجه (الصعوبة ⭐)
#include <iostream>
class Dog {
public:
void speak() { std::cout << "Woof!" << std::endl; }
};
class Cat {
public:
void speak() { std::cout << "Meow!" << std::endl; }
};
int main() {
Dog* d = new Dog();
Cat* c = new Cat();
d->speak();
c->speak();
// ❌ يجب التعامل مع كل نوع على حدة
return 0;
}
الخرج:
Woof!
Meow!
مع تعدد الأشكال (موجز):
#include <iostream>
class Animal {
public:
virtual void speak() { std::cout << "Animal speaks" << std::endl; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "Meow!" << std::endl; }
};
int main() {
Animal* animals = {new Dog(), new Cat()};
for (int i = 0; i < 2; i++) {
animals[i]->speak(); // ✅ تعدد الأشكال: يستدعي الإصدار الصحيح تلقائياً
}
return 0;
}
2. الدوال الافتراضية (virtual)
(1) 2.1 الاستخدام الأساسي
أعلن عن دالة كـ virtual في الفئة الأساسية، و override (تجاوزها) في الفئة المشتقة.
#include <iostream>
#include <string>
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
// إعلان كدالة افتراضية
virtual void speak() {
std::cout << name << " makes a sound." << std::endl;
}
// يجب أن يكون للفئة الأساسية مدمر افتراضي أيضاً!
virtual ~Animal() {}
};
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}
void speak() override {
std::cout << name << " says: Woof!" << std::endl;
}
};
int main() {
Animal* a = new Dog("Buddy");
a->speak(); // ✅ الخرج: Buddy says: Woof!
delete a; // ✅ سيستدعي مدمر Dog أولاً، ثم مدمر Animal
return 0;
}
⚠️ نقطة مهمة: عندما تحتوي الفئة الأساسية على دوال افتراضية، يجب إعلان المدمر كـ virtual أيضاً! وإلا فإن delete على مؤشر الفئة الأساسية لن يستدعي مدمر الفئة المشتقة.
(2) 2.2 الكلمة المفتاحية override (C++11)
أدخلت C++11 الكلمة المفتاحية override، التي تتيح للمترجم التحقق مما إذا كنت تتجاوز دالة افتراضية فعلاً.
class Dog : public Animal {
public:
// ✅ مع override، سيتحقق المترجم
void speak() override { ... }
// ❌ إذا كان التوقيع خاطئاً (مثلاً void speak(int x))، سيُصدر المترجم خطأ
// void speak(int x) override { ... } // خطأ ترجمة: لا توجد دالة لتجاوزها
};
💡 التوصية: استخدم دائماً override - دع المترجم يكتشف الأخطاء نيابة عنك.
3. الدوال الافتراضية البحتة والفئات المجردة
(1) 3.1 ما هي الدالة الافتراضية البحتة؟
الدالة الافتراضية البحتة هي دالة افتراضية يُعلن عنها فقط ولا تُنفَّذ في الفئة الأساسية، تُعلَّم بـ = 0.
class Animal {
public:
// دالة افتراضية بحتة
virtual void speak() = 0;
virtual ~Animal() {}
};
تُسمى الفئة التي تحتوي على دوال افتراضية بحتة فئة مجردة - لا يمكن إنشاء نسخ منها ولا يمكنها العمل إلا كفئة أساسية.
Animal a; // ❌ خطأ ترجمة: لا يمكن إنشاء نسخة من فئة مجردة
Animal* p = new Dog("Buddy"); // ✅ يمكن استخدام مؤشر الفئة الأساسية للإشارة إلى فئة مشتقة
(2) 3.2 لماذا نحتاج الدوال الافتراضية البحتة؟
لـ إجبار الفئات المشتقة على تجاوز الدالة.
class Animal {
public:
virtual void speak() = 0; // إجبار الفئات المشتقة على التنفيذ
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
// ✅ إذا نسيت تجاوز speak()، سيُصدر المترجم خطأ
};
4. كيف تعمل الدوال الافتراضية (اختياري)
(1) 4.1 جدول الدوال الافتراضية (vtable)
يُنفذ C++ تعدد الأشكال باستخدام جدول الدوال الافتراضية (vtable):
- بالنسبة للفئات التي تحتوي على دوال افتراضية، يُنشئ المترجم
vtable(جدول يخزن عناوين الدوال الافتراضية) - كل كائن يحتوي على
vptrمخفي (مؤشر إلى vtable) - عند استدعاء دالة افتراضية، يُعثر على عنوان الدالة الصحيح عبر
vptr
💡 نصيحة: هذا هو السبب في أن كائنات الفئات التي تحتوي على دوال افتراضية تشغل مساحة إضافية بحجم مؤشر (8 بايت على أنظمة 64 بت).
5. تطبيق: حساب مساحة الأشكال الهندسية
▶ مثال 1: استخدام تعدد الأشكال لحساب مساحات أشكال مختلفة (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <cmath>
// فئة أساسية مجردة
class Shape {
public:
virtual double area() = 0;
virtual void printInfo() = 0;
virtual ~Shape() {}
};
// دائرة
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
void printInfo() override {
std::cout << "Circle, radius = " << radius
<< ", area = " << area() << std::endl;
}
};
// مستطيل
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() override {
return width * height;
}
void printInfo() override {
std::cout << "Rectangle, " << width << " x " << height
<< ", area = " << area() << std::endl;
}
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(4.0, 6.0));
for (Shape* s : shapes) {
s->printInfo();
}
// تحرير الذاكرة
for (Shape* s : shapes) {
delete s;
}
return 0;
}
الخرج:
Circle, radius =
Rectangle, x
❓ أسئلة شائعة
س: لماذا يجب أن يكون المدمر افتراضياً إذا كانت هناك دوال افتراضية؟ ج: إذا قمت بـ
deleteمؤشر فئة أساسية يشير إلى كائن فئة مشتقة، ومدمر الفئة الأساسية ليس افتراضياً، فسيُستدعى مدمر الفئة الأساسية فقط، مما يسبب تسرب الموارد في الفئة المشتقة.CPPAnimal* a = new Dog("Buddy"); delete a; // ❌ إذا لم يكن ~Animal() افتراضياً، لن يُستدعى مدمر Dogالقاعدة الذهبية: إذا كانت الفئة الأساسية تحتوي على دوال افتراضية، يجب أن يكون
~BaseClass()افتراضياً.
س: ما الفرق بين override و final؟ ج: > -
override: تخبر المترجم أن هذه الدالة تتجاوز دالة افتراضية في الفئة الأساسية (دع المترجم يتحقق لك) > -final: تخبر المترجم أن هذه الدالة لا يمكن تجاوزها (تُستخدم في الفئات المشتقة)CPPclass Dog : public Animal { public: void speak() override final { ... } // final: لم تعد الفئات المشتقة قادرة على تجاوز speak() };
س: متى أستخدم تعدد الأشكال؟ ج: عندما تحتاج إلى معالجة كائنات من أنواع مختلفة عبر واجهة موحدة.
سيناريوهات نموذجية:
- الألعاب: جميع الشخصيات ترث من
Character، تُدار بشكل موحد بـCharacter*- واجهات المستخدم الرسومية: جميع عناصر التحكم ترث من
Widget، تُرسم بشكل موحد بـWidget*- أنظمة الملفات: جميع الملفات ترث من
File، تُقرأ/تكتب بشكل موحد بـFile*
▶ مثال 3: تعدد أشكال الدوال الافتراضية (الصعوبة ⭐)
#include <iostream>
#include <string>
class Shape {
public:
virtual double area() { return 0; }
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override { return 3.14 * radius * radius; }
};
int main() {
Shape* s = new Circle(2.0);
std::cout << "Surface Product: " << s->area() << std::endl;
delete s;
return 0;
}
الخرج:
Surface Product:
virtual للإعلان عن الدوال الافتراضية و override للإشارة صراحةً إلى التجاوز. استدعاء مؤشر الفئة الأساسية لدالة الفئة المشتقة هو تعدد الأشكال.
- تعدد الأشكال يتيح لمؤشرات الفئة الأساسية استدعاء التطبيق الصحيح للفئة المشتقة
- استخدم
virtualفي الفئة الأساسية للإعلان عن الدوال الافتراضية - استخدم
overrideفي الفئة المشتقة (دع المترجم يتحقق لك) - إذا كانت الفئة تحتوي على دوال افتراضية، يجب أن يكون مدمرها
virtualأيضاً - الدوال الافتراضية البحتة (
= 0) تُجبر الفئات المشتقة على التجاوز - الفئات التي تحتوي على دوال افتراضية بحتة هي فئات مجردة ولا يمكن إنشاء نسخ منها
📖 ملخص
- تعدد الأشكال: مؤشر الفئة الأساسية يستدعي دوال الفئة المشتقة
- virtual: يعلن عن الدوال الافتراضية
- override: يشير صراحةً إلى تجاوز دالة الفئة الأساسية
- المدمر الافتراضي: يضمن تنظيف موارد الفئة المشتقة بشكل صحيح
📝 تمارين
-
أساسي (الصعوبة ⭐): عرّف فئة أساسية
Vehicleبدالة افتراضية بحتةvoid move(). اجعلCarوBicycleترثان منها، بحيث تُخرج الأولى "السيارة تسير على الطريق" والثانية "الدراجة تسير على المسار". -
متوسط (الصعوبة ⭐⭐): وسّع البرنامج أعلاه بإضافة دالة افتراضية
void stop()، بحيث تُخرجCar"السيارة تتوقف" وBicycle"الدراجة تكبح". -
متقدم (الصعوبة ⭐⭐⭐): صمم نظام رواتب الموظفين:
-
Employeeفئة أساسية مجردة (الاسم، الرقم الوظيفي،virtual double calcSalary() = 0) -
FullTimeEmployee(الراتب الشهري) -
PartTimeEmployee(الأجر بالساعة × ساعات العمل) -
استخدم
vector<Employee*>لتخزين جميع الموظفين واستدعاءcalcSalary()بشكل موحد -
الدوال الافتراضية: تُعلن في الفئة الأساسية، تُتجاوز في الفئة المشتقة، تعدد الأشكال في وقت التنفيذ
- الكلمة المفتاحية virtual تعلن عن الدوال الافتراضية
- override يتحقق من التجاوز الصحيح
- الدوال الافتراضية البحتة والفئات المجردة لا يمكن إنشاء نسخ منها
- جدول الدوال الافتراضية (vtable) يُنفذ تعدد الأشكال في وقت التنفيذ
6. 🚀 الخطوات التالية
الآن بعد أن تعلمت تعدد الأشكال، لننتقل إلى القوالب (الدرس 33) - جعل الدوال والفئات تدعم أي نوع لتحقيق "البرمجة العامة" الحقيقية!