إعادة التعريف في السي بلاس بلاس | C++ Overriding

مفهوم ال Overriding في C++

مفهوم إعادة التعريف في C++

في الدرس السابق, شاهدت كيف أنه عندما يرث الكلاس من كلاس آخر فإنه يرث المتغيرات و الدوال الموجودة فيه.

إعادة التعريف أو التعريف من جديد ( Overriding ) تعني تعريف نفس الدالة التي ورثها الكلاس الإبن من الكلاس الأب من جديد, و هذه الدالة الجديدة تكون مشابهة للدالة الموروثة من حيث الشكل فقط, أي لها نفس الإسم و النوع و عدد الباراميترات, لكن محتواها مختلف بهدف أن يكون متناسب أكثر مع الكلاس الإبن.

الهدف الحقيقي من إعادة التعريف أو التعريف من جديد هو إتاحة الفرصة للكلاس الإبن ليعرّف الدوال حسب حاجته.
في دروس متقدمة سنرث من كلاسات جاهزة في C++, و نفعل Override للدوال الموجودة فيها لكي تناسب التطبيقات التي سنقوم ببنائها.


شروط إعادة تعريف الدوال الموروثة في C++

  • يجب أن يكون الـ Modifier المستخدم للدالة الجديدة هو نفسه المستخدم للدالة القديمة, و يجب أن يكون نوعه public أو protected.

  • عدد و نوع باراميترات الدالة الجديدة يجب أن يطابق عدد و نوع باراميترات الدالة القديمة.

  • نوع الإرجاع للدالة الجديدة يجب أن يكون نفس نوع الإرجاع للدالة القديمة.

  • الدالة المعرفة كـ private لا يمكن أن نفعل لها Override, لأن كلمة private تمنع إمكانية الوصول المباشر للدالة من الكلاس الإبن.

  • الدالة المعرفة كـ final لا يمكن أن نفعل لها Override, لأن كلمة final تمنع تغير محتوى الدالة بعد تعريفها.

  • الدالة المعرفة كـ static لا يمكن أن نفعل لها Override و لكن يمكن تعريفها من جديد في أي مكان, لأن كلمة static تجعل الدالة مشتركة بين جميع الكلاسات.

  • لا يمكن أن نفعل Override للكونستركتور.

مثال يوضح فائدة إعادة تعريف الدوال في C++

في المثال التالي قمنا بتعريف كلاس إسمه Country يعتبر الكلاس الأساسي لأي بلد.
في هذا الكلاس قمنا بتعريف دالة إسمها language() فكرتها طباعة لغة البلد و جعلناها تطبع اللغة الإنجليزية بشكل إفتراضي كلغة أي بلد.

بعدها قمنا بتعريف ثلاث كلاسات تمثل ثلاث بلدان مختلفة (أستراليا, لبنان, إسبانيا) لذلك جعلناها ترث من الكلاس Country الذي يعتبر أساس أي بلد.
إذاً الكلاسات الثلاثة سترث الدالة language().

هنا الفكرة التي نريد إيصالها هي أن أي كلاس يرث من Country قد يضطر إلى تعريف الدالة language() من جديد حتى تناسبه.
فمثلاً لبنان لغته هي العربية, و إسبانيا لغتها هي الإسبانية.

بعد إنشاء هذه الكلاسات, قمنا بإنشاء كائنات منها و استدعاء الدالة language() من كل واحد فيهم لطباعة اللغة التي يتكلمها الناس فبه.

مثال عملي يوضح فائدة إعادة تعريف الدوال في C++

main.cpp
#include <iostream>

using namespace std;

// الذي يعتبر الكلاس الأساسي لأي دولة في العالم, إذاً يجب أن يرثه كل كلاس يمثل دولة Country هنا قمنا بتعريف الكلاس
class Country {

	// هنا قمنا بتعريف دالة تقوم بطباعة لغة البلد و جعلناها تطبع اللغة الإنجليزية كاللغة الإفتراضية لأي بلد
    public:
        void language()
        {
            cout << "English \n";
        }

};


