تعدد الأشكال: وقت التجميع مقابل وقت التشغيل
جزء من قسم 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) ولكنه يوفر مرونة أكبر للتعامل مع الكائنات التي لا تُعرف أنواعها حتى وقت التنفيذ.
التحدي
سهللنقم ببناء نظام آلة حاسبة يوضح كلا نوعي تعدد الأشكال (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;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة