رفتن به محتوا

وراثت و روابط «هست-یک»

وراثت عمومی، زنجیره‌سازی سازنده‌ها، مشکل برش، وراثت چندگانه و مشکل الماس را بیاموزید — و بدانید چه زمانی وراثت مناسب است.

وراثت مدل «هست-یک» را پیاده‌سازی می‌کند

وراثت عمومی یک رابطه «هست-یک» (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)

برش شیء (Object Slicing) زمانی رخ می‌دهد که یک شیء مشتق‌شده با مقدار به یک متغیر کلاس پایه تخصیص یا ارسال شود. بخش مشتق‌شده «بریده می‌شود» — فقط بخش پایه باقی می‌ماند. این یک باگ ظریف و رایج است.

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 Problem) زمانی رخ می‌دهد که دو پایه یک جد مشترک دارند و باعث می‌شوند کلاس مشتق‌شده دو کپی از داده‌های جد را داشته باشد. وراثت مجازی (Virtual Inheritance) این مشکل را حل می‌کند.

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;
}

انواع بازگشتی هم‌تغییر و دسترسی Protected

انواع بازگشتی هم‌تغییر (Covariant Return Types) به کلاس مشتق‌شده اجازه می‌دهند یک تابع مجازی را بازنویسی کند و نوع اشاره‌گر یا ارجاع اختصاصی‌تر (مشتق‌شده) را برگرداند. برای مثال، اگر Base::clone() نوع Base* برگرداند، Derived::clone() می‌تواند Derived* برگرداند.

تعیین‌کننده دسترسی protected بین public و private قرار دارد: اعضای protected برای خود کلاس و کلاس‌های مشتق‌شده قابل دسترسی هستند، اما برای کد خارجی نه. از protected برای اعضایی که کلاس‌های مشتق‌شده واقعاً به آن‌ها نیاز دارند استفاده کنید — اما محتاط باشید، زیرا یک وابستگی بین پایه و مشتق‌شده ایجاد می‌کند.

دام

یک اشتباه رایج استفاده از وراثت صرفاً برای استفاده مجدد از کد است. اگر یک Stack از std::vector ارث‌بری کند، کاربران می‌توانند push_back، insert و سایر عملیات بردار را فراخوانی کنند که انتزاع پشته را نقض می‌کند. وراثت باید یک رابطه واقعی «هست-یک» را مدل کند که اصل جایگزینی لیسکوف را برآورده کند. برای استفاده مجدد از کد، ترکیب (Composition) را ترجیح دهید (یک Stack که یک vector را *شامل* می‌شود).

بهترین شیوه

همیشه از وراثت public استفاده کنید — وراثت private/protected به‌ندرت نیاز است و گیج‌کننده است. همیشه مخرب کلاس پایه را virtual اعلان کنید. اشیاء چندریختی را با ارجاع یا اشاره‌گر ارسال کنید، هرگز با مقدار (برای جلوگیری از برش). ترکیب را بر وراثت ترجیح دهید مگر اینکه رابطه واقعاً «هست-یک» باشد. از سلسله‌مراتب وراثت عمیق اجتناب کنید (۲ تا ۳ سطح معمولاً حد عملی است).

نکات کلیدی
  • وراثت عمومی مدل «هست-یک» است — کلاس مشتق‌شده باید اصل جایگزینی لیسکوف را برآورده کند
  • سازنده‌های پایه ابتدا اجرا می‌شوند، مخرب‌ها آخر (ترتیب معکوس ساخت)
  • برش شیء زمانی رخ می‌دهد که یک شیء مشتق‌شده با مقدار به یک پایه ارسال شود — همیشه از ارجاعات یا اشاره‌گرها استفاده کنید
  • مشکل الماس با وراثت virtual حل می‌شود، اما ترکیب را ترجیح دهید
  • وراثت برای جایگزین‌پذیری است، نه استفاده مجدد از کد — برای دومی از ترکیب استفاده کنید

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. برش شیء (Object Slicing) چیست؟

2. وراثت مجازی چگونه مشکل الماس را حل می‌کند؟

3. چرا نباید برای ایجاد کلاس `Stack` از `std::vector` ارث‌بری کنید؟