// Country و يرث من الكلاس Australia هنا قمنا بتعريف كلاس يمثل دولة أستراليا و إسمه
class Australia : public Country {
	
	// من جديد لأن اللغة الإنجليزية هي لغة أستراليا أصلاً language() هنا لا داعي لتعريف الدالة

};


// Country و يرث من الكلاس Lebanon هنا قمنا بتعريف كلاس يمثل دولة لبنان و إسمه
class Lebanon : public Country {

	// من جديد لأن اللغة الإنجليزية ليست لغة لبنان language() هنا يجب تعريف الدالة
    public:
        void language()
        {
            cout << "Arabic \n";
        }

};


// Country و يرث من الكلاس Spain هنا قمنا بتعريف كلاس يمثل دولة إسبانيا و إسمه
class Spain : public Country {

	// من جديد لأن اللغة الإنجليزية ليست لغة إسبانيا language() هنا يجب تعريف الدالة
    public:
        void language()
        {
            cout << "Spanish \n";
        }

};


// main() هنا قمنا بتعريف الدالة
int main()
{
    // هنا قمنا بإنشاء كائنات من البلدان الثلاثة
    Australia au;
    Lebanon lb;
    Spain sp;

    // لعرض لغة كل بلد language() هنا قمنا باستدعاء الدالة
    au.language();
    lb.language();
    sp.language();

    return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل.

English
Arabic
Spanish 
		


أنت الآن فهمت لما قد تحتاج أن تفعل Override.
سنقوم الآن بشرح طريقة عمل المترجم عندما فعلنا Override للدالة language().

إذا عدت للكلاس Lebanon, ستجد أننا فعلنا Override للدالة language().
إذاً هنا أصبح الكلاس Lebanon فعلياً يملك دالتين إٍسمهما language(), الأولى هي التي ورثها من الكلاس Country و الثانية هي التي قمنا بتعريفها فيه.


بما أن الكلاس Lebanon يملك دالتين لهما نفس الإسم, النوع و عدد البارامتيرات, كيف عرف المترجم أي دالة يختار؟

عندما قمنا بتعريف الدالة language() من جديد في الكلاس Lebanon (أي الكلاس الإبن) قام المترجم بإخفاء الدالة الأصلية التي ورثها من الكلاس Country (أي الكلاس الأب) و أظهر الدالة الجديدة فقط.


ملاحظة

داخل الكلاس Lebanon, لا يزال بإمكانك إستخدام الدالة language() الموجودة في الكلاس Country و التي قام المترجم بإخفائها عندما قمنا بتعريفها من جديد و هذا ما سنتعرف عليه في المثال التالي.

مفاهيم مهمة حول إعادة تعريف الدوال في C++


المفهوم الأول

لاحظنا قبل قليل أنه عند إعادة تعريف الدالة التي ورثها الكلاس الإبن من الكلاس الأب أن المترجم يقوم بإخفاء الدالة التي ورثها الكلاس الإبن و إظهار التي تم تعريفها فيه, و لكننا قلنا بأن الدالة الموجودة في الكلاس الأب لا يزال بإمكان الكلاس الإبن أن يصل إليها حتى و لو كان قد تم إخفاؤها و هذا ما سنتعلمه من هذا المثال.
هنا وضعنا مثال حول كيفية كيفية استدعاء الدالة الموجودة في الكلاس الأب.

في المثال التالي قمنا بتعريف كلاس إسمه Base يحتوي على دالة إسمها printMsg.
بعدها قمنا بتعريف كلاس إسمه Derived يرث من الكلاس Base و قمنا فيه بإعادة تعريف الدالة printMsg و دالة أخرى إسمها printBoth.

في الدالة printBoth() قمنا باستدعاء الدالة printMsg() التي ورثها الكلاس Derived و الدالة printMsg() التي تم إعادة تعريفها فيه.

في الأخير قمنا بإنشاء كائن من الكلاس Derived و من ثم استدعاء كل الدوال التي يملكها.

مثال

main.cpp
      #include <iostream>

      using namespace std;

      // print يحتوي على دالة واحدة إسمها Base هنا قمنا بتعريف كلاس إسمه
      class Base
      {
      public:
      void print() {
      cout << "Base::print() \n";
      }
      };

      // printBoth أيضاً و دالة أخرى إسمها print و فيه قمنا بتعريف دالة إسمها Base يرث من الكلاس Derived هنا قمنا بتعريف الكلاس
      class Derived : public Base
      {
      public:
      void print()
      {
      cout << "Derived::print() \n";
      }

      void printBoth()
      {                    // printBoth() عند استدعاء الدالة
      Base::print();   // Base الموجودة في الكلاس print() سيتم استدعاء الدالة
      print();         // الموجودة في نفس الكلاس print() ثم استدعاء الدالة
      }
      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // d إسمه Derived هنا قمنا بإنشاء كائن من الكلاس
      Derived d;

      // Derived و التي تم تعريفها في الكلاس d من الكائن print() هنا قمنا باستدعاء الدالة
      d.print();

      // Base و التي تم تعريفها في الكلاس d من الكائن print() هنا قمنا باستدعاء الدالة
      d.Base::print();

      // و التي ستستدعي كلتا الدالتين السابقتين Derived التي تم تعريفها في الكلاس d من الكائن print() هنا قمنا باستدعاء الدالة
      d.printBoth();

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

Derived::print()     -->   نتيجة إستدعاء الدالة في السطر 37
Base::print()        -->   نتيجة إستدعاء الدالة في السطر 40
Base::print()        -->   نتيجة إستدعاء الدالة في السطر 43
Derived::print()     -->   نتيجة إستدعاء الدالة في السطر 43


المفهوم الثاني

إذا كنت تريد كتابة الكود بشكل يكون سهل التعامل معه و مراجعته لاحقاً فمن الأفضل إضافة الكلمة virtual لأي دالة تريد من الكلاس الذي يرثها أن يقوم بإعادة تعريفها حتى تتناسب معه, و في الكلاس الذي يرثها من الأفضل أيضاً أن تضيف الكلمة override للدالة التي تعيد تعريفها من جديد في الكلاس الإبن.

إذا كنت تريد كتابة الكود بشكل يكون سهل التعامل معه و مراجعته لاحقاً فمن الأفضل إضافة الكلمة virtual لأي دالة تريد من الكلاس الذي يرثها أن يقوم بإعادة تعريفها حتى تتناسب معه, و في الكلاس الذي يرثها من الأفضل أيضاً أن تضيف الكلمة override للدالة التي تعيد تعريفها من جديد في الكلاس الإبن.

ملاحظة: تم إضافة هاتين الكلمتين إبتداءاً من الإصدار C++ 11 و الإصدارات اللاحقة.


في المثال التالي قمنا بتعريف كلاس إسمه Base يحتوي على دالة إسمها func و نوعها virtual void.
بعدها قمنا بتعريف كلاس إسمه Derived يرث من الكلاس Base و قمنا فيه بإعادة تعريف الدالة func مع إضافة الكلمة override عند تعريفها.

في الأخير قمنا بإنشاء كائن من الكلاس Derived و من ثم استدعاء الدالة func() الموجودة فيه.

مثال

main.cpp
      #include <iostream>

      using namespace std;

      // func يحتوي على دالة واحدة إسمها Base هنا قمنا بتعريف كلاس إسمه
      class Base
      {
      public:
      virtual void func()
      {
      cout << "Base class default behavior \n";
      }
      };

      // أيضاً func و فيه قمنا بتعريف دالة إسمها Base يرث من الكلاس Derived هنا قمنا بتعريف الكلاس
      class Derived : public Base
      {
      public:
      void func() override
      {
      cout << "Derived class overridden behaviour \n";
      }
      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // d إسمه Derived هنا قمنا بإنشاء كائن من الكلاس
      Derived d;

      // Derived و التي تم تعريفها في الكلاس d من الكائن func() هنا قمنا باستدعاء الدالة
      d.func();

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

      Derived class overridden behaviour
    

معلومة تقنية

في المثال السابق إذا قمت بإزالة الكلمتين virtual و override و لن يسبب ذلك أي مشكلة أو إختلاف لأنه كما سبق و قلنا أننا نضعهما فقط بهدف كتابة الكود بأسلوب متعارف عليه.

بالنسبة للمترجم, عند وضع الكلمة virtual فإنك مخير على وضع الكلمة override عند إعادة تعريف الدالة. و لكنه لا يسمح لك بوضع الكلمة override إذا لم تكن الدالة في الأصل نوعها virtual.


بالعودة للدرس ستتعلم المزيد حول فائدة الكلمة virtual التي يمكن استخدامها لتحقيق مبدئ التجريد ( Abstraction ).



المفهوم الثالث

إذا كنت تريد تعريف شكل الدالة ( Prototype ) فقط في الكلاس و إجبار أي كلاس يرثها على أن يقوم بتعريفها فيمكنك إستخدام الكلمة virtual لتحقيق ذلك.
أسلوب كتابة الكود المتبع في هذا المثال يقال له التجريد ( Abstraction ).

مفهوم التجريد في C++

التجريد ( Abstraction ) هو أسلوب مهم جداً يستخدم لتسهيل كتابة الأوامر على المبرمجين, فهو يجعلك قادراً على تنفيذ ما تريد دون الحاجة إلى معرفة كافة التفاصيل التي تم فيها تنفيذ ذلك.

كمثال بسيط, إذا كنت تنوي بناء برنامج يتيح لمستخدميه إرسال إقتراحات حول التطبيق من خلال البريد الإلكتروني, في هذه الحالة لن يكون عليك القلق بتاتاً حول طريقة تعامل أوامر C++ مع البروتوكولات التي تعمل عندما يتم إرسال رسائل إلكترونية من خلال هذا التطبيق لأنك لست مسؤولاً عنها. فعلياً هذه وظيفة الشركة التي تعطيك كود جاهز يمكن استخدامه لإرسال بريد إلكتروني و سيكون عليك فقط تحديد المعلومات الأساسية مثل البريد الإلكتروني للمرسل, البريد الإلكتروني للمرسل إليه, نص الرسالة إلخ..

إذاً بشكل عام, التجريد يجعلك تتعامل مع الأشياء بسطحية بدل أن تغوص في معرفة تفاصيل الكودات المعقدة.
و هذا الأسلوب سيمر معك أيضاً في دروس لاحقة حين تتعلم مبدئ يسمى تعدد الأشكال ( Polymorphism ).

تطبيق مبدأ التجريد في C++

إذا كنت تريد تعريف شكل الدالة ( Prototype ) فقط في الكلاس و إجبار أي كلاس يرثها على أن يقوم بتعريفها فيمكنك اتباع أسلوب التجريد لتحقيق ذلك.

كل ما عليك فعله لإعلام المترجم بأنك تريد إجبار الكلاس الإبن على تعريف الدالة التي يرثها من الكلاس الأب بنفسه هو تعريف الدالة في الكلاس الأب بالأساس كدالة مجرّدة و هنا سيكون عليك جعل نوعها virtual و جعلها تساوي 0 فقط.


مصطلحات تقنية  التجريد في C++

  • الدالة المجرّدة هي أي دالة تطبق أسلوب التجريد و يقال لها Abstract Function أو Pure Virtual Function.

  • الكلاس المجرّد هو أي كلاس يحتوي على دالة مجرّدة أو أكثر و يقال له Abstract Class.



شروط التجريد في C++

عند تطبيق أسلوب التجريد فإنه عليك الإنتباه للنقاط التالية:

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

  • الكلاس الذي يرث دوال مجردة و لا يقوم بإعادة كتابة محتواها أيضاً لا يمكن إنشاء كائنات منه لذات السبب.

  • الدوال الثابتة التي نوعها static لا يمكن تعريفها كدوال مجردة لأنها أصلاً تستخدم من الكلاس الأساسي.

  • أسلوب التجريد يتطلب استخدام الكلمتين virtual و override اللتين تم إضافتهما ابتداءاً من الإصدار C++ 11 لهذا تأكد أنك تستخدم هذا الإصدار أو الإصدارات الأحدث حتى تستطيع تطبيقه.



في المثال التالي قمنا بتعريف كلاس إسمه Base يحتوي على دالة مجردة إسمها func.
بعدها قمنا بتعريف كلاس إسمه Derived يرث من الكلاس Base و قمنا فيه بإعادة تعريف الدالة func.

في الأخير قمنا بإنشاء كائن من الكلاس Derived و من ثم استدعاء الدالة func() الموجودة فيه.

المثال الأول

main.cpp
        #include <iostream>

        using namespace std;

        // func يحتوي على دالة مجردة إسمها Base هنا قمنا بتعريف كلاس إسمه
        class Base
        {
        public:
        virtual void func() = 0;
        };

        // func و فيه قمنا بتعريف الدالة المجردة Base يرث من الكلاس Derived هنا قمنا بتعريف محتوى الكلاس
        class Derived : public Base
        {
        public:
        void func() override
        {
        cout << "Derived class overridden behaviour \n";
        }
        };

        // main() هنا قمنا بتعريف الدالة
        int main()
        {
        // d إسمه Derived هنا قمنا بإنشاء كائن من الكلاس
        Derived d;

        // Derived و التي تم تعريفها في الكلاس d من الكائن func() هنا قمنا باستدعاء الدالة
        d.func();

        return 0;
        }
      

سنحصل على النتيجة التالية عند التشغيل.

        Derived class overridden behaviour
      


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

المثال الثاني

main.cpp
        #include <iostream>

        using namespace std;

        // func يحتوي على دالة مجردة إسمها Base هنا قمنا بتعريف كلاس إسمه
        class Base
        {
        public:
        virtual void func() = 0;
        };

        // func و فيه قمنا بتعريف الدالة المجردة Base يرث من الكلاس Derived هنا قمنا بتعريف محتوى الكلاس
        class Derived : public Base
        {
        public:
        void func() override
        {
        cout << "Derived class overridden behaviour \n";
        }
        };

        // main() هنا قمنا بتعريف الدالة
        int main()
        {
        // b إسمه Base هنا قمنا بإنشاء كائن من الكلاس
        Base b;

        // Base و التي تم تعريفها في الكلاس b من الكائن func() هنا قمنا باستدعاء الدالة
        b.func();

        return 0;
        }
      

سنحصل على النتيجة التالية عند التشغيل.

error: cannot declare variable 'b' to be of abstract type 'Base'
    because the following virtual functions are pure within 'Base':
        'virtual void Base::func()'

لاحظ أن الخطأ فعلياً حدث بسبب السطر 26 حيث أن المترجم قال بأنه لا يمكن إنشاء كائن من الكلاس Base لأنه يحتوي على دالة مجردة ( Pure Virtual Function ) ثم كتب لنا إسم الدالة المجردة أيضاً.



المفهوم الرابع

إذا كنت تريد منع الكلاس الإبن من إعادة تعريف الدالة التي ورثها من الكلاس فيمكنك جعل نوع الدالة virtual final حتى أنه يمكنك جعل نوع الكلاس بأكمله final لا يمكن إعادة تعريف أي دالة موجودة فيه بهدف أن يتم استخدام الدوال الموجودة فيه كما هي.

الكلمة final في C++

إذا كنت تريد منع الكلاس الإبن من إعادة تعريف الدالة التي ورثها من الكلاس فيمكنك جعل نوع الدالة virtual final حتى أنه يمكنك جعل نوع الكلاس بأكمله final لا يمكن إعادة تعريف أي دالة موجودة فيه بهدف أن يتم استخدام الدوال الموجودة فيه كما هي.

ملاحظة: تم إضافة هذه الكلمة إبتداءاً من الإصدار C++ 11 و الإصدارات اللاحقة.


مصطلحات تقنية

  • الدالة التي لا يمكن إعادة تعريفها يقال لها في العادة دالة ثابتة.

  • الكلاس الذي لا يمكن إعادة تعريف أي شيء موجود فيه يقال له كلاس ثابت.



الكلاس الثابت لا يسمح لك بالوراثة منه لأن الوراثة بالأساس فكرتها أنك ترث الشيء بهدف تطويره أو الزيادة عليه.
بمعنى آخر يمكنك إنشاء كائنات منه فقط و استخدام ما هو موجود فيه.

الكلمة final يمكن استخدامها كإسم لمتغير, دالة, مصفوفة, كائن إلخ.. لأنها لا تعتبر من الكلمات المحجوزة ( Keywords ) في اللغة.

أمثلة شاملة على الكلمة final في C++


في المثال التالي قمنا بتعريف كلاس إسمه Base يحتوي على دالة ثابتة إسمها func().
بعدها قمنا بتعريف كلاس إسمه Derived يرث من الكلاس Base و حاولنا فيه إعادة تعريف الدالة func() لتنبيهك من الخطأ الذي قد يظهر في حال حاولت إعادة تعريف دالة ثابتة في الكلاس الذي يرثها.

المثال الأول

main.cpp
      #include <iostream>

      using namespace std;

      // func يحتوي على دالة ثابتة إسمها Base هنا قمنا بتعريف كلاس إسمه
      class Base
      {
      public:
      virtual void func() final
      {
      cout << "Base class default behaviour \n";
      }
      };

      // func و فيه قمنا بإعادة تعريف الدالة الثابتة Base يرث من الكلاس Derived هنا قمنا بتعريف محتوى الكلاس
      class Derived : public Base
      {
      public:
      void func() override     // هذا السطر الذي سيسبب المشكلة عند التشغيل
      {
      cout << "Derived class overridden behaviour \n";
      }
      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      return 0;
      }
    

سيظهر الخطأ التالي عند التشغيل.

error: virtual function 'virtual void Derived::func()' overriding final function
    note: overridden function is 'virtual void Base::func()'
        In member function 'virtual void Derived::func()'

في المثال السابق حدث الخطأ فعلياً بسبب السطر 19 حيث أن المترجم قال بأنه لا يمكن إعادة تعريف الدالة func() في الكلاس Derived الذي ورثها لأنها في الكلاس الأب Base معرّفة كدالة ثابتة.



في المثال التالي قمنا بتعريف كلاس ثابت إسمه Base يحتوي على دالة إسمها func().
بعدها قمنا بتعريف كلاس إسمه Derived يرث من الكلاس Base فقط لتنبيهك من الخطأ الذي قد يظهر في حال حاولت الوراثة من كلاس ثابت.

المثال الثاني

main.cpp
      #include <iostream>

      using namespace std;

      // func يحتوي على دالة إسمها Base هنا قمنا بتعريف كلاس ثابت إسمه
      class Base final
      {
      public:
      void func()
      {
      cout << "Base class default behaviour \n";
      }
      };

      // و طبعاً هذا الأمر سيسبب مشكلة عند التشغيل Base يرث من الكلاس Derived هنا قمنا بتعريف محتوى الكلاس
      class Derived : public Base
      {

      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      return 0;
      }
    

سيظهر الخطأ التالي عند التشغيل.

error: cannot derive from 'final' base 'Base' in derived type 'Derived'

في المثال السابق حدث الخطأ فعلياً بسبب السطر 16 حيث أن المترجم قال بأنه لا يمكن الوراثة من الكلاس Base لأنه كلاس ثابت.



في المثال التالي قمنا بتعريف كلاس ثابت إسمه Base يحتوي على دالة إسمها func().
بعدها قمنا بإنشاء كائن منه و استدعاء الدالة الموجودة فيه.

المثال الثالث

main.cpp
      #include <iostream>

      using namespace std;

      // func يحتوي على دالة إسمها Base هنا قمنا بتعريف كلاس ثابت إسمه
      class Base final
      {
      public:
      void func()
      {
      cout << "Base class default behaviour \n";
      }
      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // b إسمه Base هنا قمنا بإنشاء كائن من الكلاس
      Base b;

      // b من الكائن func() هنا قمنا باستدعاء الدالة
      b.func();

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

      Base class default behaviour