الدوال الافتراضية في C++: لماذا لا يمكن الاستغناء عنها؟
\n\nتخيل أنك مخرج سينمائي تقوم بتصوير فيلم عن الحيوانات. لديك سيناريو: "جميع الحيوانات تصدر صوتًا". تقوم بتصوير مشهد مع كلب - ينبح. مشهد مع قطة - تموء. كل شيء منطقي. والآن تخيل أن مساعدك (المترجم) يحضر لك صندوقًا من الحيوانات الألعاب ويقول: "صور!". تضغط على الزر، واللعبة... تصمت. لماذا؟ لأنك نسيت الدوال الافتراضية.
\n\nفي البرمجة بلغة C++، هذه المشكلة تتكرر باستمرار. تكتب فئة "حيوان"، بداخلها دالة sound(). ثم تنشئ فئتي "كلب" و"قطة" ترثان من "حيوان". ويبدو كل شيء على ما يرام، حتى تحاول العمل مع هذه الكائنات من خلال مؤشر على الفئة الأساسية. هنا يتبين أن C++ بشكل افتراضي يستدعي دالة الفئة الأساسية، وليس الدالة المعاد تعريفها في الفئة المشتقة. الدوال الافتراضية تحل هذه المشكلة نهائيًا.
في هذه المقالة، سنشرح بالتفصيل ما هي الدوال الافتراضية، ولماذا نحتاجها، وكيف تعمل تحت الغطاء، ولماذا لا يمكن تصور C++ الحديثة بدونها.
\n\nما هي الدالة الافتراضية بكلمات بسيطة
\n\nالدالة الافتراضية هي دالة في الفئة تقوم بتعليمها بالكلمة المفتاحية virtual. هذه الدالة يمكن إعادة تعريفها في الفئات المشتقة، وعند الاستدعاء من خلال مؤشر أو مرجع إلى الفئة الأساسية، سيتم استدعاء النسخة من الفئة المشتقة.
يبدو الأمر معقدًا؟ دعنا نأخذ مثالًا من الحياة. تخيل جهاز تحكم عن بعد للروبوتات. لديك فئة أساسية "روبوت" بدالة doWork(). هناك روبوت مكنسة كهربائية وروبوت توصيل. إذا ضغطت على زر "اعمل"، يجب أن تبدأ الخوارزميات المناسبة: المكنسة تبدأ بالتنظيف، وروبوت التوصيل يبدأ بتوصيل الطرد. بدون الدوال الافتراضية، كنت ستضغط على الزر والروبوت سيقف مكانه (سيتم استدعاء دالة الفئة الأساسية).
في C++، الدوال الافتراضية هي أساس تعدد الأشكال. تعدد الأشكال يسمح بالعمل بشكل موحد مع كائنات من أنواع مختلفة، إذا كانت ترث من سلف واحد.
\n\nالمشكلة بدون الدوال الافتراضية: مثال توضيحي
\n\nدعنا نرى ما يحدث إذا لم نستخدم virtual.
#include <iostream>\n#include <vector>\n\nclass Animal {\npublic:\n void sound() {\n std::cout << "Some generic animal sound" << std::endl;\n }\n};\n\nclass Dog : public Animal {\npublic:\n void sound() {\n std::cout << "Woof! Woof!" << std::endl;\n }\n};\n\nclass Cat : public Animal {\npublic:\n void sound() {\n std::cout << "Meow!" << std::endl;\n }\n};\n\nint main() {\n std::vector<Animal*> animals;\n animals.push_back(new Dog());\n animals.push_back(new Cat());\n\n for (Animal* a : animals) {\n a->sound(); // ماذا سيتم طباعته؟\n }\n\n // تنظيف الذاكرة\n for (Animal* a : animals) delete a;\n\n return 0;\n}\n\n\nقم بتشغيل هذا الكود. سترى:
\nSome generic animal sound\nSome generic animal sound\n\n\nلماذا؟ لأن المترجم ينظر إلى نوع المؤشر (Animal*)، وليس إلى الكائن الفعلي. يستدعي دالة الفئة الأساسية. هذا يسمى الربط المبكر (early binding) - يتم اتخاذ قرار أي دالة سيتم استدعاؤها في مرحلة الترجمة.
ونحن نريد الربط المتأخر (late binding) - بحيث يتم اتخاذ القرار أثناء تنفيذ البرنامج، اعتمادًا على نوع الكائن. وهذا بالضبط ما توفره الدوال الافتراضية.
\n\nالحل: إضافة virtual
\n\nلنصلح المثال بإضافة الكلمة المفتاحية virtual.
class Animal {\npublic:\n virtual void sound() { // أضفنا virtual\n std::cout << "Some generic animal sound" <<