当前位置: 首页 > news >正文

《C++》继承完全指南:从入门到精通

文章目录

    • 一、继承的基本概念
      • 1.1 什么是继承?
      • 1.2 为什么需要继承?
      • 1.3 继承的基本语法
    • 二、三种继承方式详解
      • 2.1 public继承(最常用)
      • 2.2 protected继承(较少用)
      • 2.3 private继承(极少用)
      • 2.4 访问权限总结表
    • 三、继承中的特殊现象
      • 3.1 名字隐藏(重定义)
      • 3.2 对象切片
    • 四、构造和析构的顺序
      • 4.1 构造顺序
      • 4.2 析构顺序
      • 4.3 注意事项
    • 五、菱形继承与虚拟继承
      • 5.1 什么是菱形继承?
      • 5.2 解决方案:虚拟继承
      • 5.3 虚拟继承的实现原理
    • 六、继承 vs 组合
      • 6.1 继承(is-a关系)
      • 6.2 组合(has-a关系)
      • 6.3 如何选择?
    • 七、尾言

作为一名仍在不断学习的C++开发者,我深知继承机制是面向对象编程中最基础也最容易被误解的概念之一。在刚开始学习继承时,我也曾对切片问题、菱形继承等概念感到困惑。正是这些学习过程中的困惑和解决过程,促使我写下这篇总结,希望能帮助和我一样正在学习C++的朋友们少走一些弯路。

一、继承的基本概念

1.1 什么是继承?

  • 想象你正在玩积木游戏。你有一套基础积木(比如长方形、正方形),现在你想搭建一个房子。如果每次都要从头开始拼墙、拼窗户会很麻烦。继承就像是在基础积木上添加新零件,让它变成"带窗户的墙积木",这样下次搭建时直接用这个新积木就行了。

  • 在C++中,继承就是让新类(派生类)"继承"已有类(基类)的特性,然后添加自己独有的功能。比如:

class Animal {  // 基类(父类)
public:void eat() { cout << "吃东西" << endl; }
};class Cat : public Animal {  // 派生类(子类)
public:void meow() { cout << "喵喵叫" << endl; }
};
  • 这样,Cat类自动获得了eat()方法,不用重新编写,还能新增meow()方法。

1.2 为什么需要继承?

    1. 代码复用:避免重复编写相同的代码
    1. 扩展性:可以在不修改基类的情况下扩展功能
    1. 多态基础:为后续学习多态打下基础
    1. 层次清晰:类之间的关系一目了然

1.3 继承的基本语法

class 派生类名 : 访问修饰符 基类名 {// 派生类新增的成员
};
  • 访问修饰符可以是publicprotectedprivate,它们决定了基类成员在派生类中的可见性。

二、三种继承方式详解

2.1 public继承(最常用)

class Student : public Person {// Person的public成员在这里仍然是public// protected成员仍然是protected
};

特点

  • 满足"is-a"关系(学生是人)
  • 基类的public成员在派生类中仍然是public
  • protected成员保持protected

适用场景:90%的情况下都应该使用public继承

2.2 protected继承(较少用)

class Student : protected Person {// Person的public成员在这里变成protected
};

特点

  • 基类的public成员在派生类中变成protected
  • 外部无法直接访问这些成员

使用场景:当你想限制基类成员的访问权限时

2.3 private继承(极少用)

class Student : private Person {// Person的所有可访问成员在这里都变成private
};

特点

  • 基类的public和protected成员都变成private
  • 几乎完全封闭了基类的接口

建议:优先考虑组合而不是private继承

2.4 访问权限总结表

基类成员/继承方式public继承protected继承private继承
public成员publicprotectedprivate
protected成员protectedprotectedprivate
private成员不可见不可见不可见

重要规则

    1. 基类的private成员在任何继承方式下都不可访问
    1. 最终访问权限 = min(成员在基类的访问权限,继承方式)
    1. class默认private继承,struct默认public继承(但建议显式指定)

三、继承中的特殊现象

3.1 名字隐藏(重定义)

  • 当基类和派生类有同名成员时,派生类成员会"隐藏"基类成员:
