إعادة تعريف الدوال (Method Overriding)
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 51 من 104.
يحدث تجاوز الدوال (Method overriding) عندما توفر فئة مشتقة (derived class) تنفيذًا خاصًا بها لدالة موجودة بالفعل في الفئة الأساسية (base class). يتيح ذلك للفئات المشتقة تخصيص السلوك الموروث مع الحفاظ على نفس توقيع الدالة (method signature).
لتجاوز دالة (method)، ما عليك سوى تعريف دالة في الفئة المشتقة بنفس الاسم ونوع الإرجاع والمعاملات تماماً:
class Animal {
public:
void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
d.speak(); // المخرجات: Woof!إصدار الفئة المشتقة يخفي إصدار الفئة الأساسية. ومع ذلك، لا يزال بإمكانك الوصول إلى دالة الفئة الأساسية باستخدام عامل دقة النطاق (scope resolution operator):
class Dog : public Animal {
public:
void speak() {
Animal::speak(); // استدعاء إصدار الفئة الأساسية
std::cout << "Woof!" << std::endl;
}
};هناك قيد مهم في هذا الشكل الأساسي من التجاوز (overriding). فعندما تصل إلى كائن مشتق عبر مؤشر أو مرجع للفئة الأساسية، يتم استدعاء نسخة الفئة الأساسية بدلاً من ذلك:
Dog d;
Animal* ptr = &d;
ptr->speak(); // المخرجات: Some sound (ليس Woof!)يحدث هذا لأن المترجم يحدد الطريقة التي سيتم استدعاؤها بناءً على نوع المؤشر، وليس نوع الكائن الفعلي. لتحقيق تعدد الأشكال الحقيقي في وقت التشغيل (runtime polymorphism) حيث يتم استدعاء الطريقة الصحيحة بناءً على الكائن الفعلي، ستحتاج إلى الدوال الافتراضية (virtual functions)، والتي سنغطيها في الدرس القادم.
التحدي
سهللنقم ببناء نظام إشعارات يوضح عملية تجاوز الدوال (method overriding) بشكل عملي. ستقوم بإنشاء فئة أساسية Notifier وفئتين متخصصتين للإشعارات تقومان بتخصيص كيفية تسليم الرسائل.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
Notifier.h: قم بتعريف فئة أساسيةNotifierتحتوي على:- عضو محمي (protected) باسم
std::string recipient - منشئ (constructor) يأخذ اسم المستلم ويقوم بتخزينه
- دالة عامة (public) باسم
send(const std::string& message)تقوم بطباعة:Sending to <recipient>: <message>
- عضو محمي (protected) باسم
EmailNotifier.h: قم بتعريف فئةEmailNotifierترث علنًا (publicly inherits) منNotifier:- منشئ يأخذ المستلم ويمرره إلى الفئة الأساسية
- تجاوز (Override) لدالة
send()لاستدعاء نسخة الفئة الأساسية أولاً باستخدامNotifier::send()، ثم طباعة:[Email sent successfully]
main.cpp: اقرأ مدخلين (كل منهما في سطر منفصل):- اسم المستلم (string)
- محتوى الرسالة (string)
قم بتعريف فئة
SMSNotifierمباشرة في ملف main.cpp ترث علنًا منNotifier:- منشئ يأخذ المستلم ويمرره إلى الفئة الأساسية
- تجاوز لدالة
send()لطباعة:SMS to <recipient>: <message>(مع استبدال سلوك الفئة الأساسية تمامًا دون استدعائها)
قم بإنشاء كائن واحد من كل نوع من أنواع الإشعارات (الفئة الأساسية
Notifier، وEmailNotifier، وSMSNotifier) باستخدام نفس المستلم. استدعِ دالةsend()لكل منها مع رسالة الإدخال، مع طباعة سطر فاصل بين كل منها:--- Base Notifier --- <output> --- Email Notifier --- <output> --- SMS Notifier --- <output>
على سبيل المثال، مع المدخلات Alice و Hello there!:
--- Base Notifier ---
Sending to Alice: Hello there!
--- Email Notifier ---
Sending to Alice: Hello there!
[Email sent successfully]
--- SMS Notifier ---
SMS to Alice: Hello there!لاحظ كيف تقوم EmailNotifier بتوسيع سلوك الفئة الأساسية عن طريق استدعاء دالة الأب أولاً، بينما تقوم SMSNotifier باستبدالها تمامًا بتنفيذها الخاص. لا تنسَ استخدام حراس الرأس (header guards) وتضمين الرؤوس المناسبة في كل ملف.
ورقة مرجعية
يسمح تجاوز الدوال (Method overriding) للفئة المشتقة بتقديم تنفيذ خاص بها لدالة من الفئة الأساسية، مع الحفاظ على نفس توقيع الدالة (الاسم، نوع الإرجاع، والمعاملات).
الصيغة الأساسية للتجاوز:
class Animal {
public:
void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
d.speak(); // Output: Woof!تقوم دالة الفئة المشتقة بإخفاء نسخة الفئة الأساسية. لاستدعاء دالة الفئة الأساسية من الفئة المشتقة، استخدم عامل دقة النطاق (scope resolution operator):
class Dog : public Animal {
public:
void speak() {
Animal::speak(); // استدعاء نسخة الفئة الأساسية
std::cout << "Woof!" << std::endl;
}
};قيد مهم: عند الوصول إلى كائن مشتق من خلال مؤشر أو مرجع للفئة الأساسية، يتم استدعاء نسخة الفئة الأساسية:
Dog d;
Animal* ptr = &d;
ptr->speak(); // Output: Some sound (not Woof!)هذا لأن المترجم يحدد الدالة التي سيتم استدعاؤها بناءً على نوع المؤشر، وليس نوع الكائن الفعلي. لتحقيق تعدد الأشكال الحقيقي في وقت التشغيل (runtime polymorphism)، استخدم الدوال الافتراضية (virtual functions).
جرّب بنفسك
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
// TODO: قم بتعريف فئة SMSNotifier هنا بحيث ترث بشكل علني (publicly) من Notifier
// - منشئ (Constructor) يمرر recipient إلى الفئة الأساسية (base class)
// - قم بعمل override للدالة send() لطباعة: SMS to <recipient>: <message>
// (استبدل سلوك الفئة الأساسية تماماً، ولا تستدعي send() الخاصة بالأب)
int main() {
string recipient;
string message;
getline(cin, recipient);
getline(cin, message);
// TODO: قم بإنشاء كائن Notifier مع recipient
// اطبع "--- Base Notifier ---"
// استدعِ send() مع message
// TODO: قم بإنشاء كائن EmailNotifier مع recipient
// اطبع "--- Email Notifier ---"
// استدعِ send() مع message
// TODO: قم بإنشاء كائن SMSNotifier مع recipient
// اطبع "--- SMS Notifier ---"
// استدعِ send() مع message
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مراجعة - الحاويات العامة