前言

继承是面向对象程序设计中的核心概念之一,它让代码复用上升到了类设计的层次。C++ 支持多种继承方式,功能强大,但同时也引入了一些复杂性(比如菱形继承)。本文将从继承的基本概念讲起,逐步深入到默认成员函数、作用域、友元、静态成员,最后详细剖析多继承与菱形继承问题,并给出继承与组合的选择建议。

📌 本文基于 C++11 标准,所有代码均已测试验证。


1. 继承的概念及定义

1.1 什么是继承?

继承允许我们在已有类(基类/父类)的基础上创建新类(派生类/子类),派生类会复用基类的成员(成员变量和成员函数),并可以添加自己的新成员。

举例: 学生和老师都有姓名、年龄、电话等公共属性,可以抽取出一个 Person 类,然后 Student 和 Teacher 分别继承它。

class Person {
public:
    void identity() { cout << "身份认证" << endl; }
protected:
    string _name = "张三";
    int _age = 18;
};

class Student : public Person {
public:
    void study() { cout << "学习" << endl; }
protected:
    int _stuId; // 学号
};

class Teacher : public Person {
public:
    void teach() { cout << "授课" << endl; }
protected:
    string _title; // 职称
};

1.2 继承的访问控制

继承方式有三种:publicprotectedprivate。它们决定了基类成员在派生类中的访问权限。

基类成员 public继承 protected继承 private继承
public public protected private
protected protected protected private
private 不可见 不可见 不可见

关键点:

  • 基类的 private 成员在派生类中不可见(但实际已被继承,只是无法访问)。

  • 若希望基类成员在派生类中可访问、对外不可见,应定义为 protected

  • 默认继承方式:class 为 privatestruct 为 public建议显式写出继承方式

✅ 实际开发中几乎只使用 public 继承。


2. 基类与派生类的转换(切片)

  • 派生类对象可以赋值给基类的指针或引用(称为“切片”)。

  • 基类对象不能赋值给派生类对象。

  • 基类指针指向派生类对象时,可通过强制转换转为派生类指针(需确保类型安全)。

Student s;
Person* p = &s;   // 切片
Person& r = s;
Person pobj = s;  // 拷贝构造
// s = pobj;     // 错误

3. 继承中的作用域与隐藏

基类和派生类拥有各自独立的作用域。如果派生类定义了与基类同名的成员(变量或函数),则会隐藏基类的同名成员。

class Person {
public:
    void fun() { cout << "Person::fun()" << endl; }
    int _num = 111;
};

class Student : public Person {
public:
    void fun(int x) { cout << "Student::fun(int)" << endl; }
    int _num = 999;
};

// 调用时:
Student s;
s.fun(10);      // 派生类版本
// s.fun();     // 错误:被隐藏
s.Person::fun(); // 显式调用基类版本

⚠️ 函数名相同即构成隐藏(与参数无关),建议避免定义同名成员。


4. 派生类的默认成员函数

派生类的默认构造函数、拷贝构造、赋值运算符重载、析构函数有特殊规则:

函数 规则
构造 必须调用基类构造初始化基类部分。若基类无默认构造,则必须显式调用。
拷贝构造 必须调用基类拷贝构造。
赋值重载 必须调用基类赋值运算符(注意隐藏,需指定作用域)。
析构 自动调用基类析构,顺序与构造相反。
Student(const char* name, int num)
    : Person(name)   // 显式调用基类构造
    , _num(num) {}

Student& operator=(const Student& s) {
    if (this != &s) {
        Person::operator=(s); // 显式调用基类赋值
        _num = s._num;
    }
    return *this;
}

如何定义一个不能被继承的类?

  • C++98:将构造函数设为 private

  • C++11:使用 final 关键字。

class Base final { };

5. 继承与友元、静态成员

  • 友元不能继承:基类的友元无法访问派生类的私有成员。

  • 静态成员共享:基类中定义的 static 成员在整个继承体系中只有一份,所有派生类共享。

class Person {
public:
    static int _count;
};
int Person::_count = 0;

class Student : public Person { };
// Person::_count 和 Student::_count 地址相同

6. 多继承与菱形继承问题

6.1 单继承 vs 多继承

  • 单继承:一个派生类只有一个直接基类。

  • 多继承:一个派生类有多个直接基类。

6.2 菱形继承(致命问题)

当多个类共同继承自同一个基类,且派生类又同时继承这些类时,会形成菱形继承,导致数据冗余二义性

class Person { public: string _name; };
class Student : public Person { };
class Teacher : public Person { };
class Assistant : public Student, public Teacher { };

Assistant a;
a._name = "peter"; // 错误:不明确
a.Student::_name = "xxx"; // 必须显式指定路径

6.3 虚继承解决菱形问题

使用 virtual 继承,让 Student 和 Teacher 共享同一个 Person 基类子对象。

class Student : virtual public Person { };
class Teacher : virtual public Person { };
class Assistant : public Student, public Teacher { };

Assistant a;
a._name = "peter"; // 正确,唯一

🧠 虚继承底层实现复杂(通过虚基表),会增加开销。实际开发中应尽量避免设计菱形继承


7. 继承与组合:如何选择?

关系 说明 复用方式 耦合度
is-a(继承) 派生类是基类的一种 白箱复用,内部可见
has-a(组合) 类中包含另一个类的对象 黑箱复用,仅通过接口

推荐原则:

  • 优先使用组合,因为它更灵活、维护性更好。

  • 只有在明确的 is-a 关系(如 Car 和 Benz)或需要多态时才使用继承。

  • 如果既适合继承又适合组合,优先选组合。

// 组合示例:Car has-a Tire
class Car {
    Tire _t1, _t2, _t3, _t4;
};

// 继承示例:Benz is-a Car
class Benz : public Car { };

总结

知识点 关键结论
继承方式 几乎只用 public 继承
隐藏规则 同名成员(包括函数)会隐藏
默认成员函数 必须显式调用基类对应函数
友元/静态 友元不继承,静态成员共享
菱形继承 用虚继承解决,但最好避免
继承 vs 组合 优先组合,除非 is-a 或需要多态

C++ 的继承机制功能强大,但多继承和菱形继承也带来了复杂性。理解其底层原理和设计哲学,才能写出清晰、可维护的代码。

💬 你遇到过菱形继承的实际场景吗?你是如何解决的?欢迎留言讨论!

更多推荐