Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

تعدد الأشكال: وقت التجميع مقابل وقت التشغيل

جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 56 من 104.

تعني كلمة Polymorphism "أشكالاً متعددة"، وهي مفهوم أساسي في البرمجة كائنية التوجه (OOP) تتيح التعامل مع الكائنات بشكل موحد رغم اختلاف سلوكها. تدعم لغة C++ نوعين متميزين من تعدد الأشكال، يتم تحديد كل منهما في مرحلة مختلفة من مراحل تنفيذ البرنامج.

تعدد الأشكال في وقت الترجمة (Compile-time polymorphism) (يُسمى أيضًا تعدد الأشكال الساكن) يتم حله بواسطة المترجم قبل تشغيل البرنامج. يحدد المترجم بالضبط أي دالة سيتم استدعاؤها بناءً على توقيع الدالة (function signature). يتضمن ذلك التحميل الزائد للدوال (function overloading) والقوالب (templates):

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // المترجم يختار print(int)
print(3.14);   // المترجم يختار print(double)

تعدد الأشكال في وقت التشغيل (Runtime polymorphism) (يُسمى أيضاً تعدد الأشكال الديناميكي) يتم حله أثناء تشغيل البرنامج. يعتمد القرار بشأن أي دالة سيتم استدعاؤها على نوع الكائن الفعلي، وليس على نوع المؤشر أو المرجع. يتم تحقيق ذلك من خلال الدوال الافتراضية (virtual functions):

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing circle" << std::endl; }
};

Shape* s = new Circle();
s->draw();  // يتم تحديده في وقت التشغيل: "Drawing circle"

المقايضة الرئيسية: يتميز تعدد الأشكال في وقت التجميع (compile-time polymorphism) بعدم وجود أي عبء إضافي في وقت التشغيل نظرًا لاتخاذ القرارات أثناء عملية التجميع، بينما يضيف تعدد الأشكال في وقت التشغيل (runtime polymorphism) تكلفة بسيطة (vtable lookup) ولكنه يوفر مرونة أكبر للتعامل مع الكائنات التي لا تُعرف أنواعها حتى وقت التنفيذ.

challenge icon

التحدي

سهل

لنقم ببناء نظام آلة حاسبة يوضح كلا نوعي تعدد الأشكال (polymorphism) جنباً إلى جنب. ستقوم بإنشاء نظام حيث يتعامل تعدد الأشكال في وقت التجميع (compile-time polymorphism) مع أنواع مدخلات مختلفة من خلال تحميل الدوال الزائد (function overloading)، بينما يسمح تعدد الأشكال في وقت التشغيل (runtime polymorphism) بتبديل استراتيجيات الحساب المختلفة ديناميكياً.

ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:

  • Calculator.h: قم بتعريف فئة أساسية Calculator تمثل أي استراتيجية حسابية:
    • دالة افتراضية calculate(int a, int b) تعيد int وتطبع: Base calculation: <a> ? <b> (وتعيد 0)
    • مدمر افتراضي (virtual destructor)
  • Operations.h: قم بتعريف فئتين مشتقتين من الآلة الحاسبة تقومان بتجاوز (override) سلوك الحساب:
    • Adder: تجاوز calculate() لطباعة Adding: <a> + <b> وإرجاع المجموع
    • Multiplier: تجاوز calculate() لطباعة Multiplying: <a> * <b> وإرجاع حاصل الضرب
    يجب أن تستخدم كلتا الفئتين الكلمة المفتاحية override.
  • main.cpp: أنشئ نظاماً يعرض كلا نوعي تعدد الأشكال. اقرأ مدخلين صحيحين (كل منهما في سطر منفصل).

    أولاً، استعرض تعدد الأشكال في وقت التجميع عن طريق إنشاء ثلاث دوال display() محملة بشكل زائد:

    • display(int x) تطبع: Integer value: <x>
    • display(double x) تطبع: Double value: <x>
    • display(const std::string& x) تطبع: String value: <x>

    ثم استعرض تعدد الأشكال في وقت التشغيل عن طريق إنشاء مصفوفة من مؤشرات Calculator* تحتوي على Calculator أساسي، و Adder، و Multiplier. قم بالمرور عبر حلقة واستدعاء calculate() على كل منها باستخدام قيم المدخلات الخاصة بك، مع طباعة النتيجة بعد كل عملية حسابية.

    قم بهيكلة مخرجاتك كما يلي:

    === Compile-Time Polymorphism ===
    <display outputs for int, double, string>
    
    === Runtime Polymorphism ===
    <calculate outputs with results>

    بالنسبة لقسم وقت التجميع، استدعِ display() مع المدخل الأول كعدد صحيح، ثم كعدد عشري (نفس القيمة مع إضافة .5)، ثم كالسلسلة النصية "Result". قم بتنظيف الآلات الحاسبة المخصصة ديناميكياً عند الانتهاء.

