انتقل إلى المحتوى

الميراث وعلاقات Is-A

تعرف على الميراث العام، وتسلسل البناء، ومشكلة التقطيع، والميراث المتعدد، ومشكلة الماس - ومتى يكون الميراث مناسبًا.

نماذج الميراث هو-أ

الميراث العام يمثل العلاقة "is-a":DogهوAnimal، وSavingsAccountهوBankAccount. ويخضع هذا لمبدأ استبدال ليسكوف (LSP): في أي مكان يُتوقع فيه وجود فئة أساسية، يجب أن تعمل الفئة المشتقة بشكل صحيح دون مفاجآت.

يعد الميراث أحد أقوى ميزات OOP وأكثرها سوءًا. ليست كل علاقة "هي-أ".StackليسVectorعلى الرغم من أنه يستخدم واحدًا داخليًا. يؤدي سوء استخدام الميراث حيث يكون التكوين مناسبًا إلى كود هش يصعب الحفاظ عليه.

الميراث الأساسي وتسلسل البناء

ترث الفئة المشتقة جميع أعضاء الفئة الأساسية. يقوم مُنشئ الفئة الأساسية بتشغيل أولاً، ثم مُنشئ الفئة المشتقة. يحدث التدمير بترتيب عكسي.

inheritance_basic.cpp
#include <iostream>
#include <string>

class Animal {
    std::string name_;
    int age_;

public:
    Animal(const std::string& name, int age)
        : name_{name}, age_{age}
    {
        std::cout << "Animal constructed: " << name_ << '\n';
    }

    virtual ~Animal() {
        std::cout << "Animal destroyed: " << name_ << '\n';
    }

    const std::string& name() const { return name_; }
    int age() const { return age_; }

    virtual void speak() const {
        std::cout << name_ << " makes a sound\n";
    }
};

class Dog : public Animal {  // public inheritance = is-a
    std::string breed_;

public:
    Dog(const std::string& name, int age, const std::string& breed)
        : Animal{name, age}  // base class constructor MUST be called first
        , breed_{breed}
    {
        std::cout << "Dog constructed: " << name() << '\n';
    }

    ~Dog() override {
        std::cout << "Dog destroyed: " << name() << '\n';
    }

    void speak() const override {
        std::cout << name() << " barks! (" << breed_ << ")\n";
    }
};

int main() {
    Dog rex{"Rex", 5, "German Shepherd"};
    rex.speak();  // Rex barks! (German Shepherd)
    // Destruction order: Dog destroyed, then Animal destroyed
    return 0;
}

مشكلة التقطيع

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

slicing.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>

class Shape {
public:
    virtual ~Shape() = default;
    virtual std::string type() const { return "Shape"; }
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_{r} {}
    std::string type() const override { return "Circle"; }
};

// BUG: passing by value slices the object!
void print_type_bad(Shape s) {
    std::cout << s.type() << '\n';  // always prints "Shape"!
}

// CORRECT: pass by reference to preserve polymorphism
void print_type_good(const Shape& s) {
    std::cout << s.type() << '\n';  // prints actual type
}

int main() {
    Circle c{5.0};

    print_type_bad(c);   // "Shape"  — SLICED!
    print_type_good(c);  // "Circle" — correct

    // Slicing also happens with containers of values:
    // std::vector<Shape> shapes;  // BAD: slicing on insert
    // Use pointers instead:
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(3.0));
    std::cout << shapes[0]->type() << '\n';  // "Circle"

    return 0;
}

الميراث المتعدد ومشكلة الماس

يسمح C++ للفصل بالوراثة من قواعد متعددة. تحدث مشكلة الماس عندما تشترك قاعدتان في سلف مشترك، مما يتسبب في احتواء الفئة المشتقة على نسختين من بيانات السلف. الميراث الافتراضي يحل هذه المشكلة.

diamond.cpp
#include <iostream>

// Diamond problem:
//       Device
//      /      \
//  Printer  Scanner
//      \      /
//     AllInOne

