news 2026/10/17 2:44:31

C++进阶篇(一):继承——权限、切片、默认成员函数与菱形继承(附完整代码仓库)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++进阶篇(一):继承——权限、切片、默认成员函数与菱形继承(附完整代码仓库)

0.1 概述&序言

这里是白杨。入门篇十八篇走完(string / vector / list / stack / queue / 模板),进阶篇正式开张——第一站:继承。

上一篇模板进阶的结尾卖过关子:stack既像 vector 的"一种"、又有"一个" vector,两种关系都对;这层模糊地带今天终于能说透了——一种是is-a(继承),一种是has-a(组合)。

这篇要过四件事:

  • 继承怎么写、三种继承方式下权限怎么变(九宫格表)
  • 派生类对象和基类之间的"互相转换":什么是对象切片
  • 派生类的默认成员函数(构造 / 析构的调用顺序)+ 同名隐藏的坑
  • 多继承的终极 BOSS:菱形继承与虚继承

老规矩,跑得通的结果才写进来(环境见下);完整代码文末自取,也可以直接去仓库:gitee.com/BaiYang_SQ/inherit-sim。

实测环境:w64devkit g++ 15.2.0(-std=c++11 -O2 -Wall -Wextra)+ VS2022;教学简化版。
文中代码均为本人手写并实测;官方文档:cppreference·类继承。

让我们开始吧。

一、为什么要有继承:从"复制粘贴"说起

没有继承的时候,写 Student 和 Teacher 是这样的:姓名、年龄、电话这些公共信息,两个类里一人抄一份。三五个类还行,三十个类就是灾难。

继承干的事:把公共部分提到基类里,派生类只管自己特有的部分。

