C++: تعدد الأشكال

آخر تحديث: 2026-08-26

في الدرس 31، تعلمنا عن الوراثة - التي تتيح للفئات المشتقة إعادة استخدام كود الفئة الأساسية.

لكن في السيناريوهات الواقعية، تحتاج إلى استخدام مؤشر الفئة الأساسية لمعالجة كائنات الفئات المشتقة المختلفة بشكل موحد.

هذا هو تعدد الأشكال - واجهة واحدة، تطبيقات متعددة.


1. ما هو تعدد الأشكال؟

(1) 1.1 تعدد الأشكال في الحياة اليومية

مثال من الحياة اليومية المكافئ البرمجي
تطلب من كلب أن يتحدث، فينبح استدعاء speak() على مؤشر الفئة الأساسية، لكن يُنفَّذ إصدار الفئة المشتقة
تطلب من قطة أن تتحدث، فتموو نفس استدعاء الدالة يُنتج سلوكاً مختلفاً لكائنات مختلفة

جوهر تعدد الأشكال: استخدام مؤشر الفئة الأساسية للإشارة إلى كائن الفئة المشتقة، بحيث يستدعى دالة افتراضية ترتبط ديناميكياً بتطبيق الفئة المشتقة.

(2) 1.2 لماذا نحتاج تعدد الأشكال؟

بدون تعدد الأشكال (ممل):

▶ مثال 2: عرض البرمجة كائنية التوجه (الصعوبة ⭐)

TEXT 📖 للعرض فقط
#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;
}

الخرج:

TEXT 📖 للعرض فقط
Woof!
Meow!

مع تعدد الأشكال (موجز):

CPP
#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 (تجاوزها) في الفئة المشتقة.

TEXT 📖 للعرض فقط
#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، التي تتيح للمترجم التحقق مما إذا كنت تتجاوز دالة افتراضية فعلاً.

CPP
class Dog : public Animal {
public:
 // ✅ مع override، سيتحقق المترجم
 void speak() override { ... }
 
 // ❌ إذا كان التوقيع خاطئاً (مثلاً void speak(int x))، سيُصدر المترجم خطأ
 // void speak(int x) override { ... } // خطأ ترجمة: لا توجد دالة لتجاوزها
};

💡 التوصية: استخدم دائماً override - دع المترجم يكتشف الأخطاء نيابة عنك.



3. الدوال الافتراضية البحتة والفئات المجردة

(1) 3.1 ما هي الدالة الافتراضية البحتة؟

الدالة الافتراضية البحتة هي دالة افتراضية يُعلن عنها فقط ولا تُنفَّذ في الفئة الأساسية، تُعلَّم بـ = 0.

TEXT 📖 للعرض فقط
class Animal {
public:
 // دالة افتراضية بحتة
 virtual void speak() = 0;
 
 virtual ~Animal() {}
};

تُسمى الفئة التي تحتوي على دوال افتراضية بحتة فئة مجردة - لا يمكن إنشاء نسخ منها ولا يمكنها العمل إلا كفئة أساسية.

CPP
Animal a; // ❌ خطأ ترجمة: لا يمكن إنشاء نسخة من فئة مجردة
Animal* p = new Dog("Buddy"); // ✅ يمكن استخدام مؤشر الفئة الأساسية للإشارة إلى فئة مشتقة

(2) 3.2 لماذا نحتاج الدوال الافتراضية البحتة؟

لـ إجبار الفئات المشتقة على تجاوز الدالة.

TEXT 📖 للعرض فقط
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):

  1. بالنسبة للفئات التي تحتوي على دوال افتراضية، يُنشئ المترجم vtable (جدول يخزن عناوين الدوال الافتراضية)
  2. كل كائن يحتوي على vptr مخفي (مؤشر إلى vtable)
  3. عند استدعاء دالة افتراضية، يُعثر على عنوان الدالة الصحيح عبر vptr

💡 نصيحة: هذا هو السبب في أن كائنات الفئات التي تحتوي على دوال افتراضية تشغل مساحة إضافية بحجم مؤشر (8 بايت على أنظمة 64 بت).