class Father {
public:void show() { cout << "我是爸爸"; }
};class Son : public Father {
public:void show() {  // 隐藏了Father的show()Father::show();  // 显式调用基类方法cout << "我是儿子"; }
};

解决方法:使用作用域解析符::显式指定

3.2 对象切片

  • 当派生类对象赋值给基类对象时,会发生"切片":
class CakeBase { int flour; };
class CreamCake : public CakeBase { int cream; };CreamCake wholeCake;
CakeBase slicedCake = wholeCake;  // 只保留基类部分

为什么会发生切片?

  • 这是因为派生类对象中包含完整的基类子对象,赋值时只拷贝基类部分。

如何避免切片?

    1. 使用指针或引用:
    CakeBase* p = &wholeCake;  // 不切片
    CakeBase& r = wholeCake;   // 不切片
    
    1. 使用虚函数(多态)

四、构造和析构的顺序

4.1 构造顺序

  • 就像盖房子:
      1. 先打地基(基类构造)
      1. 再建上层(派生类构造)
class Base {
public:Base() { cout << "基类构造"; }
};class Derived : public Base {
public:Derived() { cout << "派生类构造"; }
};// 创建Derived对象时输出:
// "基类构造" → "派生类构造"

4.2 析构顺序

  • 就像拆房子:
      1. 先拆上层(派生类析构)
      1. 再拆地基(基类析构)
~Derived() { cout << "派生类析构"; }
~Base() { cout << "基类析构"; }// 销毁Derived对象时输出:
// "派生类析构" → "基类析构"

4.3 注意事项

    1. 派生类构造函数必须调用基类构造函数
    1. 如果基类没有默认构造函数,必须在派生类构造函数的初始化列表中显式调用
    1. 析构函数应该声明为virtual(多态情况下)

五、菱形继承与虚拟继承

5.1 什么是菱形继承?

Person
Student
Teacher
Assistant
  • 这种继承结构会导致:
      1. 数据冗余:Assistant会有两份Person成员
      1. 二义性:无法确定访问的是哪个Person成员

5.2 解决方案:虚拟继承

class Person {};
class Student : virtual public Person {};  // 虚拟继承
class Teacher : virtual public Person {};  // 虚拟继承
class Assistant : public Student, public Teacher {};

虚拟继承的特点

    1. 确保整个继承体系中只保留一份基类子对象
    1. 通过虚基表指针和虚基表实现
    1. 解决了数据冗余和二义性问题

5.3 虚拟继承的实现原理

    1. 编译器会为每个虚拟继承的类添加虚基表指针
    1. 虚基表中存储了到共享基类的偏移量
    1. 通过这个偏移量可以找到共享的基类子对象

六、继承 vs 组合

6.1 继承(is-a关系)

// "宝马是一种汽车"
class BMW : public Car {};

适用场景

  • 派生类确实是基类的特殊化
  • 需要实现多态
  • 严格的层次关系

6.2 组合(has-a关系)

// "汽车有一个发动机"
class Car {Engine engine;  // 组合
};

优势

    1. 更低的耦合度
    1. 更高的灵活性
    1. 更好的封装性

6.3 如何选择?

优先使用组合的情况

  • 只是需要复用代码
  • 不是严格的"is-a"关系
  • 需要动态更换组件

必须使用继承的情况

  • 实现多态(虚函数)
  • 严格的"is-a"关系
  • 接口实现

七、尾言

  • 由于个人水平有限,文中难免存在疏漏或表述不当之处。如果您在阅读过程中发现任何问题,或者有更好的实践建议,欢迎在评论区留言讨论。
http://www.lryc.cn/news/611242.html

相关文章:

  • 【Day 16】Linux-性能查看
  • 计算机基础:操作系统学习的基石
  • 分布式微服务--Nacos 集群部署
  • RabbitMQ延时队列的两种实现方式
  • 磁悬浮转子的“静音术”:深度解析无接触抑制旋转幽灵的奥秘
  • 基于华为开发者空间的Open WebUI数据分析与可视化实战
  • 【Linux系统编程】线程概念与控制
  • MATLAB实现菲涅尔法全息成像仿真
  • Spring Boot 整合 Web 开发全攻略
  • Java面试宝典:深入解析JVM运行时数据区
  • Linux 内存管理之 Rmap 反向映射(二)
  • EP01:【DL 第二弹】张量(Tensor)的创建和常用方法
  • BloodHound 8.0 首次亮相,在攻击路径管理方面进行了重大升级
  • IPD研发管理——决策评审DCP指南
  • Java从入门到精通 - 集合框架(一)
  • MySQL主从延迟到崩溃:Binlog格式、半同步复制与GTID的博弈
  • 视频转二维码在教育场景中的深度应用
  • 结合opencv解释图像处理中的结构元素(Structuring Element)
  • 【Java企业级开发】(七)Spring框架
  • 区块链:重构信任的价值互联网革命​
  • 场外个股期权的额度为何受限?
  • 浮动IP(Floating IP)的删除通常需要满足什么条件
  • 基于ZYNQ ARM+FPGA的声呐数据采集系统设计
  • uniapp转app时,cover-view的坑
  • 什么情况下浮动IP(Floating IP)会“漂移”(Drift)
  • OneCode 3.0 前端架构全面研究
  • ​​机器学习贝叶斯算法
  • MinIO01-入门
  • 本地部署文档管理平台 BookStack 并实现外部访问( Windows 版本)
  • Claude Code 完整指南:入门到应用