【C++游记】子承父业——乃继承也
枫の个人主页
你不能改变过去,但你可以改变未来
算法/C++/数据结构/C
Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记】
话不多说,让我们一起进入今天的学习吧~~~
一、继承的概念及核心价值
1.1 继承的本质:类层次的代码复用
继承(inheritance)允许在保持原有类(基类/父类)特性的基础上,扩展新的方法(成员函数)和属性(成员变量),生成新类(派生类/子类)。它区别于函数层次的复用,是面向对象程序设计中"由简单到复杂"认知过程的体现,能有效减少代码冗余。
1.2 继承的优化:一个爸爸
#include <iostream>
#include <string>
using namespace std;// 基类:提取Student和Teacher的共性
class Person
{
public:// 身份认证(复用)void identity() {cout << _name << "身份认证通过" << endl;}
protected:string _name = "张三"; // 姓名(复用)string _address; // 地址(复用)string _tel; // 电话(复用)int _age = 18; // 年龄(复用)
};// 学生类:继承Person,仅定义独有成员
class Student : public Person
{
public:void study() {cout << _name << "(学生)学习中" << endl;}
protected:int _stuid; // 学生独有:学号
};// 教师类:继承Person,仅定义独有成员
class Teacher : public Person
{
public:void teaching() {cout << _name << "(教师)授课中" << endl;}
protected:string _title; // 教师独有:职称
};// 测试:复用基类方法,调用派生类独有方法
int main()
{Student s;Teacher t;// 复用基类identity()方法s.identity(); t.identity(); // 调用派生类独有方法s.study(); t.teaching(); return 0;// 输出结果:// 张三身份认证通过// 张三身份认证通过// 张三(学生)学习中// 张三(教师)授课中
}
二、继承的定义格式与访问权限控制
2.1 继承的定义格式
派生类定义需指定"基类"和"继承方式",语法格式如下:
class 派生类名 : 继承方式 基类名
{// 派生类成员(独有属性/方法)
};
其中:
- 基类/派生类:基类(Base Class)也称父类,派生类(Derived Class)也称子类(因翻译差异两种称呼通用);
- 继承方式:包括public(公继承)、protected(保护继承)、private(私有继承);
- 默认继承方式:使用class定义类时默认private继承,使用struct时默认public继承,建议显式指定继承方式。
2.2 继承方式对成员访问权限的影响
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
- 基类private成员"不可见":指成员虽被继承到派生类对象中,但语法限制派生类(类内/类外)均无法直接访问,需通过基类的public/protected接口间接访问;
- protected限定符的意义:若基类成员需"类外不可访问、派生类内可访问",则定义为protected,这是专为继承设计的访问限定符;
- 实际应用建议:几乎只使用public继承,protected/private继承会限制派生类扩展性,维护成本高。
三、基类与派生类的对象转换规则
3.1 允许的转换:向上转换(派生类→基类)
派生类对象可以赋值给基类的指针、引用或对象,此过程称为"切片/切割"——即仅取派生类中基类对应的部分,丢弃派生类独有成员。实例如下:
#include <iostream>
#include <string>
using namespace std;class Person
{
protected:string _name = "张三"; // 姓名int _age = 18; // 年龄
public:void showInfo() {cout << "姓名:" << _name << ",年龄:" << _age << endl;}
};class Student : public Person
{
public:int _No = 2024001; // 学号(独有)void showStuInfo() {cout << "学号:" << _No << ",姓名:" << _name << endl;}
};int main()
{Student sobj;// 1. 派生类对象 → 基类指针Person* pp = &sobj;pp->showInfo(); // 调用基类方法,仅访问基类成员// 2. 派生类对象 → 基类引用Person& rp = sobj;rp.showInfo(); // 调用基类方法// 3. 派生类对象 → 基类对象(调用基类拷贝构造)Person pobj = sobj;pobj.showInfo(); // 仅包含基类成员return 0;// 输出结果:// 姓名:张三,年龄:18// 姓名:张三,年龄:18// 姓名:张三,年龄:18
}
3.2 禁止的转换:向下转换(基类→派生类)
基类对象不能直接赋值给派生类对象,因为基类不包含派生类的独有成员,无法完成派生类对象的完整初始化,编译会直接报错。实例如下:
int main()
{Person pobj;Student sobj;// sobj = pobj; // 编译报错:error C2679: 二进制"=": 没有找到接受"Person"类型的右操作数的运算符return 0;
}
四、继承中的作用域与隐藏规则
基类和派生类拥有独立的作用域,当两者存在同名成员时,会触发"隐藏"规则(也称"重定义"),这是继承中易混淆的核心知识点之一。
4.1 隐藏规则的核心内容
- 基类和派生类的作用域相互独立;
- 派生类与基类有同名成员时,派生类成员会屏蔽基类同名成员的直接访问,需通过基类名::基类成员显式访问;
- 成员函数的隐藏:仅需"函数名相同"即构成隐藏,无需参数列表或返回值匹配(与"重载"区分:重载要求同一作用域、参数列表不同);
- 实际建议:继承体系中尽量不定义同名成员,避免混淆。
4.2 隐藏规则实例演示
#include <iostream>
#include <string>
using namespace std;class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111; // 基类:身份证号
};class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl; // 无隐藏,访问基类_namecout << "身份证号:" << Person::_num << endl;// 显式访问基类_num(隐藏)cout << "学号:" << _num << endl; // 访问派生类_num(隐藏基类)}
protected:int _num = 999; // 派生类:学号(与基类_num同名,触发隐藏)
};int main()
{Student s1;s1.Print();// 输出结果:// 姓名:小李子// 身份证号:111// 学号:999return 0;
}
4.3 常见误区:函数隐藏 vs 函数重载
函数隐藏与重载的核心区别在于"作用域":隐藏发生在不同作用域(基类vs派生类),重载发生在同一作用域。实例如下:
#include <iostream>
using namespace std;class A
{
public:void fun() { cout << "A::func()" << endl; } // 基类函数
};class B : public A
{
public:// 函数名相同,参数不同,构成隐藏(非重载)void fun(int i) { cout << "B::func(int i): " << i << endl; }
};int main()
{B b;b.fun(10); // 正确:调用B::fun(int)// b.fun(); // 编译报错:B::fun(int)隐藏了A::fun()b.A::fun(); // 正确:显式访问基类fun()return 0;
}
五、派生类的默认成员函数实现规则
C++类有6个默认成员函数(构造、析构、拷贝构造、赋值重载、取地址重载、const取地址重载),其中前4个在派生类中需特殊处理,核心原则是"先初始化基类,后清理派生类"。
5.1 派生类默认成员函数的核心规则
- 构造函数:派生类构造必须调用基类构造初始化基类部分;若基类无默认构造(无参/全缺省),需在派生类构造的初始化列表显式调用基类构造;
- 拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类部分的拷贝初始化;
- 赋值运算符重载:派生类赋值重载必须调用基类赋值重载完成基类部分的赋值;
- 析构函数:派生类析构函数执行完后,编译器会自动调用基类析构函数(先清理派生类,后清理基类)。
5.2 派生类默认成员函数实现示例
#include <iostream>
#include <string>
using namespace std;class Person
{
public:// 基类构造函数Person(const char* name = "peter") : _name(name) {cout << "Person()" << endl;}// 基类拷贝构造Person(const Person& p) : _name(p._name) {cout << "Person(const Person& p)" << endl;}// 基类赋值重载Person& operator=(const Person& p) {if (this != &p) {_name = p._name;}cout << "Person::operator=" << endl;return *this;}// 基类析构函数~Person() {cout << "~Person()" << endl;}
protected:string _name;
};class Student : public Person
{
public:// 派生类构造:初始化列表显式调用基类构造Student(const char* name, int num) : Person(name), _num(num) {cout << "Student()" << endl;}// 派生类拷贝构造:显式调用基类拷贝构造Student(const Student& s) : Person(s), _num(s._num) {cout << "Student(const Student& s)" << endl;}// 派生类赋值重载:显式调用基类赋值重载Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s); // 调用基类赋值_num = s._num;}cout << "Student::operator=" << endl;return *this;}// 派生类析构:编译器自动调用基类析构~Student() {cout << "~Student()" << endl;}
protected:int _num; // 学号
};// 测试:初始化与清理顺序
int main()
{Student s1("jack", 18); // 构造顺序:Person() → Student()Student s2(s1); // 拷贝构造顺序:Person拷贝 → Student拷贝Student s3("rose", 17);s1 = s3; // 赋值顺序:Person赋值 → Student赋值return 0; // 析构顺序:~Student() → ~Person()(与构造顺序相反)
}
六、继承中的特殊场景处理
6.1 继承与友元:友元关系不可继承
基类的友元不能访问派生类的私有/保护成员,友元关系仅存在于声明它的类与友元之间,不传递给派生类。爸爸的朋友不是我的朋友。
6.2 继承与静态成员:静态成员全局唯一
基类定义的static成员,在整个继承体系中仅存在一份实例,派生类与基类共享该静态成员。
七、多继承与菱形继承问题解析
7.1 多继承的概念
- 单继承:一个派生类仅有一个直接基类(如:A:public B);
- 多继承:一个派生类有多个直接基类(如:A:public B,public C);
7.2 菱形继承的问题
菱形继承是多继承的特殊情况,表现为"一个派生类的两个直接基类继承自同一个间接基类",会导致数据冗余和二义性。
7.3 菱形虚拟继承:解决数据冗余和二义性
C++通过虚拟继承(virtual inheritance)解决菱形继承问题,在直接基类继承间接基类时使用virtual关键字,使间接基类在继承体系中只保留一份实例。
#include <iostream>
#include <string>
using namespace std;// 间接基类
class Person {
public:string _name; // 姓名
};// 虚拟继承:Student -> Person
class Student : virtual public Person {
protected:int _num; // 学号
};// 虚拟继承:Teacher -> Person
class Teacher : virtual public Person {
protected:int _id; // 职工号
};// 派生类:Assistant继承Student和Teacher
class Assistant : public Student, public Teacher {
protected:string _major; // 主修课程
};int main() {Assistant a;a._name = "peter"; // 正确:无二义性(仅一份_name)return 0;
}
虚拟继承的实现原理:
虚拟继承通过引入"虚基表"和"虚基指针"实现,派生类对象中不再直接包含间接基类的成员,而是通过指针访问共享的间接基类实例,从而解决数据冗余问题。注意:虚拟继承仅在菱形继承场景使用,普通继承无需使用,否则会增加内存和时间开销!
八、继承与组合的选择原则
继承和组合都是代码复用的重要方式,各有适用场景,核心区别在于:
- 继承:体现"is-a"关系(如"学生是一个人"),是一种强耦合关系,派生类依赖基类的实现,基类变化会影响派生类;
- 组合:体现"has-a"关系(如"汽车有一个发动机"),是一种弱耦合关系,类通过包含其他类的对象实现功能,不依赖其内部实现。
8.1 组合的优势与应用场景
实际开发中优先使用组合,因为它具有更低的耦合度和更高的灵活性。示例如下:
// 发动机类
class Engine {
public:void run() { cout << "发动机启动" << endl; }void stop() { cout << "发动机关闭" << endl; }
};// 汽车类(组合发动机,体现"has-a"关系)
class Car {
private:Engine _engine; // 组合发动机对象
public:void drive() {_engine.run(); // 使用发动机功能cout << "汽车行驶中" << endl;}void park() {_engine.stop(); // 使用发动机功能cout << "汽车已停车" << endl;}
};int main() {Car c;c.drive();c.park();return 0;
}
九、总结
- 继承的核心价值是实现类层次的代码复用,减少冗余;
- 访问权限由基类成员限定符和继承方式共同决定,实际开发中优先使用public继承;
- 基类与派生类的转换遵循"向上兼容"原则,向下转换需谨慎;
- 同名成员会触发隐藏规则,继承体系中应避免定义同名成员;
- 派生类默认成员函数需正确处理基类部分的初始化与清理;
- 菱形继承问题通过虚拟继承解决,但应尽量避免复杂的多继承结构;
- 设计时优先考虑组合,仅在必要时使用继承,遵循"高内聚低耦合"原则。
十、结语
今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~