【C++】继承

前言
C++有三大特性——封装、继承、多态,是面向对象的基石。此前模拟实现string、vector、list等容器时,我们也就体会到封装的价值,迭代器本身属于三大特性中的封装,所有会感到string、vector、list的结构很相似,但底层天差地别,这就在于把底层复杂的细节全部屏蔽掉,然后用相似的迭代器来访问,这就是封装带来的便利之处。
前面我们模拟实现过string、vector、list、stack、queue的底层结构,那这篇博客就来细讲C++三大特性之一的继承。

继承

一、继承的概念及定义

C 语言的复用停留在函数层级,而 C++ 的继承实现了类层级的复用 —— 在保留原有类(基类)成员的基础上,扩展新成员生成派生类,贴合 “从简单到复杂” 的认知逻辑。

1.1 无继承的痛点:代码冗余

StudentTeacher类为例,二者包含大量重复的成员(姓名、地址、身份验证等),仅少数成员不同:

#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;// 学生类classStudent{public:voididentity(){/* 身份验证逻辑 */}// 重复voidstudy(){/* 学习逻辑 */}// 独有protected: string _name; string _address; string _tel;int _age;// 重复int _stuid;// 独有};// 教师类classTeacher{public:voididentity(){/* 身份验证逻辑 */}// 重复voidteaching(){/* 授课逻辑 */}// 独有protected: string _name; string _address; string _tel;int _age;// 重复 string _title;// 独有};

重复代码不仅增加开发量,还会导致后续维护成本翻倍(比如修改身份验证逻辑需改两处)。

1.2 继承的解决方案:抽离公共部分

将重复成员抽离为Person基类,StudentTeacher通过继承复用这些成员,仅需定义独有部分:

#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;// 基类:封装学生/教师的公共成员classPerson{public:voididentity(){ cout <<"身份验证:"<< _name << endl;}protected: string _name ="yuuki"; string _address; string _tel;int _age =18;};// 派生类:学生(public继承Person)classStudent:publicPerson{public:voidstudy(){/* 学习逻辑 */}protected:int _stuid;// 独有成员};// 派生类:教师(public继承Person)classTeacher:publicPerson{public:voidteaching(){/* 授课逻辑 */}protected: string _title;// 独有成员};intmain(){ Student s; Teacher t; s.identity();// 复用基类的identity方法 t.identity();// 复用基类的identity方法return0;}

输出结果

身份验证:yuuki 身份验证:yuuki 

二、继承的基础语法

2.1 继承的定义格式