على سبيل المثال، مع المدخلات 10 و 3:

=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result

=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30

لاحظ كيف يختار المترجم التحميل الزائد الصحيح لـ display() بناءً على نوع الوسيط (قرار وقت التجميع)، بينما يتم تحديد دالة calculate() الصحيحة بناءً على نوع الكائن الفعلي في وقت التشغيل من خلال آلية vtable.

ورقة مرجعية

تدعم لغة C++ نوعين من تعدد الأشكال (polymorphism) يتم حلهما في مراحل مختلفة:

يتم حل تعدد الأشكال في وقت التجميع (static polymorphism) بواسطة المترجم قبل التنفيذ. ويتضمن ذلك تحميل الدوال الزائد (function overloading) والقوالب (templates):

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // Compiler chooses print(int)
print(3.14);   // Compiler chooses print(double)

يتم حل تعدد الأشكال في وقت التشغيل (dynamic polymorphism) أثناء التنفيذ باستخدام الدوال الافتراضية (virtual functions). حيث يحدد نوع الكائن الفعلي الدالة التي سيتم استدعاؤها:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing circle" << std::endl; }
};

Shape* s = new Circle();
s->draw();  // Decided at runtime: "Drawing circle"

المقايضة: لا يتطلب تعدد الأشكال في وقت التجميع أي عبء إضافي وقت التشغيل، بينما يضيف تعدد الأشكال في وقت التشغيل تكلفة بسيطة (البحث في vtable) ولكنه يوفر مرونة أكبر.

جرّب بنفسك

#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"

// TODO: قم بإنشاء ثلاث دوال display() محملة بشكل زائد (overloaded):
// 1. display(int x) - يطبع "Integer value: <x>"
// 2. display(double x) - يطبع "Double value: <x>"
// 3. display(const std::string& x) - يطبع "String value: <x>"



int main() {
    // قراءة مدخلين من نوع integer
    int a, b;
    std::cin >> a;
    std::cin >> b;
    
    // === Compile-Time Polymorphism ===
    std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
    // TODO: استدعِ display() باستخدام:
    // - a كعدد صحيح (integer)
    // - a كعدد عشري (double) (أضف 0.5 إليه)
    // - السلسلة النصية "Result"
    
    
    std::cout << std::endl;
    
    // === Runtime Polymorphism ===
    std::cout << "=== Runtime Polymorphism ===" << std::endl;
    // TODO: قم بإنشاء مصفوفة من مؤشرات Calculator* تحتوي على 3 عناصر:
    // - كائن Calculator أساسي
    // - كائن Adder
    // - كائن Multiplier
    
    // TODO: قم بالمرور عبر المصفوفة، واستدعِ calculate(a, b) لكل عنصر،
    // واطبع "Result: <return_value>" بعد كل عملية حسابية
    
    
    // TODO: تنظيف الذاكرة المخصصة ديناميكيًا
    
    
    return 0;
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming