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

C++ 继承:从概念到实战的全方位指南

在面向对象程序设计的世界里,继承是实现代码复用、构建清晰类层次的核心机制。很多初学者在接触继承时,容易被访问权限、隐藏规则、菱形继承等概念绕晕。今天,我们就结合《01. 继承.pdf》的核心内容,从基础到进阶,手把手带你掌握 C++ 继承的精髓,避开那些容易踩的坑。

一、继承是什么?解决了什么痛点?

先从一个真实场景说起:如果要设计Student(学生)和Teacher(教师)两个类,你会发现它们有大量重复成员 —— 姓名(_name)、地址(_address)、电话(_tel),还有身份认证(identity())这样的重复函数。直接在两个类里分别定义这些内容,不仅代码冗余,后续修改时还要 “两处同步改”,维护成本极高。

这就是继承要解决的核心问题:将多个类的公共成员抽取到 “基类(父类)”,让 “派生类(子类)” 继承基类的特性,同时扩展自己的独有成员。比如我们可以先定义Person基类,把_name、identity()等公共部分放进去,再让Student和Teacher继承Person,只新增学号(_stuid)、职称(_title)等独有成员。这样一来,代码复用率大幅提升,结构也更清晰。

从定义上看,继承(inheritance)是面向对象程序设计中 “类设计层次的复用”—— 区别于函数层次的复用,它让类之间形成 “从简单到复杂” 的层次结构,完美契合人类的认知规律。

二、继承的基础语法:3 个核心要素

掌握继承,首先要搞懂 “基类 / 派生类”“继承方式”“访问权限” 这三个核心要素,以及它们之间的关系。

1. 基本定义格式

继承的语法非常直观,派生类定义时通过:指定基类和继承方式,格式如下:


// 基类(父类):存储公共成员class Person {public:// 公共函数:身份认证(学生、教师共用)void identity() {cout << "身份认证:" << _name << endl;}protected:// 保护成员:子类可访问,类外不可访问string _name = "张三"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄};// 派生类(子类):public继承Personclass Student : public Person {public:// 子类独有函数:学习void study() {cout << _name << "正在学习" << endl;}protected:// 子类独有成员:学号int _stuid;};// 派生类(子类):public继承Personclass Teacher : public Person {public:// 子类独有函数:授课void teaching() {cout << _name << "正在授课" << endl;}protected:// 子类独有成员:职称string _title;};

这里Person是基类(也叫父类),Student和Teacher是派生类(也叫子类),public是继承方式 —— 这是实际开发中最常用的继承方式。

2. 3 种继承方式与访问权限的关系

继承方式(public/protected/private)决定了 “基类成员在派生类中的访问权限”,这是继承的核心考点之一。

基类成员类型

public 继承后

protected 继承后

private 继承后

public 成员

派生类 public

派生类 protected

派生类 private

protected 成员

派生类 protected

派生类 protected

派生类 private

private 成员

不可见

不可见

不可见

必须记住的 4 个关键结论:
  1. 基类 private 成员 “不可见”≠“不继承”:基类的 private 成员会被继承到派生类对象中(占用内存),但语法上限制派生类无论在类内还是类外都无法访问。
  1. protected 成员是为继承而生:如果基类成员想 “不让类外访问,但允许子类访问”,就定义为protected—— 这是protected和private的核心区别。
  1. 权限计算规则:基类非 private 成员在派生类的访问权限 = Min(成员在基类的访问限定符, 继承方式),权限优先级:public > protected > private。比如基类 public 成员 + protected 继承,在派生类中就是 protected 权限。
  1. 默认继承方式要注意:用class定义类时,默认继承方式是private;用struct时默认是public。但建议显式写出继承方式,避免歧义。

3. 实际开发的继承选择

实际开发中几乎只使用 public 继承。因为protected/private继承下来的成员,只能在派生类内部使用,后续无法进一步扩展(比如派生类的子类无法访问),维护性极差。

三、继承中的 “坑”:3 个核心规则