class 派生类名 : 继承方式 基类名 { // 派生类独有成员 }; 
  • 基类(父类):被继承的类(如Person);
  • 派生类(子类):基于基类扩展的类(如Student/Teacher);
  • 继承方式public/protected/private(实际开发优先用public)。

2.2 继承方式与成员访问权限

基类成员有public/protected/private三种访问权限,不同继承方式会改变派生类中基类成员的访问权限,核心规则如下(记重点即可):

核心规则说明
1基类private成员:无论哪种继承方式,派生类中不可访问(仅基类自身可访问);
2基类protected成员:派生类可访问,外部不可访问;
3class默认继承方式为privatestruct默认为public
4实际开发仅用public继承(protected/private继承扩展性差)。
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;classPerson{public:voidPrint(){ cout << _name << endl;}// public成员protected: string _name;// protected成员private:int _age;// private成员};// public继承(推荐)classStudent:publicPerson{public:voidTest(){Print();// 可访问(基类public→派生类public) _name ="Tom";// 可访问(基类protected→派生类protected)// _age = 20; // 不可访问(基类private)}protected:int _stuid;};

2.3 类模板的继承

继承模板类时,需通过类模板名<类型>::指定基类域(编译器无法自动推导):

#include<iostream>#include<vector>usingnamespace std;namespace yuuki {// 继承std::vector模板类实现栈template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){vector<T>::push_back(x);// 必须指定vector<T>域}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ yuuki::stack<int> st; st.push(1); st.push(2); st.push(3);while(!st.empty()){ cout << st.top()<<" ";// 输出:3 2 1 st.pop();}return0;}

三、基类与派生类的类型转换

public继承下的类型转换是面试高频考点,核心规则:

  1. 派生类对象 → 基类指针 / 引用:直接支持(称为 “切片”—— 切出派生类中的基类部分);
  2. 基类对象 → 派生类对象:不支持(基类不含派生类的独有成员);
  3. 基类指针 → 派生类指针:需强制类型转换(仅当基类指针指向派生类对象时安全)。
#include<iostream>usingnamespace std;classPerson// 基类{virtualvoidfunc(){}// 虚函数(为dynamic_cast做准备)protected: string _name;int _age;};classStudent:publicPerson// 派生类{public:int _stuid;};intmain(){ Student sobj;// 1. 派生类对象 → 基类指针/引用(切片) Person* pp =&sobj; Person& rp = sobj; Person pobj = sobj;// 切片赋值// 2. 基类对象 → 派生类对象(报错)// sobj = pobj; // 3. 基类指针 → 派生类指针(安全场景) Student* ps1 =dynamic_cast<Student*>(pp);// pp指向sobj,转换成功 cout << ps1 << endl;// 非空地址// 3. 基类指针 → 派生类指针(不安全场景) Person pobj2; pp =&pobj2; Student* ps2 =dynamic_cast<Student*>(pp);// pp指向基类对象,转换失败 cout << ps2 << endl;// 空地址return0;}

四、继承的核心坑点:作用域与隐藏

4.1 类域的隐藏规则

继承体系中,基类和派生类有独立作用域,若出现同名成员,派生类成员会 “隐藏” 基类成员:

  1. 同名成员变量:优先访问派生类的;
  2. 同名成员函数:仅函数名相同就隐藏(无需参数 / 返回值一致);
  3. 访问被隐藏的基类成员:需加基类名::
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
#include<iostream>usingnamespace std;classPerson// 基类{protected: string _name ="yuuki";int _num =18;// 身份证号};classStudent:publicPerson// 派生类{public:voidPrint(){ cout <<"姓名:"<< _name << endl;// 复用基类_name cout <<"学生编号:"<< _num << endl;// 访问派生类_num(隐藏基类) cout <<"身份证号:"<< Person::_num << endl;// 访问被隐藏的基类_num}protected:int _num =999;// 学生编号(与基类_num同名)};intmain(){ Student s; s.Print();return0;}

输出结果

姓名:yuuki 学生编号:999 身份证号:18 

4.2 经典面试题:函数隐藏 vs 重载

classA{public:voidfunc(){ cout <<"func()"<< endl;}};classB:publicA{public:voidfunc(int i){ cout <<"func(int i): "<< i << endl;}};intmain(){ B b; b.func(10);// 正常调用B::func(int)// b.func(); // 报错!A::func()被B::func(int)隐藏,无法直接访问 b.A::func();// 正确:显式访问基类被隐藏的函数return0;}

结论:A 和 B 的func隐藏关系(而非重载)—— 重载要求函数在同一作用域,而隐藏是不同作用域的同名函数。

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

派生类的 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),需遵循 “先基类、后派生类” 的规则:

5.1 构造函数

  • 派生类构造必须先调用基类构造,初始化基类成员;
  • 若基类无默认构造(无参 / 全缺省),派生类需在初始化列表显式调用基类构造。
classPerson{public:// 基类无默认构造(必须传参)Person(constchar* name):_name(name){ cout <<"Person构造"<< endl;}protected: string _name;};classStudent:publicPerson{public:// 派生类构造:先调用Person(name),再初始化_stuidStudent(constchar* name,int stuid):Person(name)// 显式调用基类构造(必须),_stuid(stuid){ cout <<"Student构造"<< endl;}protected:int _stuid;};intmain(){ Student s("Tom",1001);// 输出:Person构造 → Student构造return0;}

5.2 析构函数

  • 派生类析构执行完毕后,编译器自动调用基类析构(保证 “先析构派生、后析构基类”);
  • 析构函数名会被编译器统一处理为destructor(),因此基类析构不加virtual时,派生类析构会隐藏基类析构。
classPerson{public:~Person(){ cout <<"Person析构"<< endl;}};classStudent:publicPerson{public:~Student(){ cout <<"Student析构"<< endl;}};intmain(){ Student s;// 析构顺序:Student析构 → Person析构return0;}

5.3 拷贝构造 / 赋值重载

  • 拷贝构造:派生类需先拷贝基类部分,再拷贝自身成员;
  • 赋值重载:派生类需先调用基类的operator=,再赋值自身成员。


classPerson{public:Person(constchar* name ="yuuki"):_name(name){}// 基类拷贝构造Person(const Person& p):_name(p._name){ cout <<"Person拷贝构造"<< endl;}// 基类赋值重载 Person&operator=(const Person& p){if(this!=&p) _name = p._name; cout <<"Person赋值重载"<< endl;return*this;}protected: string _name;};classStudent:publicPerson{public:// 派生类拷贝构造Student(const Student& s):Person(s)// 拷贝基类部分,_stuid(s._stuid){ cout <<"Student拷贝构造"<< endl;}// 派生类赋值重载 Student&operator=(const Student& s){if(this!=&s){ Person::operator=(s);// 调用基类赋值重载 _stuid = s._stuid;} cout <<"Student赋值重载"<< endl;return*this;}protected:int _stuid =1001;};intmain(){ Student s1; Student s2 = s1;// 拷贝构造:Person拷贝构造 → Student拷贝构造 Student s3; s3 = s1;// 赋值重载:Person赋值重载 → Student赋值重载return0;}

5.4 总代码

#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;classPerson{public:/*默认构造第一种情况*///// 默认构造(有初始化)//Person(const char* name = "YUUKI")// :_name(name)//{// cout << "Person()" << endl;//}/*默认构造第二种情况,需要子类帮助*/// 默认构造(无初始化)Person(constchar* name):_name(name){ cout <<"Person()"<< endl;}// 拷贝构造Person(const Person& p):_name(p._name){ cout <<"Person(const Person& p)"<< endl;}// 赋值运算符重载 Person&operator=(const Person& p){ cout <<"Person& operator=(const Person& p)"<< endl;if(this!=&p){ _name = p._name;}return*this;}// 析构函数~Person(){ cout <<"~Person"<< endl;}protected: string _name;};classStudent:publicPerson{public:// 默认生成的构造函数的行为// 1、内置类型->不确定// 2、自定义类型->调用默认构造// 3、继承父类成员看做一个整体对象,调用父类的默认构造// 子类默认构造函数,Student(constchar* name,int num,constchar* addrss)// 将Person看做一个整体:Person(name)// 错误写法 -> :_name(name),_num(num),_addrss(addrss){}// 报错:因为父类和子类改成隐藏,因重复调用子类,导致栈溢出/*Student& operator=(const Student& s) { operator=(s); }*/~Student(){// 错误写法: ~Person();// 原因:// 1. 语法定义,先子类后父类。如果写成上面,就成了先父类再子类// 2. 子类析构完后,编译器会自动掉用父类的析构}protected:// 无缺省值/*int _num; string _addrss;*/// 有缺省值int _num =18; string _addrss ="广东佛山市";// 自定义类型会调用自动生成的构造};intmain(){ Student s1("yuuki",18,"广东佛山市"); Student s2(s1); Student s3("YUUKI",28,"广东深圳市"); s1 = s3;// 不需要在子类写赋值运算符,只需要父类里写即可return0;}

方法:将父类看成一个类型,与其他类型一起编写,可更好理解



六、继承的特殊场景

6.1 不能被继承的类

  • 方法 1(C++98):将基类构造函数设为私有(派生类无法调用构造,无法实例化);
  • 方法 2(C++11):用final关键字修饰基类(直接禁止继承)。
// 方法2:final修饰(推荐)classBasefinal{public:voidfunc(){ cout <<"Base::func()"<< endl;}};// class Derive : public Base {}; // 报错!Base被final修饰,不能继承

6.2 继承与友元

友元关系不能继承—— 基类的友元无法直接访问派生类的私有 / 保护成员,需在派生类中重新声明友元:

classStudent;// 前向声明classPerson{public:friendvoidDisplay(const Person& p,const Student& s);protected: string _name ="yuuki";};classStudent:publicPerson{public:friendvoidDisplay(const Person& p,const Student& s);// 重新声明友元protected:int _stuid =1001;};// 友元函数:可访问Person和Student的保护成员voidDisplay(const Person& p,const Student& s){ cout << p._name << endl; cout << s._stuid << endl;}intmain(){ Person p; Student s;Display(p, s);// 输出:yuuki → 1001return0;}

6.3 继承与静态成员

基类的静态成员在整个继承体系中只有一份(所有派生类共享):

classPerson{public:staticint _count;// 静态成员:统计对象数量};int Person::_count =0;// 静态成员类外初始化classStudent:publicPerson{};classTeacher:publicPerson{};intmain(){ Person::_count++; Student::_count++; Teacher::_count++; cout << Person::_count << endl;// 输出:3(三者共享_count)return0;}

七、多继承与菱形继承(C++ 的坑)

7.1 多继承的基本概念

  • 单继承:一个派生类只有一个基类(推荐使用);
  • 多继承:一个派生类有多个基类(易出问题,尽量避免);
  • 菱形继承:多继承的特殊情况(A→B、A→C、B+C→D),会导致数据冗余二义性(D 对象中有两份 A 的成员)。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.2 菱形继承的问题

classPerson{public: string _name;};// 基类classStudent:publicPerson{public:int _stuid;};// 派生类1classTeacher:publicPerson{public: string _title;};// 派生类2classAssistant:publicStudent,publicTeacher{public:int _id;};// 菱形顶点intmain(){ Assistant a;// a._name = "Tom"; // 报错!二义性:_name来自Student还是Teacher? a.Student::_name ="Tom";// 显式指定,解决二义性(但数据冗余仍存在) a.Teacher::_name ="Jerry";return0;}

7.3 虚继承解决菱形继承(不推荐)

通过virtual关键字实现虚继承,可消除数据冗余和二义性,但底层实现复杂、性能损耗大,实战中建议避免设计菱形继承

classPerson{public: string _name;};classStudent:virtualpublicPerson{public:int _stuid;};// 虚继承classTeacher:virtualpublicPerson{public: string _title;};// 虚继承classAssistant:publicStudent,publicTeacher{public:int _id;};intmain(){ Assistant a; a._name ="Tom";// 正常访问(仅一份_name)return0;}

7.4 多继承中指针偏移问题

选择以下选项:() A: p1 == p2 == p3 B: p1 < p2 < p3 C: p1 == p3 != p2 D: p1 != p2 != p3 
classBase1{public:int _b1;};classBase2{public:int _b2;};classDerive:publicBase1,publicBase2{public:int _d;};intmain(){ Derive d; Base1* p1 =&d; Base2* p2 =&d; Derive* p3 =&d;return0;}

7.5 IO库中的菱形虚拟继承

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

八、继承 vs 组合(设计原则)

特性继承(is-a 关系)组合(has-a 关系)
关系派生类是一个基类(如 Student 是 Person)类包含另一个类(如 Car 包含 Engine)
封装性破坏基类封装(派生类可访问基类保护成员)高封装(被组合类的细节不可见)
耦合度高(基类修改会影响派生类)低(被组合类修改不影响组合类)
复用方式白箱复用(基类细节可见)黑箱复用(仅通过接口访问)

设计原则:优先使用组合

  • 若类之间是 “is-a” 关系(如 Student 是 Person),用继承;
  • 若类之间是 “has-a” 关系(如 Car 有 Engine),用组合;
  • 若两者皆可,优先选组合(降低耦合,提升代码可维护性)。

总结

  1. 继承的核心是代码复用,实战中优先用public继承;
  2. 继承的核心坑点是同名成员隐藏,需通过基类::访问被隐藏成员;
  3. 派生类默认成员函数需遵循 “先基类、后派生类” 的规则;
  4. 多继承(尤其是菱形继承)易出问题,尽量避免;
  5. 设计类时,优先用组合而非继承(降低耦合)。

继承是 C++ 多态的基础,但滥用会导致代码臃肿、难以维护 —— 理解继承的规则,更要理解 “何时不用继承”,才是面向对象设计的关键。

Read more

AI生成图片R18提示词:新手入门指南与最佳实践

快速体验 在开始今天关于 AI生成图片R18提示词:新手入门指南与最佳实践 的探讨之前,我想先分享一个最近让我觉得很有意思的全栈技术挑战。 我们常说 AI 是未来,但作为开发者,如何将大模型(LLM)真正落地为一个低延迟、可交互的实时系统,而不仅仅是调个 API? 这里有一个非常硬核的动手实验:基于火山引擎豆包大模型,从零搭建一个实时语音通话应用。它不是简单的问答,而是需要你亲手打通 ASR(语音识别)→ LLM(大脑思考)→ TTS(语音合成)的完整 WebSocket 链路。对于想要掌握 AI 原生应用架构的同学来说,这是个绝佳的练手项目。 从0到1构建生产级别应用,脱离Demo,点击打开 从0打造个人豆包实时通话AI动手实验 AI生成图片R18提示词:新手入门指南与最佳实践 背景与痛点 对于刚接触AI生成图片的新手来说,使用R18提示词往往会遇到几个典型问题: 1. 内容合规性把控困难:多数平台对生成内容有严格限制,容易触发审核机制导致生成失败 2. 提示词效果不稳定:相同的提示词在不同模型或参数下可能产生差异巨大的结果

By Ne0inhk
“神经网络的奥秘”一篇带你读懂AI学习核心

“神经网络的奥秘”一篇带你读懂AI学习核心

引言:“神经网络的奥秘”一篇带你读懂AI学习核心 想学AI却卡在神经网络?这篇带你轻松突破核心难点! 如今打开手机,AI修图、智能推荐、语音助手随时待命;刷到科技新闻,自动驾驶、AI制药、大模型对话的进展不断刷新认知。而这一切AI能力的核心,都离不开一个关键技术——神经网络。 很多人把神经网络当成“高深黑箱”,觉得必须有深厚的数学功底才能理解。但其实,神经网络的核心逻辑和人类大脑的学习方式很相似,哪怕是非科班出身,也能通过通俗的解释搞懂它的运作原理。这篇文章就从“是什么、怎么学、用在哪”三个维度,带你彻底读懂神经网络,真正入门AI学习的核心。 * 引言:“神经网络的奥秘”一篇带你读懂AI学习核心 * 一、先搞懂基础:神经网络到底是什么? * 二、核心奥秘:神经网络是如何“学习”的? * 三、必懂概念:新手入门神经网络的5个关键术语 * 四、实际应用:神经网络在我们身边的5个场景 * 五、新手学习路径:从入门到实战的3个阶段

By Ne0inhk

Trae IDE 安装与使用保姆级教程:字节跳动的 AI 编程神器

一、Trae 是什么? Trae(发音 /treɪ/)是字节跳动推出的 AI 原生集成开发环境(AI IDE),于 2025 年 1 月正式发布。与传统的 IDE + AI 插件组合不同,Trae 从底层架构上就将 AI 能力深度集成,实现了真正意义上的"AI 主导开发"。 核心定位 Trae 以 “自主智能体(Agent)” 为核心定位,彻底重构了传统开发流程: * Chat 模式:智能代码补全、问答、解释和优化 * Builder 模式:自然语言一键生成完整项目框架 * SOLO 模式:AI 自主规划并执行开发任务 版本划分 版本定位核心特色适用人群Trae

By Ne0inhk
SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 SpringAI 概述         1.1 大模型的使用         2.0 SpringAI 新手入门         2.1 配置 pom.xml 文件         2.2 配置 application.yaml 文件         2.3 配置 ChatClient         2.4 同步调用         2.5 流式调用         2.6 System 设定         2.7 日志功能         2.8 会话记忆功能

By Ne0inhk