توضح هذه المقالة الوراثة على ثلاثة مستويات: المبتدئين والمتوسطين والمتقدمين. خبير لا. وليس كلمة واحدة عن الصلبة. بصراحة.
مبتدئ
ما هو الميراث؟
الوراثة هي واحدة من المبادئ الأساسية ل OOP. وفقًا لذلك ، يمكن للفصل استخدام المتغيرات والأساليب لفئة أخرى خاصة بها.
تسمى الفئة التي ترث البيانات فئة فرعية أو فئة مشتقة أو فئة فرعية. يُطلق على الفئة التي تُورث البيانات أو الأساليب منها فئة فائقة أو فئة أساسية أو فئة رئيسية. تعد مصطلحي "الوالد" و "الطفل" مفيدة للغاية لفهم الميراث. عندما يتلقى الطفل خصائص والديه ، يتلقى الفصل المشتق طرق ومتغيرات الفئة الأساسية.
الميراث مفيد لأنه يسمح لك ببناء وإعادة استخدام الكود ، والذي بدوره يمكن تسريع عملية التنمية بشكل ملحوظ. على الرغم من ذلك ، يجب استخدام الميراث بحذر ، لأن معظم التغييرات في الطبقة الفائقة ستؤثر على جميع الفئات الفرعية ، مما قد يؤدي إلى عواقب غير متوقعة.
في هذا المثال ، serial_number
يتم تعريف أو تعريف طريقة turn_on()
في فئة Computer
الفرعية. ومع ذلك ، يمكن استخدامها لأنها موروثة من الفئة الأساسية.
ملاحظة مهمة : لا يمكن توريث المتغيرات والأساليب الخاصة.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; }
أنواع الوراثة
هناك عدة أنواع من الميراث في C ++:
- البيانات العامة - العامة (
public
) والمحمية ( protected
) موروثة دون تغيير مستوى الوصول إليها ؛ - محمي (
protected
) - تصبح جميع البيانات الموروثة محمية ؛ - خاص - تصبح جميع البيانات الموروثة خاصة.
بالنسبة للفئة الأساسية Device
، لا يتغير مستوى الوصول إلى البيانات ، ولكن نظرًا لأن الفئة المشتقة من Computer
ترث البيانات على أنها خاصة ، تصبح البيانات خاصة لفئة Computer
.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; }
تستخدم فئة Computer
الآن طريقة turn_on()
مثل أي طريقة خاصة: يمكن استدعاء turn_on()
من داخل الفصل ، ولكن محاولة الاتصال بها مباشرة من main
ستؤدي إلى حدوث خطأ في وقت الترجمة. بالنسبة Device
الفئة الأساسية ، turn_on()
طريقة turn_on()
عامة ، ويمكن استدعاءها من main
.
البنائين والمدمرات
في C ++ ، لا يتم توريث المنشئات والمدمرات. ومع ذلك ، يتم استدعاؤها عند تهيئة الفئة التابعة لكائنها. تسمى المنشئات بالتسلسل الهرمي واحدًا تلو الآخر ، بدءًا من الفئة الأساسية وتنتهي مع آخر فئة مشتقة. تسمى المدمرات بترتيب عكسي.
ملاحظة مهمة: لا تغطي هذه المقالة المدمرات الافتراضية. يمكن العثور على مواد إضافية حول هذا الموضوع ، على سبيل المثال ، في هذه المقالة على Habr .
#include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; }
المنشئات: Device
-> Computer
-> Laptop
.
المدمرات: Laptop
-> Computer
-> Device
.
وراثة متعددة
يحدث التوريث المتعدد عندما يكون للفئة الفرعية فئتان أو أكثر. في هذا المثال ، ترث فئة Laptop
كل من جهاز العرض Computer
في نفس الوقت.
#include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; }
قضايا الوراثة المتعددة
يتطلب الميراث المتعدد تصميمًا دقيقًا ، حيث يمكن أن يؤدي إلى عواقب غير متوقعة. معظم هذه العواقب ناتجة عن غموض في الميراث. في هذا المثال ، يرث Laptop
الأسلوب turn_on()
من كلا الوالدين وليس من الواضح الطريقة التي يجب استدعاءها.
#include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; }
على الرغم من حقيقة أن البيانات الخاصة ليست موروثة ، فمن المستحيل حل الميراث الغامض عن طريق تغيير مستوى الوصول إلى البيانات إلى القطاع الخاص. عند التجميع ، يتم أولاً البحث عن طريقة أو متغير ، وبعد ذلك ، يتحقق مستوى الوصول إليها.
مشكلة المعين