掌握了基础语法后,还要警惕继承体系中的特殊规则 —— 这些是面试和实战中最容易出错的地方。

1. 基类与派生类的对象转换:切片规则

在public继承下,派生类对象和基类对象之间有特定的转换规则,形象地称为 “切片(切割)”:

  • 允许的转换
    1. 派生类对象可以赋值给基类指针 / 引用:比如Student s; Person* p = &s;,此时基类指针 / 引用指向的是派生类对象中 “基类那部分”,就像把派生类 “切” 出基类的部分赋值过去。
    1. 派生类对象可以赋值给基类对象:本质是调用基类的拷贝构造函数,只拷贝基类部分的成员。
  • 禁止的转换:基类对象不能赋值给派生类对象。比如Person p; Student s = p;会编译报错,因为基类没有派生类的独有成员(如_stuid),无法完成赋值。

注意:基类指针 / 引用可以通过强制类型转换赋值给派生类指针 / 引用,但只有当基类指针指向的是派生类对象时才安全。如果是多态场景,建议用dynamic_cast进行安全转换。

2. 同名成员的隐藏规则:不是重载!

在继承体系中,基类和派生类有同名成员(变量或函数)时,会触发 “隐藏” 规则 —— 这是很多初学者混淆的点:

  • 隐藏的定义:派生类成员会屏蔽基类对同名成员的直接访问,即使函数参数不同(这和重载不同,重载要求 “同一作用域”,而继承是不同作用域)。
  • 函数隐藏的关键:只要函数名相同,就构成隐藏,无需参数列表匹配。比如基类有void fun(),派生类有void fun(int i),派生类的fun(int)会隐藏基类的fun()。
  • 访问隐藏成员的方式:如果要在派生类中访问基类的同名成员,必须显式加基类作用域,比如Person::_num。

实战建议:尽量不要在继承体系中定义同名成员,容易混淆出错。比如基类Person有int _num(身份证号),子类Student有int _num(学号),子类的_num会隐藏父类的_num,访问时必须显式指定作用域。

3. 派生类的默认成员函数:必须调用基类!

C++ 类有 6 个默认成员函数(构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载),其中前 4 个在继承中有特殊规则,核心是 “派生类必须依赖基类完成初始化和清理”:

(1)构造函数:先基类后派生类
  • 派生类的构造函数必须调用基类的构造函数,初始化基类部分的成员。
  • 如果基类有默认构造函数(无参或全缺省),派生类可以省略调用,编译器会自动隐式调用;
  • 如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的构造函数,否则编译报错。

示例:

// 基类:无默认构造函数(只有带参构造)class Person {public:Person(const char* name) : _name(name) {}protected:string _name;};// 派生类:必须显式调用基类构造函数class Student : public Person {public:// 初始化列表中显式调用基类构造Student(const char* name, int num) : Person(name), _num(num) {}protected:int _num;};
(2)析构函数:先派生类后基类
  • 派生类的析构函数无需显式调用基类析构,编译器会在派生类析构函数执行完毕后,自动调用基类析构函数。
  • 这样做是为了保证 “先清理派生类成员,再清理基类成员” 的顺序,避免资源泄漏。

注意:析构函数名会被编译器特殊处理为destructor(),所以基类析构不加virtual时,派生类析构和基类析构是 “隐藏” 关系,不是重载。

(3)拷贝构造与赋值重载:需显式调用基类
  • 派生类的拷贝构造函数必须调用基类的拷贝构造,否则基类部分的成员不会被正确拷贝;
  • 派生类的赋值重载必须调用基类的赋值重载,因为派生类的赋值重载会隐藏基类的赋值重载,需要显式加基类作用域调用(如Person::operator=(s))。

四、多继承与菱形继承:C++ 的 “痛点”

多继承是 C++ 的一个特性,但也带来了复杂的问题 —— 尤其是菱形继承,这是 C++ 继承中最容易踩坑的部分。