namespacebasic{classPerson{public:voididentity(){cout<<"identity(): "<<_name<<" / "<<_age<<endl;}protected:string _name="张三";// 姓名(protected:派生类里能直接访问)string _address;// 地址string _tel;// 电话private:int_age=18;// 年龄(private:派生类里"看得见摸不着")};classStudent:publicPerson{public:voidstudy(){cout<<"study(): "<<_name<<" 正在学习"<<endl;// protected 成员直接可用// cout << _age << endl; // 编译报错:private 成员在派生类中不可见}protected:int_stuid=2026;// 学号};classTeacher:publicPerson{public:voidteaching(){cout<<"teaching(): "<<_name<<" 正在授课"<<endl;}protected:string _title="助教";// 职称};}

定义格式拆开看就三个词:class 派生类 : 继承方式 基类,比如class Student : public Person。实测输出:

identity(): 张三 / 18 study(): 张三 正在学习 identity(): 张三 / 18 teaching(): 张三 正在授课

派生类对象可以直接调用基类的 public 成员(identity),protected 成员可以在派生类内部直接用(_name),而 private 的 _age 在派生类里不可见。

【一句话记】继承 = 类设计层次的代码复用,表达的是is-a(是一种)关系:Student 是一种 Person。

二、三种继承方式:权限九宫格

继承方式有三种(public/protected/private,写法上只差这一个词),基类成员的访问权限也有三种,组合起来就是经典的九宫格:

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

(九宫格对照见上一节图片的右侧;下面三点比死记表格更管用。)

三条要点:

  1. 派生类里的访问权限 = Min(成员在基类的权限, 继承方式)——public > protected > private,谁更"小"听谁的;
  2. private 成员是"不可见"而不是"不存在":它照样被继承、躺在对象里,只是派生类的语法层面不能碰;
  3. class默认 private 继承、struct默认 public 继承——所以写继承一定显式写出 public,别踩默认值。

【必背】实践里 99% 用 public 继承;protected / private 继承知道有这回事就行。

三、基类和派生类对象之间的转换:切片是重点

派生类对象里"内含"一个基类子对象——这是所有转换规则的根。四条规则:

  1. 派生类对象可以赋值给基类的指针 / 引用:Person* pp = &sobj;、Person& rp = sobj;——指针和引用只是"绑到基类那半";
  2. 派生类对象可以赋值给基类对象:Person pobj = sobj;——这其实调用了基类的拷贝构造,只拷基类部分,派生类部分被切掉,这就是切片;
  3. 反过来不行:基类对象不能赋值给派生类对象,直接编译报错;
  4. 基类指针 / 引用强转回派生类,需要它真的指向派生类对象才安全(dynamic_cast属于 RTTI,后面章节细讲)。
conv::Studentsobj("jack",18);conv::Person*pp=&sobj;// 指针:绑到基类子对象conv::Person&rp=sobj;// 引用:同理conv::Person pobj=sobj;// 切片:调基类拷贝构造,_No 没了// sobj = pobj; // 编译报错:基类对象不能赋值给派生类对象

实测输出:

指针看基类部分: jack | 引用看基类部分: jack 切片后 pobj: jack(_No 被切掉了) sobj 不受影响: jack / _No=18

【易错】切片不报错、不崩溃,就是悄悄地丢数据——函数参数如果按基类对象(而不是引用/指针)传,传进去的派生对象就被切了。这是继承里最隐蔽的坑之一。

四、作用域与隐藏

基类和派生类都有独立的作用域。一旦出现同名:

  1. 派生类的同名成员会屏蔽基类成员的直接访问——这叫隐藏;
  2. 想访问基类版本,得写基类::成员;
  3. 成员函数的隐藏只看函数名,不看参数列表(不是重载!);
  4. 实际开发里,继承体系中最好别定义同名成员。
namespacehide{classPerson{protected:string _name="小李子";int_num=111;// 身份证号};classStudent:publicPerson{public:voidPrint(){cout<<" 姓名:"<<_name<<endl;cout<<" 身份证号:"<<Person::_num<<endl;// 指定基类作用域cout<<" 学号:"<<_num<<endl;// 默认访问自己的}protected:int_num=999;// 学号(同名 → 隐藏基类的 _num)};classA{public:voidfun(){cout<<"A::fun()"<<endl;}};classB:publicA{public:voidfun(inti){cout<<"B::fun(int i): "<<i<<endl;}};}

实测输出:

姓名:小李子 身份证号:111 学号:999 B::fun(int i): 10 A::fun()

Student::_num(999)和Person::_num(111)是两个独立的数据;B::fun(int)一出现,基类A::fun()就被隐藏了——b.fun()编译不过,必须写b.A::fun()。

【易错】两个小测验(自行默答):A和B里两个 func 是什么关系?——隐藏,不是重载(重载要求同作用域);b.fun()能编译吗?——不能,基类版本被隐藏后找不到。

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

"默认"的意思:你不写,编译器帮你生成。放到派生类里,这套生成规则有讲究:

  1. 派生类的构造函数必须调用基类构造初始化基类那部分成员(基类没有默认构造时,必须在初始化列表显式调用);
  2. 派生类的拷贝构造必须调用基类拷贝构造;
  3. 派生类的operator=必须调用基类的operator=——注意它隐藏了基类的operator=,要写Person::operator=(s)显式调用;否则函数发生了无限递归
  4. 派生类的析构函数在被调用完成后,会自动调用基类析构;
  5. 顺序:构造先基类、后派生;
  6. 顺序:析构先派生、后基类(像剥洋葱,谁构造得早谁析构得晚);
  7. 析构函数名会被编译器统一处理成destructor(),所以基类析构不加 virtual 时,派生类和基类析构构成的是隐藏关系(多态章节会再展开这条)。

解释一下3:当代码这样写的时候:

Student&operator=(constStudent&s){if(this!=&s){operator=(s);_num=s._num;_addrss=s._addrss;}return*t;}

尝试运行:如图

我们来解释一下: 子类的operator=隐藏了父类的operator=, 导致的在子类的operator=中调用operator=调用的实际上还是子类的而不是父类的。
解决办法:加一个显示指定:
Person::operator=(s);

namespacedm{classPerson{public:Person(constchar*name="peter"):_name(name){cout<<" Person(const char*) 构造:"<<_name<<endl;}Person(constPerson&p):_name(p._name){cout<<" Person(const Person&) 拷贝构造"<<endl;}Person&operator=(constPerson&p){cout<<" Person::operator=(const Person&) 赋值"<<endl;if(this!=&p)_name=p._name;return*this;}~Person(){cout<<" ~Person() 析构"<<endl;}protected:string _name;};classStudent:publicPerson{public:Student(constchar*name,intnum):Person(name),_num(num){cout<<" Student(const char*, int) 构造"<<endl;}Student(constStudent&s):Person(s),_num(s._num){cout<<" Student(const Student&) 拷贝构造"<<endl;}Student&operator=(constStudent&s){cout<<" Student::operator=(const Student&) 赋值"<<endl;if(this!=&s){Person::operator=(s);// 隐藏了基类的,必须显式调_num=s._num;}return*this;}~Student(){cout<<" ~Student() 析构"<<endl;}protected:int_num;};}

实测输出(构造 s1 → 拷贝 s2 → 构造 s3 → 赋值 s1=s3 → 依次析构):

--- 构造 s1 --- Person(const char*) 构造:jack Student(const char*, int) 构造 --- 拷贝构造 s2(s1) --- Person(const Person&) 拷贝构造 Student(const Student&) 拷贝构造 --- 赋值 s1 = s3 --- Student::operator=(const Student&) 赋值 Person::operator=(const Person&) 赋值 --- 离开作用域:析构顺序 = 派生类先、基类后 --- ~Student() 析构 ~Person() 析构 (×3,每个对象都是"先派生后基类")

【必背】构造函数:基类 → 派生;析构函数:派生 → 基类。operator= 记得显式调基类的。

顺带:一个"不能被继承"的类怎么写

  • C++98 写法:把基类构造函数设为私有——派生类必须调用基类构造,看不见就调不了,自然生不出对象;
  • C++11 写法:class Base final {};——final 修饰的类直接禁止被继承。实测见仓库(取消注释class Derive : public Base就会编译报错)。

六、友元与静态成员

友元不能被继承:基类的友元访问不了派生类的私有 / 保护成员;反过来也一样。想让Display(p, s)同时看懂 Person 和 Student 的内部,得让两个类都把 Display 设为友元(所以代码里有个class Student;的前置声明——Person 要认识 Student 这个名字)。

classStudent;// 前置声明classPerson{public:friendvoidDisplay(constPerson&p,constStudent&s);protected:string _name="peter";};classStudent:publicPerson{public:friendvoidDisplay(constPerson&p,constStudent&s);// 再声明一次protected:int_stuNum=20260001;};

静态成员则相反,整个继承体系里只有一份。实测输出(地址对比最有说服力):

Display: peter / 20260001 非静态成员 _name 的地址不同: 0x2a325ff710 vs 0x2a325ff760(派生类各自继承一份) 静态成员 _count 的地址: 0x7ff65c0f9030 vs 0x7ff65c0f9030(两个地址一模一样:全体系共用一份) 两个类域都能访问: 0 / 0

【一句话记】友元不能继承(要重新声明);静态成员不用继承(全体系就一份,Person::_count和Student::_count是同一个)。

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

一个派生类只有一个直接基类叫单继承;有两个或以上叫多继承。多继承的对象内存模型:先继承的基类排在前面,派生类自己的成员放最后。

菱形继承是多继承的特殊情况——Assistant 同时是 Student 和 Teacher,而两位又都继承自 Person:

classPerson{public:string _name;};classStudent:publicPerson{protected:int_num;};classTeacher:publicPerson{protected:int_id;};classAssistant:publicStudent,publicTeacher{protected:string _majorCourse;};

这种结构带来两个经典问题:

  • 数据冗余:Assistant 里躺着两份Person 的成员(两份 _name);
  • 二义性:a._name = "peter";直接编译报错error C2385: 对"_name"的访问不明确;写a.Student::_name/a.Teacher::_name能编译,但只是"指明用哪一份",冗余的问题一点没解决。

实测(x64):

非虚菱形:Student::_name = xxx(Student 那份) 非虚菱形:Teacher::_name = yyy(Teacher 那份)(两份数据,冗余) sizeof(非虚 Assistant) = 112 字节

虚继承:在继承方式前面加virtual——class Student : virtual public Person。效果是一份 Person 只保留唯一一份,二义性和冗余一起消失:

虚菱形:av._name = 王五(最派生类 AssistantV 执行的 PersonV("王五") 生效) sizeof(虚 AssistantV) = 96 字节

注意上面那行"王五":虚基类由最派生类初始化。构造AssistantV("张三", "李四", "王五")时,StudentV / TeacherV 里传的名字都会被忽略,只有最派生类 AssistantV 初始化列表里的PersonV(name3)生效——所以三个名字里"王五"赢。

最后补一道经典选择题(指针偏移):

指针偏移实测:Base1* p1 == Derive* p3(偏移 0 字节);Base2* p2 偏移 4 字节 (课本 7.3 选择题答案 C:p1 == p3 != p2——先继承的基类排在前面)

【面试】虚继承解决了什么问题?代价是什么?——解决数据冗余和二义性;代价是多出虚基表指针(本实测里 112 → 96:省下一份 Person 的成员,同时多了两个 vbptr 的开销)、对象布局变复杂、访问还多一次间接。所以"多继承可以设计,但别设计出菱形继承"——后来的 Java 干脆不支持多继承,从语言层面躲开了这个坑。标准库自己也只在 IO 家族(iostream那棵菱形树)里用过这一招。

八、继承 vs 组合:is-a 和 has-a

  • public 继承 = is-a:每个派生类对象都是一个基类对象;
  • 组合 = has-a:B 里放了一个 A 对象,B 就"有"一个 A。

两者的工程差异:

对比继承(白箱复用)组合(黑箱复用)
关系is-ahas-a
内部细节基类对派生类可见(破坏封装)只看接口,内部不可见(封装好)
耦合强(基类一变,派生类跟着受影响)弱(依赖接口)
// Tire 和 Car 更像 has-a:车"有"四条轮胎classCar{/* ... */Tire _t1,_t2,_t3,_t4;};// BMW 和 Car 更像 is-a:宝马"是"一种车classBMW:publicCar{public:voidDrive(){cout<<"好开-操控"<<endl;}};

还记得 stack 和 vector 吗——两种关系都说得通。这种情况怎么办?优先组合:

template<classT>classstack2// 组合版:有一个 vector{vector<T>_v;// has-apublic:voidpush(constT&x){_v.push_back(x);}// ...};

实测输出:好开-操控、组合版 stack2: 8。

【一句话记】is-a 用继承;has-a 用组合;两种都符合时优先组合(耦合低、封装好)。例外只有两个:实现多态必须继承;实在没有组合方案时才用继承。

九、速记卡

考前扫一眼,面试不卡壳:

十、自测三连

先自己默答,再看答案对照:

  1. 派生类里成员的访问权限怎么算?private 成员到底有没有被继承?
答案

= Min(成员在基类的权限, 继承方式),按 public > protected > private 取更"低"的那个。private 成员被继承了、存在于对象里,但派生类的语法层面"不可见",不能直接访问(可以通过基类的 public/protected 接口间接使用)。

  1. 什么是对象切片?为什么派生类对象给基类指针/引用赋值不算切片?
答案

派生类对象赋值给基类对象(Person pobj = sobj;)时调用的是基类拷贝构造,只拷基类部分,派生类成员被丢掉——这就是切片。指针/引用只是"绑到对象的基类子对象"上,对象本身没被复制或裁掉,所以不算切片。

  1. 菱形继承有什么问题?虚继承怎么解决?虚基类由谁初始化?
答案

问题:数据冗余(多份虚基类成员)+ 二义性(不指定类域没法访问)。解法:继承时写virtual public(虚继承),保留唯一一份基类成员。虚基类由最派生类的构造函数初始化,中间类初始化列表里对虚基类的传参会被忽略。


附录 A:完整代码仓库

完整工程(单文件版,全部实验一次跑完)在 Gitee:

gitee.com/BaiYang_SQ/inherit-sim。

仓库文件说明
inherit_sim.cpp单文件:全部实验;章节 ↔ 函数名对照见仓库 README 导览表
output.txt全部实验的真实运行输出(含 sizeof / 地址 / 偏移实测,不用装编译器)
build.batWindows 一键编译 + 运行 + 刷新 output.txt
README.md一分钟上手 + 代码导览

面试前手写练手,照这个骨架压考点(完整版见仓库):

// 继承·考点骨架(完整可跑版见 Gitee 仓库)#include<iostream>#include<string>usingnamespacestd;classPerson{public:Person(conststring&name="peter"):_name(name){}string _name;};classStudent:publicPerson{public:Student(conststring&name,intno):Person(name),_No(no){}int_No;};// 隐藏:同名就隐藏,函数只看名字classA{public:voidfun(){cout<<"A::fun"<<endl;}};classB:publicA{public:voidfun(inti){cout<<"B::fun "<<i<<endl;}};intmain(){Students("jack",18);Person*pp=&s;// OK:绑基类部分Person p=s;// 切片:_No 被切掉cout<<pp->_name<<" / "<<p._name<<endl;B b;b.fun(10);b.A::fun();// 被隐藏的基类版本要指定类域// Student s2 = p; // 编译报错:基类对象不能赋给派生类return0;}

好了,C++ 进阶篇第一篇到此完结!下一篇继续进阶主线——多态(预告:虚函数、重写、虚函数表、动态绑定到底怎么发生)。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:44:14

AI时代小白程序员如何突破求职瓶颈,抢占大模型红利?

文章指出&#xff0c;AI技术正在改变职场招聘环境&#xff0c;尤其是对刚毕业的小白和程序员。AI能自动化完成许多初级任务&#xff0c;导致入门级岗位需求减少&#xff0c;同时对求职者的能力要求更高&#xff0c;需要具备AI协同能力、业务判断力和创造力。文章建议年轻人应减…

作者头像 李华
网站建设 2026/10/17 2:44:03

一个尾点改写配置:Copilot 远程代码执行漏洞复盘

一个尾点改写配置&#xff1a;Copilot 远程代码执行漏洞复盘原文&#xff1a;Embrace The Red - 《Visual Studio Copilot and SSMS Remote Code Execution (CVE-2026-103572)》&#xff08;https://embracethered.com/blog/posts/2026/visual-studio-copilot-remote-code-exec…

作者头像 李华
网站建设 2026/10/17 2:43:51

小白程序员入门大模型,这些 Agent 开发关键词你必须知道!

本文介绍了 AI 大模型应用开发的关键技术&#xff0c;包括 AI Agent 的核心要素&#xff08;任务规划、记忆机制等&#xff09;、RAG 技术的要点&#xff08;Chunking、向量检索等&#xff09;、AI 编程工具&#xff08;Vibe Coding、GitHub Copilot&#xff09;以及 Agent 框架…

作者头像 李华
网站建设 2026/10/17 2:43:28

免费软件授权平台怎么接入?AI一键对接流程

给现有软件增加授权功能&#xff0c;雪松授权云无需先自建授权服务器。平台管理授权规则&#xff0c;客户端完成验证和业务限制&#xff0c;AI辅助实现接入代码。 下面介绍接入路径与检查点&#xff0c;不是已经完成测试的项目报告。具体资源限制以当前套餐为准。 1. 先确定要…

作者头像 李华
网站建设 2026/10/17 2:43:23

双电芯主动均衡方案:MP2642GR-Z与PIC18F45K22协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华