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

C++ 继承机制详解

C++ 继承机制详解。涵盖继承定义、访问权限控制、对象切片、作用域隐藏规则、默认成员函数调用顺序、虚继承解决菱形继承问题,以及继承与组合的设计权衡。通过实例演示了构造函数初始化列表、析构顺序及多继承指针偏移等关键细节,帮助开发者理解内存模型与设计模式。

DataScient发布于 2026/3/27更新于 2026/10/2586 浏览
C++ 继承机制详解

C++ 继承机制详解

1. 继承的概念及定义

1.1 继承的概念

在面向对象程序设计中,我们常需要模拟现实世界中的层级关系。例如,学生和老师都有姓名、年龄等共性,但又有各自的特性(如学号、职称)。如果每个类都重复定义这些共性信息,代码将变得冗余且难以维护。

C++ 的继承 (inheritance) 机制正是为了解决这个问题而设计的。它允许我们在保持原有类特性的基础上进行扩展,增加新的方法或属性,从而产生新的类。被继承的类称为基类(或父类),新产生的类称为派生类(或子类)。

核心意义:继承是面向对象编程中实现代码复用的最重要手段之一。它体现了由简单到复杂的认知过程,呈现了类层次的层次结构。

1.2 继承定义

1.2.1 定义格式

派生类的类名称后面加上冒号和继承方式,再跟基类名称。继承方式的符号与访问限定符一致,分为公有 (public)、保护 (protected)、私有 (private)。

class Derived : public Base {
    // 派生类成员
};
1.2.2 继承基类成员访问方式的变化

基类成员在派生类中的访问权限取决于两个因素:基类成员的原始访问限定符和继承方式。

  1. 私有成员不可见:基类的 private 成员在派生类中无论以什么方式继承都是不可见的。虽然它们物理上存在于派生类对象中,但语法上禁止直接访问。
  2. 保护成员的作用:如果基类成员不想在类外直接访问,但需要在派生类中访问,应定义为 protected。这是因继承才出现的限定符。
  3. 权限计算规则:基类其他成员在派生类的访问方式遵循 Min(成员在基类的访问限定符,继承方式) 原则。权限大小上 public > protected > private。
  4. 默认继承方式:使用关键字 class 时默认继承方式是 private,使用 struct 时默认是 public。建议显式写出继承方式以便维护。
  5. 最佳实践:实际开发中一般使用 public 继承。protected 或 private 继承会限制成员的使用范围,降低扩展性和维护性,通常不提倡。
class Person {
public:
    void Print() { cout << _name << endl; }
protected:
    string _name;
private:
    int _age;
};

class Student : public Person {
:
     _stunum;
};
protected
int

1.3 继承类模板

当基类是类模板时,需要注意实例化的问题。由于模板是按需实例化,未调用的函数不会生成代码,因此子类查找时可能找不到对应接口。

// 示例:Stack 基于 Vector 实现
template<class T>
class Stack : public std::vector<T> {
public:
    void push(const T& x) {
        // 必须显式指定作用域,否则编译器找不到 push_back
        vector<T>::push_back(x);
    }
    void pop() {
        vector<T>::pop_back();
    }
    const T& top() {
        return vector<T>::back();
    }
    bool empty() {
        return vector<T>::empty();
    }
};

对于构造与析构函数等特殊的函数,编译器会做特殊处理,自动调用,因此即使不显式调用也不会报错。

2. 基类和派生类间的转换

  • 向上转换:public 继承的派生类对象可以赋值给基类的指针或引用。这被称为对象切片,寓意把派生类中基类那部分切出来,基类指针指向的是这部分数据。
  • 向下转换:基类对象不能赋值给派生类对象,因为派生类增加了成员,基类对象没有对应的值。
  • 强制转换:基类指针可以通过强制类型转换赋值给派生类指针,但这必须是安全的。如果基类是多态类型,建议使用 RTTI 的 dynamic_cast 进行识别后再转换。
class Person {
public:
    string _name;
};

class Student : public Person {
public:
    int _No;
};