5. تطبيق: حساب مساحة الأشكال الهندسية

▶ مثال 1: استخدام تعدد الأشكال لحساب مساحات أشكال مختلفة (الصعوبة ⭐⭐)

CPP 📖 للعرض فقط
#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;
}
44 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

الخرج:

TEXT 📖 للعرض فقط
Circle, radius = 
Rectangle,  x 

❓ أسئلة شائعة

س: لماذا يجب أن يكون المدمر افتراضياً إذا كانت هناك دوال افتراضية؟ ج: إذا قمت بـ delete مؤشر فئة أساسية يشير إلى كائن فئة مشتقة، ومدمر الفئة الأساسية ليس افتراضياً، فسيُستدعى مدمر الفئة الأساسية فقط، مما يسبب تسرب الموارد في الفئة المشتقة.

CPP
Animal* a = new Dog("Buddy");
delete a; // ❌ إذا لم يكن ~Animal() افتراضياً، لن يُستدعى مدمر Dog

القاعدة الذهبية: إذا كانت الفئة الأساسية تحتوي على دوال افتراضية، يجب أن يكون ~BaseClass() افتراضياً.

س: ما الفرق بين override و final؟ ج: > - override: تخبر المترجم أن هذه الدالة تتجاوز دالة افتراضية في الفئة الأساسية (دع المترجم يتحقق لك) > - final: تخبر المترجم أن هذه الدالة لا يمكن تجاوزها (تُستخدم في الفئات المشتقة)

CPP
class Dog : public Animal {
public:
void speak() override final { ... } // final: لم تعد الفئات المشتقة قادرة على تجاوز speak()
};

س: متى أستخدم تعدد الأشكال؟ ج: عندما تحتاج إلى معالجة كائنات من أنواع مختلفة عبر واجهة موحدة.

سيناريوهات نموذجية:

  • الألعاب: جميع الشخصيات ترث من Character، تُدار بشكل موحد بـ Character*
  • واجهات المستخدم الرسومية: جميع عناصر التحكم ترث من Widget، تُرسم بشكل موحد بـ Widget*
  • أنظمة الملفات: جميع الملفات ترث من File، تُقرأ/تكتب بشكل موحد بـ File*

▶ مثال 3: تعدد أشكال الدوال الافتراضية (الصعوبة ⭐)

CPP
#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;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
Surface Product: 
💡 نصيحة: استخدم virtual للإعلان عن الدوال الافتراضية و override للإشارة صراحةً إلى التجاوز. استدعاء مؤشر الفئة الأساسية لدالة الفئة المشتقة هو تعدد الأشكال.



📖 ملخص

📝 تمارين

  1. أساسي (الصعوبة ⭐): عرّف فئة أساسية Vehicle بدالة افتراضية بحتة void move(). اجعل Car و Bicycle ترثان منها، بحيث تُخرج الأولى "السيارة تسير على الطريق" والثانية "الدراجة تسير على المسار".

  2. متوسط (الصعوبة ⭐⭐): وسّع البرنامج أعلاه بإضافة دالة افتراضية void stop()، بحيث تُخرج Car "السيارة تتوقف" و Bicycle "الدراجة تكبح".

  3. متقدم (الصعوبة ⭐⭐⭐): صمم نظام رواتب الموظفين:

  4. Employee فئة أساسية مجردة (الاسم، الرقم الوظيفي، virtual double calcSalary() = 0)

  5. FullTimeEmployee (الراتب الشهري)

  6. PartTimeEmployee (الأجر بالساعة × ساعات العمل)

  7. استخدم vector<Employee*> لتخزين جميع الموظفين واستدعاء calcSalary() بشكل موحد

  8. الدوال الافتراضية: تُعلن في الفئة الأساسية، تُتجاوز في الفئة المشتقة، تعدد الأشكال في وقت التنفيذ


6. 🚀 الخطوات التالية

الآن بعد أن تعلمت تعدد الأشكال، لننتقل إلى القوالب (الدرس 33) - جعل الدوال والفئات تدعم أي نوع لتحقيق "البرمجة العامة" الحقيقية!

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%