مشكلة الماس هي مشكلة كلاسيكية في اللغات التي تدعم الوراثة المتعددة. هذه المشكلة تحدث عندما ترث الفئات B
و C
A
و ترث الفئة D
B
و C
على سبيل المثال ، تحدد الفئات A
و B
و C
طريقة print_letter()
. إذا تم print_letter()
من قبل الفئة D
، فليس من الواضح الطريقة التي يجب أن تسمى - طريقة من الفئة A
أو B
أو C
لغات مختلفة لها طرق مختلفة لحل المشاكل على شكل الماس. في C ++ ، حل المشكلة متروك للمبرمج.
مشكلة الماس على شكل مشكلة تصميم في المقام الأول ، وينبغي توفيرها في مرحلة التصميم. في مرحلة التطوير ، يمكن حلها على النحو التالي:
- استدعاء طريقة فئة فائقة معينة
- الرجوع إلى كائن الفئة الفرعية ككائن من فئة فائقة معينة
- تجاوز الطريقة الإشكالية في آخر فئة
turn_on()
في التعليمات البرمجية ، turn_on()
في فئة Laptop
الفرعية).
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; }
إذا لم يتم تجاوز طريقة turn_on()
في الكمبيوتر المحمول ، Laptop_instance.turn_on()
استدعاء Laptop_instance.turn_on()
إلى حدوث خطأ في Laptop_instance.turn_on()
. يمكن لكائن Laptop
الوصول إلى turn_on()
طريقة turn_on()
وقت واحد: Device:Computer:Laptop.turn_on()
Device:Monitor:Laptop.turn_on()
.
مشكلة الماس: البنائين والمدمرات
نظرًا لأنه في C ++ ، عند تهيئة كائن الفئة الفرعية ، يتم استدعاء منشئي جميع الفئات الأصل ، وتحدث مشكلة أخرى: سيتم استدعاء مُنشئ الجهاز الأساسي فئة مرتين.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; }
الميراث الافتراضي
الوراثة الافتراضية تمنع كائنات فئة أساسية متعددة من الظهور في التسلسل الهرمي للميراث. وبالتالي ، سيتم استدعاء مُنشئ الجهاز الأساسي فئة مرة واحدة فقط ، ولن يؤدي الوصول إلى طريقة turn_on()
دون turn_on()
في turn_on()
إلى حدوث turn_on()
في turn_on()
.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; }
ملاحظة : لا يسمح الميراث الظاهري في فصول Computer
والشاشة بالوراثة المعينية إذا لم يرث Laptop
فئة الأطفال فئة Device
تقريبًا ( class Laptop: public Computer, public Monitor, public Device {};
).
فئة مجردة
في C ++ ، تعتبر الفئة التي توجد فيها طريقة افتراضية واحدة على الأقل مجردة. إذا لم يتم تجاوز الطريقة الافتراضية في الفئة الفرعية ، فلن يتم تجميع الشفرة. أيضًا ، في C ++ يستحيل إنشاء كائن لفئة مجردة - ستؤدي المحاولة أيضًا إلى حدوث خطأ أثناء التحويل البرمجي.
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; }
السطح البيني
C ++ ، على عكس بعض لغات OOP ، لا يوفر كلمة أساسية منفصلة للدلالة على واجهة. ومع ذلك ، فإن تنفيذ الواجهة ممكن عن طريق إنشاء فئة تجريدية خالصة - فئة لا يوجد فيها سوى إعلانات أسلوب. وغالبًا ما يشار إلى هذه الفئات باسم فئة الأساس التجريدي (ABC).
#include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; }
متقدم
على الرغم من أن الميراث هو مبدأ أساسي في OOP ، يجب استخدامه بحذر. من المهم التفكير في أنه من المحتمل تغيير أي رمز سيتم استخدامه ويمكن استخدامه بطريقة غير واضحة للمطور.
الوراثة من فئة تنفيذها أو تنفيذها جزئيا
إذا لم يكن الميراث يأتي من واجهة (فئة تجريدية خالصة في سياق C ++) ، ولكن من فئة يوجد فيها أي تطبيقات ، يجدر النظر في أن الوريث متصل بالفصل الأصل بأقرب اتصال ممكن. يمكن أن تؤثر معظم التغييرات في الفصل الأصل على الوريث ، مما قد يؤدي إلى سلوك غير متوقع. مثل هذه التغييرات في سلوك الوريث ليست واضحة دائمًا - يمكن أن يحدث خطأ في قانون العمل الذي تم اختباره بالفعل. يتفاقم هذا الموقف بسبب وجود تسلسل هرمي طبقي معقد. تجدر الإشارة دائمًا إلى أنه يمكن تغيير الرمز ليس فقط من قِبل الشخص الذي كتبه ، وقد لا يؤخذ زملائه في الاعتبار مسارات الميراث الواضحة للمؤلف.
في المقابل ، تجدر الإشارة إلى أن الميراث من الطبقات المنفذة جزئيًا له ميزة لا يمكن إنكارها. غالبًا ما تعمل المكتبات والأُطر على النحو التالي: فهي توفر للمستخدم فئة تجريدية مع العديد من الطرق الافتراضية والعديد من الطرق المنفذة. وبالتالي ، فإن أكبر قدر من العمل قد تم بالفعل - لقد تم بالفعل كتابة المنطق المعقد ، ويمكن للمستخدم فقط تخصيص الحل الجاهز ليناسب احتياجاته.
السطح البيني
تقدم الوراثة من واجهة (فئة تجريدية خالصة) الوراثة كفرصة لهيكلة الكود وحماية المستخدم. نظرًا لأن الواجهة تصف العمل الذي ستعمله فئة التطبيق ، ولكنها لا تصف كيف ، فإن أي مستخدم للواجهة محمي من التغييرات في الفصل التي تنفذ هذه الواجهة.
واجهة: مثال الاستخدام
بادئ ذي بدء ، تجدر الإشارة إلى أن المثال يرتبط ارتباطًا وثيقًا بمفهوم تعدد الأشكال ، ولكن سيتم النظر فيه في سياق الميراث من فئة مجردة خالصة.
يجب تكوين تطبيق يقوم بتشغيل منطق الأعمال المجردة من ملف تكوين منفصل. في مرحلة مبكرة من التطوير ، لم يتم تنسيق تنسيق ملف التكوين هذا بشكل كامل. تمرير تحليل الملفات خلف واجهة يوفر العديد من المزايا.
عدم الوضوح فيما يتعلق بتنسيق ملف التكوين لا يؤدي إلى إبطاء عملية تطوير البرنامج الرئيسي. يمكن لمطورين اثنين العمل بشكل متوازٍ - أحدهما على منطق الأعمال والآخر على المحلل اللغوي. نظرًا لأنهم يتفاعلون من خلال هذه الواجهة ، يمكن لكل منهم العمل بشكل مستقل. يسهل هذا النهج ترميز اختبارات الوحدة بالشفرة ، حيث يمكن كتابة الاختبارات اللازمة باستخدام وهمي لهذه الواجهة.
أيضًا ، عند تغيير تنسيق ملف التكوين ، لا يتأثر منطق العمل الخاص بالتطبيق. الشيء الوحيد الذي يتطلب الانتقال الكامل من تنسيق إلى آخر هو كتابة تطبيق جديد للفئة المجردة الموجودة بالفعل (فئة التحليل اللغوي). علاوة على ذلك ، تتطلب العودة إلى تنسيق الملف الأصلي الحد الأدنى من العمل - استبدال محلل موجود بأخرى.
استنتاج
يوفر الوراثة العديد من الفوائد ، ولكن يجب أن يتم تصميمها بعناية لتجنب المشكلات التي تمثلها. في سياق الميراث ، يوفر C ++ مجموعة واسعة من الأدوات التي تفتح الكثير من الاحتمالات للمبرمج.
و SOLID هو جيد.