int main() {
    Student sobj;
    Person pobj = sobj; // 切片:子类对象赋值给父类对象
    
    Person* pp = &sobj;
    Student* ps1 = dynamic_cast<Student*>(pp); // 安全转换
    
    pobj._name = "张三";
    // sobj = (Student)pobj; // 编译报错:父类对象不能赋值给子类对象
    
    return 0;
}

3. 继承中的作用域

3.1 隐藏规则

  1. 在继承体系中,基类和派生类都有独立的作用域。
  2. 如果派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
  3. 如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数列表不同也不构成重载。
  4. 在派生类成员函数中,可以使用 基类::基类成员 显式访问被隐藏的基类成员。
class Person {
protected:
    string _name = "小李子";
    int _num = 111;
};

class Student : public Person {
public:
    void Print() {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl; // 显式访问基类成员
        cout << "学号:" << _num << endl;
    }
protected:
    int _num = 999;
};

3.2 作用域相关示例

A 和 B 类中的同名函数不构成重载,而是 B 的函数对 A 的函数构成隐藏。调用时如果不传参,将调用 B 中的版本;若需调用 A 中的版本,必须显式指定作用域。

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

4.1 4 个常见默认成员函数

  1. 构造函数:派生类的构造函数必须调用基类的构造函数来初始化基类部分。如果基类没有默认构造函数,必须在初始化列表中显式调用。
  2. 拷贝构造函数:派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 赋值运算符:派生类的 operator= 隐藏了基类的 operator=,显示调用时需要指定基类作用域。
  4. 析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数。顺序是先派生类后基类,保证资源清理的正确性。

注意:析构函数会被编译器特殊处理成 destructor(),因此即使不加 virtual,基类析构函数和派生类析构函数也构成隐藏关系。但在多态场景下,基类析构函数建议声明为 virtual。

class Person {
public:
    Person(const char* name = "xxx") : _name(name) { cout << "Person()" << endl; }
    ~Person() { cout << "~Person()" << endl; }
protected:
    string _name;
};

class Student : public Person {
public:
    Student(const char* name, int num) : Person(name), _num(num) {}
    ~Student() { 
        // 不需要显式调用父类析构,编译器会自动处理
        // Person::~Person(); 
    }
protected:
    int _num;
};

4.2 实现一个不能被继承的类

  1. 私有构造函数:将基类的构造函数设为私有,派生类无法访问,从而无法实例化。
  2. final 关键字:C++11 新增 final 修饰基类,派生类就不能继承了。
class Base final {
public:
    void func() { cout << "Base::func" << endl; }
};

class Derive : public Base { // 编译错误:final 类不能被继承
    void func4() { cout << "Derive::func4" << endl; }
};

5. 继承与友元

友元关系不能继承。基类的友元不能访问派生类的私有和保护成员。如果需要访问,需要将友元函数声明为派生类的友元。

6. 继承与静态成员

基类定义了 static 静态成员,则整个继承体系里只有一个这样的成员。无论派生出多少个派生类,都共用同一份静态成员实例。

7. 多继承及其菱形继承问题

7.1 继承模型

  • 单继承:一个派生类只有一个直接基类。
  • 多继承:一个派生类有两个或以上直接基类。内存模型通常是先继承的基类在前,派生类成员在后。
  • 菱形继承:多继承的特殊情况,会导致数据冗余和二义性问题。例如 Assistant 同时继承 Student 和 Teacher,两者都继承自 Person,导致 Assistant 中有两份 Person 的数据。
class Person {
public:
    string _name;
};

class Student : public Person {
protected:
    int _num;
};

class Teacher : public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;
    // a._name = "peter"; // 二义性:不知道访问哪个基类的_name
    a.Student::_name = "xxx"; // 解决二义性,但数据冗余依然存在
    return 0;
}

7.2 虚继承

为了解决菱形继承的数据冗余和二义性问题,C++ 提供了 virtual 关键字。在中间层继承时使用 virtual public,确保最终派生类中只有一份基类成员。

class Person {
public:
    string _name;
};

class Student : virtual public Person {
protected:
    int _num;
};

class Teacher : virtual public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;
    a._name = "peter"; // 正常访问,无二义性
    return 0;
}