class Device {
protected:
    int id_;
public:
    Device(int id) : id_{id} {
        std::cout << "Device(" << id_ << ")\n";
    }
    virtual ~Device() = default;
};

// Virtual inheritance: only ONE copy of Device in AllInOne
class Printer : virtual public Device {
public:
    Printer(int id) : Device{id} {
        std::cout << "Printer\n";
    }
    void print_doc() { std::cout << "Printing on device " << id_ << '\n'; }
};

class Scanner : virtual public Device {
public:
    Scanner(int id) : Device{id} {
        std::cout << "Scanner\n";
    }
    void scan_doc() { std::cout << "Scanning on device " << id_ << '\n'; }
};

class AllInOne : public Printer, public Scanner {
public:
    // With virtual inheritance, the most-derived class
    // must initialize the virtual base directly
    AllInOne(int id) : Device{id}, Printer{id}, Scanner{id} {
        std::cout << "AllInOne\n";
    }
};

int main() {
    AllInOne aio{42};
    aio.print_doc();  // Printing on device 42
    aio.scan_doc();   // Scanning on device 42
    // Only ONE Device sub-object exists — no ambiguity
    return 0;
}

أنواع الإرجاع المتغيرة والوصول المحمي

أنواع الإرجاع المتغيرة تسمح للفئة المشتقة بتجاوز دالة افتراضية وإرجاع مؤشر (مشتق) أكثر تحديدًا أو نوع مرجعي. على سبيل المثال، إذا قامBase::clone()بإرجاعBase*، فيمكن لـDerived::clone()إرجاعDerived*.

يقع محدد الوصول protected بينpublicوprivate: يمكن الوصول إلى الأعضاء المحميين من خلال الفئة نفسها والفئات المشتقة منها، ولكن ليس من خلال التعليمات البرمجية الخارجية. استخدمprotectedللأعضاء الذين تحتاجهم الفئات المشتقة بشكل شرعي - ولكن كن حذرًا، لأنه يخلق اقترانًا بين القاعدة والمشتقة.

خطأ شائع

من الأخطاء الشائعة استخدام الميراث فقط لإعادة استخدام التعليمات البرمجية. إذا ورثStackمنstd::vector، فيمكن للمستخدمين استدعاءpush_backوinsertوعمليات المتجهات الأخرى التي تنتهك تجريد المكدس. يجب أن يكون الميراث بمثابة نموذج حقيقي لعلاقة is-a التي تلبي مبدأ استبدال ليسكوف. لإعادة استخدام الكود، تفضل التكوين (Stackالذي *يحتوي* علىvector).

أفضل ممارسة

استخدم دائمًا الميراث public — نادرًا ما يكون الميراث الخاص/المحمي ضروريًا ومربكًا. قم دائمًا بتعريف أدوات تدمير الفئة الأساسية باسم virtual. قم بتمرير الكائنات متعددة الأشكال حسب المرجع أو المؤشر، وليس حسب القيمة أبدًا (لتجنب التقطيع). أفضّل التكوين على الميراث إلا إذا كانت العلاقة "is-a" حقًا. تجنب التسلسلات الهرمية العميقة للميراث (عادةً ما يكون المستوى 2-3 هو الحد العملي).

النقاط الرئيسية
  • نماذج الميراث العامة is-a — يجب أن تستوفي الفئة المشتقة مبدأ استبدال ليسكوف
  • يتم تشغيل المنشئين الأساسيين أولاً، ويتم تشغيل المدمرين أخيرًا (ترتيب عكسي للبناء)
  • Object slicing occurs when a derived object is passed by value to a base — always use references or pointers
  • The diamond problem is solved withvirtualinheritance, but prefer composition instead
  • Inheritance is for substitutability, not code reuse — use composition for the latter

اختبار — اختبر معرفتك

(15 نقطة خبرة)

1. What is object slicing?

2. كيف يحل الميراث الافتراضي مشكلة الماس؟

3. لماذا لا يجب أن ترث من`std::vector`لإنشاء فئة`Stack`؟