【C++】13. 继承
- 一、继承的概念及定义
- 1.1 继承的概念
- 1.2 继承定义
- 1.2.1 定义格式
- 1.2.2 继承关系和访问限定符
- 1.2.3 继承基类成员访问方式的变化
- 二、基类和派生类对象的赋值兼容转换
- 三、继承中的作用域
- 3.1 隐藏规则
- 3.2 考察继承作用域相关选择题
- 3.2.1 A和B类中的两个func构成什么关系(==B==)
- 3.2.2 上面程序的编译运行结果是什么(==A==)
- 四、派生类的默认成员函数
- 4.1 四个常见默认成员函数
- 4.1.1 ==派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。==
- 4.1.2 ==派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。==
- 4.1.3 ==派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。==
- 4.1.4 ==派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。==
- 4.1.5 派生类对象初始化先调用基类构造再调派生类构造。
- 4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。
- 4.2 实现一个不能被继承的类
- 五、继承与友元
- 六、继承与静态成员
- 七、多继承及其菱形继承问题
- 7.1 继承模型
- 7.1.1 单继承
- 7.1.2 多继承
- 7.1.3 菱形继承
- (1)菱形继承定义
- (2)菱形继承产生的问题 —— 二义性+数据冗余
- 7.2 虚继承
- (1)虚继承:解决 菱形继承产生二义性和数据冗余的问题
- (2)虚继承 解决数据冗余和二义性的原理
- 八、继承的总结和反思
一、继承的概念及定义
1.1 继承的概念
// 基类classPerson{protected:voidprint(){cout<<"name:"<<_name<<endl;cout<<"age:"<<_age<<endl;}protected:string _name="张三";// 姓名int_age=18;// 年龄};// 派生类classStudent:publicPerson{protected:int_stuid;// 学号};// 派生类classTeacher:publicPerson{protected:int_jobid;// 工号};intmain(){Person p;p.Print();Student s;s.print();Teacher t;t.print();return0;}继承后父类 Person 的成员(成员函数 + 成员变量)都会变成子类的一部分。
这里体现出了 Student 和 Teacher 复用了 Person 的成员。
1.2 继承定义
1.2.1 定义格式
1.2.2 继承关系和访问限定符
1.2.3 继承基类成员访问方式的变化
实例 演示三种继承关系下基类成员的各类型成员访问关系的变化:
// 基类classPerson{public:voidPrint(){cout<<_name<<endl;}protected:string _name;// 姓名private:int_age;// 年龄};// 派生类//class Student : protected Person//class Student : private PersonclassStudent:publicPerson{protected:int_stunum;// 学号};二、基类和派生类对象的赋值兼容转换
// 基类classPerson{protected:string _name;// 姓名string _sex;// 性别int_age;// 年龄};// 派生类classStudent:publicPerson{public:int_id;// 学号};voidTest(){Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj=sobj;Person*pp=&sobj;Person&rp=sobj;// 2.基类对象不能赋值给派生类对象sobj=pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp=&sobj Student*ps1=(Student*)pp;// 这种情况转换是可以的ps1->_No=10;pp=&pobj;Student*ps2=(Student*)pp;// 这种情况转换虽然可以,但是会存在越界访问的问题ps2->_No=10;}三、继承中的作用域
3.1 隐藏规则
// Student的_num和Person的_num构成隐藏关系,虽然这样的代码虽然能跑,但是非常容易混淆// 基类classPerson{protected:string _name="李四";// 姓名int_num=333444;// 身份证号};// 派生类classStudent:publicPerson{public:voidPrint(){cout<<"姓名:"<<_name<<endl;cout<<"身份证号:"<<Person::_num<<endl;// 类名::成员显示访问cout<<"学号:"<<_num<<endl;// 打印的是派生类的学号}protected:int_num=111222;// 学号};intmain(){Student s1;s1.Print();return0;};3.2 考察继承作用域相关选择题
classA{public:voidfun(){cout<<"func()"<<endl;}};classB:publicA{public:voidfun(inti){cout<<"func(int i)->"<<i<<endl;//改为://A::fun(); //类名::成员 还是可以访问的//cout << "func(int i)->" << i << endl;}};intmain(){B b;b.fun(10);return0;};3.2.1 A和B类中的两个func构成什么关系(B)
A. 重载 B.隐藏 C.没关系
3.2.2 上面程序的编译运行结果是什么(A)
A. 编译报错 B.运行报错 C.正常运行
四、派生类的默认成员函数
4.1 四个常见默认成员函数
4.1.1派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
4.1.2派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
4.1.3派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。
4.1.4派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
(因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(要符合先定义的先析构))
4.1.5 派生类对象初始化先调用基类构造再调派生类构造。
4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。
可以使用以下代码 验证上面的结论:
派生类的构造函数、拷贝构造、赋值重载都要手动调用基类的对应函数,只有析构函数不用(编译器自动调)
#include<iostream>#include<string>usingnamespacestd;// ========== 基类:Person(人) ==========// 基类定义了4个默认成员函数,演示它们的行为classPerson{public:// ===== 1. 构造函数(带缺省参数) =====// 参数name有默认值"Peter",所以可以不传参// 作用:初始化Person对象的_name成员Person(constchar*name="Peter"):_name(name)// 使用初始化列表,将name赋值给_name{cout<<"Person()"<<endl;// 打印表示构造函数被调用}// ===== 2. 拷贝构造函数 =====// 作用:用已有的Person对象p,拷贝构造一个新的Person对象// 参数是const引用,防止拷贝时无限递归Person(constPerson&p):_name(p._name)// 从p拷贝_name成员{cout<<"Person(const Person& p)"<<endl;// 打印表示拷贝构造被调用}// ===== 3. 赋值重载函数 =====// 作用:将p的值赋给当前对象(对象已存在)// 返回自身引用,支持链式赋值(a = b = c)Person&operator=(constPerson&p){cout<<"Person operator=(const Person& p)"<<endl;// 打印表示赋值被调用// 防止自己给自己赋值(例如:p1 = p1)if(this!=&p)// this是当前对象的地址,&p是参数对象的地址_name=p._name;// 将p的_name赋值给当前对象的_namereturn*this;// 返回当前对象(支持链式赋值)}// ===== 4. 析构函数 =====// 作用:Person对象销毁时自动调用,释放资源// 这里没有动态分配的资源,只打印表示被调用~Person(){cout<<"~Person()"<<endl;// 打印表示析构函数被调用}protected:string _name;// 姓名(protected:派生类可以访问)};// ========== 派生类:Student(学生) ==========// Student继承自Person,拥有Person的所有成员// 演示派生类的4个默认成员函数如何正确实现classStudent:publicPerson{public:// ===== 1. 派生类的构造函数 =====// 作用:初始化Student对象(包括基类部分和派生类自己的部分)// 参数:name传给基类构造,num初始化自己的_numStudent(constchar*name,intnum):Person(name)// ✅ 关键:显式调用基类构造函数,初始化继承来的_name,_num(num)// 初始化派生类自己的成员_num{cout<<"Student()"<<endl;// 打印表示派生类构造函数被调用// 注意:先调用基类构造,再执行派生类构造体}// ===== 2. 派生类的拷贝构造函数 =====// 作用:用已有的Student对象s,拷贝构造一个新的Student对象// 参数:const引用,接收被拷贝的对象Student(constStudent&s):Person(s)// ✅ 关键:显式调用基类拷贝构造// 这里发生"切片":把s当作Person用,只拷贝基类部分(_name),_num(s._num)// 拷贝派生类自己的成员_num{cout<<"Student(const Student& s)"<<endl;// 打印表示派生类拷贝构造被调用// 注意:先调用基类拷贝构造,再执行派生类拷贝构造体}// ===== 3. 派生类的赋值重载函数 =====// 作用:将s的值赋给当前Student对象// 返回自身引用,支持链式赋值Student&operator=(constStudent&s){cout<<"Student& operator= (const Student& s)"<<endl;// 打印表示派生类赋值被调用// 防止自己给自己赋值if(this!=&s){// ✅ 关键:显式调用基类的赋值重载函数// 这里发生"切片":把s当作Person用,只处理基类部分(_name)Person::operator=(s);// 赋值派生类自己的成员_num_num=s._num;}return*this;// 返回当前对象}// ===== 4. 派生类的析构函数 =====// 作用:Student对象销毁时自动调用~Student(){cout<<"~Student()"<<endl;// 打印表示派生类析构被调用// ✅ 关键:不需要手动调用基类析构!// 执行完派生类析构体后,编译器会自动调用基类的析构函数}protected:int_num;// 学号(派生类自己的成员)};// ========== 主函数:测试所有默认成员函数 ==========intmain(){// ===== 测试1:构造函数 =====// 创建s1,调用构造函数// 执行顺序:Person() → Student()Students1("Jack",18);// ===== 测试2:拷贝构造函数 =====// 用s1拷贝构造s2// 执行顺序:Person(const Person&) → Student(const Student&)Students2(s1);// ===== 测试3:构造函数 =====// 创建s3// 执行顺序:Person() → Student()Students3("Rose",17);// ===== 测试4:赋值重载函数 =====// 将s3赋值给s1// 执行顺序:Student::operator= → Person::operator=s1=s3;// ===== main结束,所有对象析构 =====// 析构顺序:后创建的先析构(s3 → s2 → s1)// 每个对象的析构顺序:~Student() → ~Person()return0;}4.2 实现一个不能被继承的类
方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
// C++11的⽅法classBasefinal{public:voidfunc5(){cout<<"Base::func5"<<endl;}protected:inta=1;private:// C++98的⽅法/*Base() {}*/};classDerive:publicBase{voidfunc4(){cout<<"Derive::func4"<<endl;}protected:intb=2;};intmain(){Base b;Derive d;return0;}五、继承与友元
// 前置声明:告诉编译器Student是一个类,后面会定义classStudent;// ========== 基类:Person ==========classPerson{public:// 声明Display函数是Person的友元// 友元函数可以访问Person类的私有/保护成员friendvoidDisplay(constPerson&p,constStudent&s);protected:string _name;// 姓名(保护成员)};// ========== 派生类:Student ==========classStudent:publicPerson{protected:int_stuNum;// 学号(保护成员)// 注意:Student没有声明Display为友元};// ========== 全局函数:Display ==========voidDisplay(constPerson&p,constStudent&s){// ✅ 可以访问:Display是Person的友元cout<<p._name<<endl;// ❌ 报错:Display不是Student的友元// 友元关系不继承!即使Display是Person的友元// 也不能访问Student的protected/private成员cout<<s._stuNum<<endl;// 编译错误:不可访问}intmain(){Person p;Student s;Display(p,s);// 调用Display函数return0;}六、继承与静态成员
//无论继承多少层,大家用的始终是同一个变量// 基类classPerson{public:Person(){++_count;}protected:string _name;// 姓名public:staticint_count;// 统计人的个数};intPerson::_count=0;classStudent:publicPerson{protected:int_stuNum;// 学号};classGraduate:publicStudent{protected:string _seminarCourse;// 研究科目};voidTestPerson(){Student s1;Student s2;Student s3;Graduate s4;cout<<" 人数 :"<<Person::_count<<endl;Student::_count=0;cout<<" 人数 :"<<Person::_count<<endl;}七、多继承及其菱形继承问题
7.1 继承模型
7.1.1 单继承
7.1.2 多继承
7.1.3 菱形继承
(1)菱形继承定义
(2)菱形继承产生的问题 —— 二义性+数据冗余
classPerson{public:string _name;// 姓名};// Student继承PersonclassStudent:publicPerson{protected:int_num;// 学号};// Teacher继承PersonclassTeacher:publicPerson{protected:int_id;// 职工编号};// Assistant同时继承Student和Teacher(菱形继承)// 助教:既是学生又是老师classAssistant:publicStudent,publicTeacher{protected:string _majorCourse;// 主修课程};intmain(){Assistant a;// ❌ 二义性:_name不知道是从Student继承来的,还是从Teacher继承来的// 因为Student和Teacher都有一份从Person继承来的_namea._name="Peter";// 编译报错:ambiguous(二义性)// ✅ 解决二义性:指定从哪个路径访问// 但数据冗余问题依然存在(内存中有两份_name)a.Student::_name="xxx";// 访问从Student继承来的_namea.Teacher::_name="yyy";// 访问从Teacher继承来的_name// 此时内存中有两份_name:// a.Student::_name = "xxx"// a.Teacher::_name = "yyy"// 浪费内存,且容易混淆return0;}7.2 虚继承
(1)虚继承:解决 菱形继承产生二义性和数据冗余的问题
classPerson{public:string _name;// 姓名};// Student虚继承Person,解决菱形继承的数据冗余问题classStudent:virtualpublicPerson{protected:int_num;// 学号};// Teacher虚继承Person,解决菱形继承的数据冗余问题classTeacher:virtualpublicPerson{protected:int_id;// 职工编号};// Assistant同时继承Student和Teacher// 由于Student和Teacher都是虚继承Person,所以Person部分只有一份classAssistant:publicStudent,publicTeacher{protected:string _majorCourse;// 主修课程};intmain(){Assistant a;// ✅ 可以直接访问,因为_name只有一份(虚继承解决了数据冗余)a._name="Peter";// 通过不同路径访问,实际上是同一个变量// 因为虚继承让Person部分只有一份a.Student::_name="xxx";// 通过Student路径访问a.Teacher::_name="yyy";// 通过Teacher路径访问// 最终a._name的值是"yyy"// 因为Student::_name和Teacher::_name指向同一块内存// 后赋值的"yyy"覆盖了之前的"xxx"return0;}(2)虚继承 解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成 员的模型。
classA{public:int_a;};// class B : public AclassB:virtualpublicA{public:int_b;};// class C : public AclassC:virtualpublicA{public:int_c;};classD:publicB,publicC{public:int_d;};intmain(){D d;d.B::_a=1;d.C::_a=2;d._b=3;d._c=4;d._d=5;return0;}下图是菱形继承的内存对象成员模型:这里可以看到数据冗余。
——————————————————————————————————————————————
下面是上面的 Person 关系菱形虚拟继承的原理解释:
八、继承的总结和反思
// Car和BMW Car和Benz构成is-a的关系classCar{protected:string _colour="白色";// 颜色string _num="粤DK0896";// 车牌号};classBMW:publicCar{public:voidDrive(){cout<<"好开-操控"<<endl;}};classBenz:publicCar{public:voidDrive(){cout<<"好坐-舒适"<<endl;}};// Tire和Car构成has-a的关系classTire{protected:string _brand="Michelin";// 品牌size_t _size=17;// 尺寸};classCar{protected:string _colour="白色";// 颜色string _num="粤DK0896";// 车牌号Tire _t;// 轮胎};