注意:虚继承会增加底层实现的复杂性和性能开销,设计时应尽量避免菱形继承结构。

7.3 多继承中指针偏移问题

在多继承中,派生类对象转换为不同基类指针时,地址会发生偏移。这是因为派生类对象在内存中包含了多个基类的子对象。

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main() {
    Derive d;
    Base1* p1 = &d; // 指向 Base1 部分
    Base2* p2 = &d; // 指向 Base2 部分,地址发生偏移
    Derive* p3 = &d; // 指向 Derive 起始位置
    // p1 == p3 != p2
    return 0;
}

7.4 IO 库中的菱形虚拟继承

标准库中的输入输出流类采用了虚继承来解决菱形继承问题,例如 basic_ostream 和 basic_istream 都虚继承自 basic_ios。

8. 继承和组合

  • 继承 (is-a):是一种所属关系。派生类对象是一个基类对象。通过继承复用通常被称为白箱复用,耦合度较高。
  • 组合 (has-a):是一种组成关系。B 对象中包含 A 对象。通过组合复用通常被称为黑箱复用,耦合度低,封装性好。

设计原则:优先使用组合,而不是继承。只有当类之间确实是 is-a 关系,或者需要实现多态时,才考虑使用继承。如果既适合继承又适合组合,优先选择组合。

// 组合示例:Car 包含 Tire
class Tire {
protected:
    string _brand;
};

class Car {
protected:
    Tire _t1;
    Tire _t2;
    Tire _t3;
    Tire _t4;
};

// 继承示例:BMW 是 Car
class BMW : public Car {
public:
    void Drive() { cout << "好开 - 操控" << endl; }
};

目录

  1. C++ 继承机制详解
  2. 1. 继承的概念及定义
  3. 1.1 继承的概念
  4. 1.2 继承定义
  5. 1.2.1 定义格式
  6. 1.2.2 继承基类成员访问方式的变化
  7. 1.3 继承类模板
  8. 2. 基类和派生类间的转换
  9. 3. 继承中的作用域
  10. 3.1 隐藏规则
  11. 3.2 作用域相关示例
  12. 4. 派生类的默认成员函数
  13. 4.1 4 个常见默认成员函数
  14. 4.2 实现一个不能被继承的类
  15. 5. 继承与友元
  16. 6. 继承与静态成员
  17. 7. 多继承及其菱形继承问题
  18. 7.1 继承模型
  19. 7.2 虚继承
  20. 7.3 多继承中指针偏移问题
  21. 7.4 IO 库中的菱形虚拟继承
  22. 8. 继承和组合

更多推荐文章

查看全部
  • 35 岁程序员职业转型与 Android 系统源码进阶指南
  • Web-Check 与 cpolar 实现异地远程访问网站检测工具
  • VS Code + GitHub Copilot 避坑指南:从安装配置到最佳实践
  • Ubuntu Edgy Eft 完整软件源列表配置指南
  • OpenClaw 本地 AI 智能体入门与实战指南
  • STC 单片机摄像头组别高效搜线算法与帧率优化方案
  • 开源漏洞扫描工具 SiriusScan 的 Docker 部署与常见问题解决
  • Swift Composable Architecture 大型 SwiftUI 应用架构实践
  • C++ 驱动开发核心技术要点与流程
  • Telegram 中文搜索机器人 @letstgbot 技术解析与开发实战
  • 35 岁程序员职业危机分析与技术进阶指南
  • 大模型辅助代码阅读:提升工程理解效率
  • Web 团队开发 App 是否适合选择 Capacitor
  • Vue3 + PlayCanvas 实战:3D 地图自由巡视与多关卡闯关系统
  • Ubuntu 部署 OpenClaw 完整教程
  • KIMI 与文心一言、通义千问大模型能力对比评测
  • Noname:开源三国杀游戏平台完全指南
  • VsCode 插件开发实战:实现 Git 代码自动提交推送
  • Python 基础与环境配置
  • SAP 对接钉钉 Webhook 实现方案及证书问题排查

相关免费在线工具

  • 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