1. 多继承的定义

  • 单继承:一个派生类只有一个直接基类(如Student继承Person);
  • 多继承:一个派生类有两个或以上直接基类(如Assistant同时继承Student和Teacher)。
  • 多继承对象的内存模型:先继承的基类在内存前面,后继承的基类在后面,派生类成员放在最后。

2. 菱形继承的问题:数据冗余与二义性

菱形继承是多继承的特殊情况:类 A 派生出类 B 和类 C,类 D 同时继承类 B 和类 C,形成 “菱形” 结构。比如Person派生出Student和Teacher,Assistant(助教)同时继承Student和Teacher,此时会出现两个严重问题:

  • 数据冗余:Assistant对象中会有两份Person的成员(如_name),浪费内存
  • 二义性:访问_name时,编译器不知道是Student继承的_name还是Teacher继承的_name,编译报错。

示例:


class Assistant : public Student, public Teacher {protected:string _majorCourse; // 主修课程};int main() {Assistant a;a._name = "peter"; // 编译报错:对“_name”的访问不明确// 需显式指定作用域:a.Student::_name = "xxx";return 0;}

即使显式指定作用域能解决二义性,数据冗余问题依然存在。

3. 解决方案:虚继承

为了解决菱形继承的问题,C++ 引入了 “虚继承(Virtual Inheritance)”:在间接基类的继承处加上virtual关键字,让间接派生类只保留一份间接基类的成员。

修改后的代码:


// 虚继承:Student和Teacher继承Person时加virtualclass Student : virtual public Person { ... };class Teacher : virtual public Person { ... };// Assistant继承Student和Teacherclass Assistant : public Student, public Teacher { ... };int main() {Assistant a;a._name = "peter"; // 正确:无二义性,仅一份_namereturn 0;}

虚继承能同时解决数据冗余和二义性问题,建议:尽量不要设计菱形继承。因为虚继承底层实现复杂,会有性能损失,且代码可读性降低。很多语言(如 Java)直接不支持多继承,就是为了规避这个问题3。

五、继承与组合:该怎么选?

除了继承,组合也是实现代码复用的重要方式。两者的适用场景不同,选对了能大幅降低代码耦合度。

1. 继承与组合的核心区别

特性

继承(Inheritance)

组合(Composition)

关系

is-a(是一个):如BMW是Car的一种

has-a(有一个):如Car有Tire(轮胎)

复用方式

白箱复用:基类内部细节对子类可见

黑箱复用:被组合对象仅通过接口提供功能

耦合度

高:基类修改会直接影响子类

低:组合类之间依赖弱,维护性好

适用场景

类间明确是 is-a 关系,或需实现多态

类间是 has-a 关系,追求低耦合

  • 继承示例:BMW和Car是 is-a 关系,BMW是Car的一种,适合用继承。
  • 组合示例:Car和Tire是 has-a 关系,Car有 4 个Tire,适合用组合。

2. 实战建议:优先使用组合

原因如下:

