跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 继承机制详解:从基础概念到虚拟继承

C++ 继承是类设计层次的复用机制。详细阐述了继承关系中的访问控制规则、派生类默认成员函数的调用顺序(构造先基后子,析构先子后基)、以及同名成员的隐藏现象。重点分析了多继承带来的菱形继承问题,包括数据冗余和二义性,并解释了虚拟继承如何通过共享基类实例解决上述难题。最后对比了继承与组合的区别,强调实际开发中优先使用组合以降低耦合度。

观心发布于 2026/2/23更新于 2026/10/397 浏览
C++ 继承机制详解:从基础概念到虚拟继承

一、概念

继承是类设计层次的复用。在 C++ 中,基类(父类)与派生类(子类)构成了这种层次关系。

语法示例:

class Person { /* ... */ };
class Student : public Person { /* ... */ };

继承关系决定了访问权限的传递方式。基类的私有成员虽然被继承到了派生类对象中,但在派生类内部和外部都无法直接访问。如果希望基类成员在派生类中可访问,但对外不可见,应将其定义为 protected。

访问规则遵循以下逻辑:基类其他成员在子类的访问方式取决于 Min(成员在基类的访问限定符,继承方式),即 public > protected > private。

二、基类和派生类对象赋值转换

不同类型对象赋值需要类型转换。例如 int 转 double 是隐式的,但类之间则不同。

父类对象不能赋值给子类对象,即使强转也不行。因为父类对象缺少子类特有的成员变量,强行赋值会导致数据丢失或随机值。

父类对象可以赋值给子类的指针或引用。这是多态的基础。

子类对象可以赋值给父类的对象、指针、引用。 当子类对象赋值给父类对象时,发生的是'切片'操作(Slicing),只拷贝了父类那部分成员,没有发生真正的类型转换。

三、继承中的作用域

作用域查找发生在编译时。子类和父类拥有独立的类域。如果子类定义了与父类同名的成员,会隐藏父类的同名成员,这被称为重定义。

在子类成员函数中,若需访问被隐藏的父类成员,可以使用作用域解析运算符 :: 显式指定。

注意: 尽量不在继承体系中定义同名成员,以免混淆。如果是成员函数的隐藏,只要函数名相同即构成隐藏,参数列表是否一致不影响隐藏的发生(这与重载不同,重载要求在同一作用域且参数不同)。

class Person {
public:
    void fun() { cout << "Person::func()" << endl; }
protected:
    string _name = "小李子";
};

class Student : public Person {
public:
    // 隐藏了父类的 fun()
    void fun(int) { cout << "Student::func(int)" << endl; }
    void Print() {
        cout << _name << endl;          // 访问子类自己的
        cout << Person::_num << endl;   // 显式访问父类
    }
protected:
    int _num = 999;
};

四、派生类的默认成员函数

设计理念遵循:父类负责初始化父类部分,子类负责初始化自身部分。

1. 构造函数

子类不能在初始化列表中直接初始化基类的私有成员。必须通过调用基类的构造函数来完成初始化。

  • 默认情况: 如果子类构造函数未显式调用基类构造函数,编译器会自动调用基类的默认构造函数。若基类没有默认构造函数,则会报错。
  • 显式调用: 推荐在初始化列表中显式调用基类构造函数。
class Person {
public:
    Person(const char* name) : _name(name) { cout << "Person()" << endl; }
protected:
    string _name;
};

class Student : public Person {
public:
    // 必须在初始化列表中调用基类构造函数
    Student(const char* name, int id) : Person(name), _id(id) { }
protected:
    int _id;
};

初始化顺序严格遵循成员声明顺序,通常先执行基类构造,再执行成员变量初始化。

2. 拷贝构造

派生类的拷贝构造函数必须调用基类的拷贝构造函数。

class Student : public Person {
public:
    Student(const Student& s) : Person(s), _id(s._id) { }
};

这里利用了子类对象可以隐式转换为基类引用的特性,将 s 传给 Person 的拷贝构造。

3. 赋值重载

赋值运算符同样需要处理基类部分的赋值。由于派生类可能隐藏基类的赋值运算符,通常需要显式调用基类的版本。

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

4. 析构函数

构造和析构的顺序是相反的:构造是先基后子,析构是先子后基。

子类析构函数完成后,系统会自动调用基类析构函数。如果手动调用 ~Base(),可能会破坏自动调用的机制,导致重复析构或顺序错误。

为什么要保证先子后父? 因为子类对象中可能依赖父类成员的资源(如动态内存)。如果父类先析构,子类访问父类成员就会变成悬空指针或非法访问。

五、继承与友元

友元关系不能继承。父类的友元无法访问子类的私有或保护成员。如果需要在子类中使用友元,必须在子类中重新声明。

六、继承与静态成员

静态成员属于类而非对象。在继承体系中,无论派生出多少个子类,静态成员在整个继承链中只有一个实例,不会在子类中单独拷贝一份。

七、多继承、菱形继承与虚拟继承

实践中建议谨慎使用多继承,尽量避免菱形继承结构。

单继承:一个子类只有一个直接父类。 多继承:一个子类有两个或以上直接父类。 菱形继承:多继承的特殊情况,导致数据冗余和二义性。

问题演示

class Person { public: string _name; int _age; };
class Student : public Person { protected: int _num; };
class Teacher : public Person { protected: int _id; };
class Assistant : public Student, public Teacher { protected: string _majorCourse; };

// 访问 _age 会产生二义性
Assistant as; 
as._age = 19; // 报错:对'_age'的访问不明确

虽然可以通过 as.Student::_age 解决二义性,但这违背常理且浪费空间(存在两份 _age)。

虚拟继承解决方案

引入 virtual 关键字进行虚拟继承,确保基类在最终派生类中只有一份实例。

class Student : virtual public Person { ... };
class Teacher : virtual public Person { ... };

这样,Assistant 对象中只包含一份 Person 的数据,解决了冗余和二义性问题。底层实现上,编译器会通过虚基表指针计算偏移量来定位共享的基类成员。

八、继承和组合

class D : public C {}; // 继承
class E { private: C _cc; }; // 组合
  • 继承(白箱复用):父类细节对子类可见,耦合度高。改变父类可能影响子类。适合表达 is-a 关系(如学生是人)。
  • 组合(黑箱复用):对象内部细节不可见,只能通过公共接口交互,耦合度低。适合表达 has-a 关系(如车有轮胎)。

实际开发中,优先使用组合。只有在明确需要多态行为或表达本质上的 is-a 关系时,才考虑使用继承。

目录

  1. 一、概念
  2. 二、基类和派生类对象赋值转换
  3. 三、继承中的作用域
  4. 四、派生类的默认成员函数
  5. 1. 构造函数
  6. 2. 拷贝构造
  7. 3. 赋值重载
  8. 4. 析构函数
  9. 五、继承与友元
  10. 六、继承与静态成员
  11. 七、多继承、菱形继承与虚拟继承
  12. 问题演示
  13. 虚拟继承解决方案
  14. 八、继承和组合

更多推荐文章

查看全部
  • JavaScript Proxy 代理机制与核心方法详解

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online