【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

C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展 💡 学习目标:掌握运算符重载的核心语法与规则,能够为自定义类型重载常用运算符,实现类对象的灵活运算。 💡 学习重点:运算符重载的基本形式、成员函数与全局函数重载的区别、常见运算符的重载实现、禁止重载的运算符。 一、运算符重载的概念与核心价值 ✅ 结论:运算符重载是 C++ 静态多态的重要体现,允许为自定义类型(如类、结构体)重新定义运算符的行为,让自定义对象可以像内置类型一样使用运算符。 运算符重载的核心价值: 1. 简化代码书写:用直观的运算符替代繁琐的成员函数调用,提升代码可读性 2. 统一操作风格:让自定义类型的运算逻辑与内置类型保持一致,降低学习和使用成本 3. 扩展类型功能:根据业务需求定制运算符的行为,满足自定义类型的运算需求 ⚠️ 注意事项:运算符重载不会改变运算符的优先级和结合性,也不会改变运算符的操作数个数。 二、运算符重载的基本语法 运算符重载的本质是函数重载,分为成员函数重载和全局函数重载两种形式。 2.1 成员函数重载语法 将运算符重载函数定义为类的成员函数,语法格式如下: class

By Ne0inhk
C++学习之旅【C++伸展树介绍以及红黑树的实现】

C++学习之旅【C++伸展树介绍以及红黑树的实现】

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》《数据结构与算法》 《C++知识内容》《Linux系统知识》 ✨逆境不吐心中苦,顺境不忘来时路!🎬 博主简介: 引言:前篇文章,小编已经介绍了关于C++AVL树的实现!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于C++伸展树介绍以及红黑树的实现!伸展树与红黑树是两类极具代表性的BBST,且在工程实践中各有不可替代的价值:伸展树摒弃了"严格平衡”的执念,通过“伸展”操作将最近访问的节点移至根节点,利用“局部性原理”优化频繁访问的场景,实现均摊O(logn)的时间复杂度,适合缓存、热点数据查询等场景;红黑树则通过给节点着色并遵守严格的颜色规则,确保树的最长路径不超过最短路径的两倍,以 “弱平衡” 换稳定的最坏O(logn)性能,是C++ STL 中 std::map、std:

By Ne0inhk
【C++】C++中内存管理的利器“智能指针”

【C++】C++中内存管理的利器“智能指针”

各位大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页:落羽的落羽 文章目录 * 一、智能指针的场景需求 * 二、智能指针的设计思路 * 三、C++标准库中的智能指针 * 1. auto_ptr * 2. unique_ptr * 3. shared_ptr * 4. weak_ptr * 5. 特殊说明 * 四、智能指针的实现原理 * 五、shared_ptr的循环引用问题与weak_ptr的使用 一、智能指针的场景需求 如果一个程序中手动new了对象,申请了空间资源,然后下面抛出了异常,就会导致申请的资源没有手动释放,造成内存泄露了。我们就需要在捕捉到异常后在catch语句里先delete资源。可是,new本身也可能抛异常的,导致我们处理起来就会很麻烦。智能指针在这样的场景下处理就十分轻松了。 doubledivide(int

By Ne0inhk
《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——构造正/倒排索引(中篇)》

《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——构造正/倒排索引(中篇)》

前引:这是一个聚焦基础搜索引擎核心工作流的实操项目,基于 C/C++ 技术生态落地:从全网爬虫抓取网页资源,到服务器端完成 “去标签 - 数据清洗 - 索引构建” 的预处理,再通过 HTTP 服务接收客户端请求、检索索引并拼接结果页返回 —— 完整覆盖了轻量级搜索引擎的端到端逻辑。项目采用 C++11、STL、Boost 等核心技术栈,搭配 CentOS 7 云服务器 + GCC 编译环境(或 VS 系列开发工具)部署,既适配后端工程的性能需求,也能通过可选的前端技术(HTML5/JS 等)优化用户交互,是理解搜索引擎底层原理与 C++ 工程实践的典型案例 目录 【一】Jieba分词工具 【二】正/倒排索引结构设计

By Ne0inhk