  • 组合耦合度低,代码维护性好:即使被组合对象(如Tire)内部修改,只要接口不变,Car类就无需修改;
  • 继承耦合度高,基类的任何修改(如成员变量名变化)都会影响所有子类;
  • 例外情况:如果类之间明确是 is-a 关系(如Student是Person),或需要实现多态(后续章节讲解),则必须用继承。

比如stack(栈)和vector(向量)的关系:既可以用继承(stack是一种vector),也可以用组合(stack有一个vector成员)。此时优先选组合,因为组合能避免暴露vector的不必要接口(如push_back),且耦合度更低。

六、实战技巧:不能被继承的类怎么设计?

有时候我们需要设计一个 “不能被继承的类”(如工具类、单例类),《01. 继承.pdf》提供了两种方法:

1. C++98 方法:基类构造函数私有化

原理:派生类构造函数必须调用基类构造函数。如果基类构造函数私有化,派生类无法调用,也就无法实例化对象。

示例:


class Base {private:// 构造函数私有化Base() {}public:void func() { cout << "Base::func" << endl; }};// 编译报错:无法访问Base的私有构造函数class Derive : public Base { ... };

2. C++11 方法:final 关键字

C++11 新增final关键字,用final修饰基类后,任何子类继承该基类都会编译报错,简洁高效。

示例:


// final修饰基类,禁止继承class Base final {public:void func() { cout << "Base::func" << endl; }};// 编译报错:无法从“final”基类“Base”继承class Derive : public Base { ... };


文章转载自:

http://p4uMqt9w.nsjpz.cn
http://XkyYg1YN.nsjpz.cn
http://lvzF5lP9.nsjpz.cn
http://1D1To6WZ.nsjpz.cn
http://QHlvP4rT.nsjpz.cn
http://UJTD8yCq.nsjpz.cn
http://oaauHpzc.nsjpz.cn
http://ZR0VLvIt.nsjpz.cn
http://QE3OVLHZ.nsjpz.cn
http://cZZ0Fy9h.nsjpz.cn
http://zBr6KBlv.nsjpz.cn
http://lNL0DnTb.nsjpz.cn
http://ayJ54q6i.nsjpz.cn
http://9ulEDdtw.nsjpz.cn
http://ekeEEsp5.nsjpz.cn
http://JJWH4zKE.nsjpz.cn
http://8a44VhBo.nsjpz.cn
http://hheiuxYO.nsjpz.cn
http://5eOw6ANO.nsjpz.cn
http://7jq9S0ch.nsjpz.cn
http://g2Ujw2B7.nsjpz.cn
http://vXdAUjpC.nsjpz.cn
http://0XHZidV6.nsjpz.cn
http://7VBBRPdD.nsjpz.cn
http://t6WPbVLE.nsjpz.cn
http://0HUemSFk.nsjpz.cn
http://Gh9mcFZm.nsjpz.cn
http://ZYTeDrEa.nsjpz.cn
http://t887Wnbv.nsjpz.cn
http://CTeealjw.nsjpz.cn
http://www.dtcms.com/a/383816.html

相关文章:

  • Python中全局Import和局部Import的区别及应用场景对比
  • S16 赛季预告
  • 【硬件-笔试面试题-95】硬件/电子工程师,笔试面试题(知识点:RC电路中的时间常数)
  • synchronized锁升级的过程(从无锁到偏向锁,再到轻量级锁,最后到重量级锁的一个过程)
  • Altium Designer(AD)自定义PCB外观颜色
  • Flink快速上手使用
  • 安卓学习 之 选项菜单(OptionMenu)
  • CKA04--storageclass
  • Dask read_csv未指定数据类型报错
  • 【代码随想录算法训练营——Day11】栈与队列——150.逆波兰表达式求值、239.滑动窗口最大值、347.前K个高频元素
  • TruthfulQA:衡量语言模型真实性的基准
  • 继承与多态
  • Python爬虫实战:研究Pandas,构建新浪网股票数据采集和分析系统
  • 【从零开始】14. 数据评分与筛选
  • 正则表达式与文本三剑客(grep、sed、awk)基础与实践
  • JavaWeb--day5--请求响应分层解耦
  • 去卷积:用魔法打败魔法,让图像清晰
  • Java开发者LLM实战——LangChain4j最新版教学知识库实战
  • 算法 --- 哈希表
  • 【科研绘图系列】R语言绘制全球海洋温度对浮游生物分裂率影响的数据可视化分析
  • 141.环形链表
  • C++ 最短路SPFA
  • 一文读懂 Java 注解运行原理
  • Dify开发中系统变量(system)和用户变量(user)的区别
  • 扩散模型之(五)基于概率流ODE方法
  • 【代码模板】Linux内核模块带指针的函数如何返回错误码?(ERR_PTR(-ENOMEM)、IS_ERR(ent)、PTR_ERR(ent))
  • 查询 mysql中 所有的 非空记录字段
  • Spring Bean:不只是“对象”那么简单
  • 快速选中对象
  • ByteDance_FrontEnd