【C++】13. 继承

📅 2026/8/14 16:52:52
【C++】13. 继承
【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构成什么关系B3.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(){coutname:_nameendl;coutage:_ageendl;}protected:string _name张三;// 姓名int_age18;// 年龄};// 派生类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_nameendl;}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 pobjsobj;Person*ppsobj;Personrpsobj;// 2.基类对象不能赋值给派生类对象sobjpobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针ppsobj Student*ps1(Student*)pp;// 这种情况转换是可以的ps1-_No10;pppobj;Student*ps2(Student*)pp;// 这种情况转换虽然可以但是会存在越界访问的问题ps2-_No10;}三、继承中的作用域3.1 隐藏规则// Student的_num和Person的_num构成隐藏关系虽然这样的代码虽然能跑但是非常容易混淆// 基类classPerson{protected:string _name李四;// 姓名int_num333444;// 身份证号};// 派生类classStudent:publicPerson{public:voidPrint(){cout姓名:_nameendl;cout身份证号:Person::_numendl;// 类名::成员显示访问cout学号:_numendl;// 打印的是派生类的学号}protected:int_num111222;// 学号};intmain(){Student s1;s1.Print();return0;};3.2 考察继承作用域相关选择题classA{public:voidfun(){coutfunc()endl;}};classB:publicA{public:voidfun(inti){coutfunc(int i)-iendl;//改为//A::fun(); //类名::成员 还是可以访问的//cout func(int i)- i endl;}};intmain(){B b;b.fun(10);return0;};3.2.1 A和B类中的两个func构成什么关系BA. 重载 B.隐藏 C.没关系3.2.2 上面程序的编译运行结果是什么AA. 编译报错 B.运行报错 C.正常运行四、派生类的默认成员函数4.1 四个常见默认成员函数4.1.1派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。4.1.2派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。4.1.3派生类的 operator 必须要调用基类的 operator 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。4.1.4派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。要符合先定义的先析构4.1.5 派生类对象初始化先调用基类构造再调派生类构造。4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。可以使用以下代码 验证上面的结论派生类的构造函数、拷贝构造、赋值重载都要手动调用基类的对应函数只有析构函数不用编译器自动调#includeiostream#includestringusingnamespacestd;// 基类Person人 // 基类定义了4个默认成员函数演示它们的行为classPerson{public:// 1. 构造函数带缺省参数 // 参数name有默认值Peter所以可以不传参// 作用初始化Person对象的_name成员Person(constchar*namePeter):_name(name)// 使用初始化列表将name赋值给_name{coutPerson()endl;// 打印表示构造函数被调用}// 2. 拷贝构造函数 // 作用用已有的Person对象p拷贝构造一个新的Person对象// 参数是const引用防止拷贝时无限递归Person(constPersonp):_name(p._name)// 从p拷贝_name成员{coutPerson(const Person p)endl;// 打印表示拷贝构造被调用}// 3. 赋值重载函数 // 作用将p的值赋给当前对象对象已存在// 返回自身引用支持链式赋值a b cPersonoperator(constPersonp){coutPerson operator(const Person p)endl;// 打印表示赋值被调用// 防止自己给自己赋值例如p1 p1if(this!p)// this是当前对象的地址p是参数对象的地址_namep._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{coutStudent()endl;// 打印表示派生类构造函数被调用// 注意先调用基类构造再执行派生类构造体}// 2. 派生类的拷贝构造函数 // 作用用已有的Student对象s拷贝构造一个新的Student对象// 参数const引用接收被拷贝的对象Student(constStudents):Person(s)// ✅ 关键显式调用基类拷贝构造// 这里发生切片把s当作Person用只拷贝基类部分_name,_num(s._num)// 拷贝派生类自己的成员_num{coutStudent(const Student s)endl;// 打印表示派生类拷贝构造被调用// 注意先调用基类拷贝构造再执行派生类拷贝构造体}// 3. 派生类的赋值重载函数 // 作用将s的值赋给当前Student对象// 返回自身引用支持链式赋值Studentoperator(constStudents){coutStudent operator (const Student s)endl;// 打印表示派生类赋值被调用// 防止自己给自己赋值if(this!s){// ✅ 关键显式调用基类的赋值重载函数// 这里发生切片把s当作Person用只处理基类部分_namePerson::operator(s);// 赋值派生类自己的成员_num_nums._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::operators1s3;// main结束所有对象析构 // 析构顺序后创建的先析构s3 → s2 → s1// 每个对象的析构顺序~Student() → ~Person()return0;}4.2 实现一个不能被继承的类方法1基类的构造函数私有派生类的构成必须调用基类的构造函数但是基类的构成函数私有化以后派生类看不见就不能调用了那么派生类就无法实例化出对象。方法2C11新增了⼀个final关键字final修改基类派生类就不能继承了。// C11的⽅法classBasefinal{public:voidfunc5(){coutBase::func5endl;}protected:inta1;private:// C98的⽅法/*Base() {}*/};classDerive:publicBase{voidfunc4(){coutDerive::func4endl;}protected:intb2;};intmain(){Base b;Derive d;return0;}五、继承与友元// 前置声明告诉编译器Student是一个类后面会定义classStudent;// 基类Person classPerson{public:// 声明Display函数是Person的友元// 友元函数可以访问Person类的私有/保护成员friendvoidDisplay(constPersonp,constStudents);protected:string _name;// 姓名保护成员};// 派生类Student classStudent:publicPerson{protected:int_stuNum;// 学号保护成员// 注意Student没有声明Display为友元};// 全局函数Display voidDisplay(constPersonp,constStudents){// ✅ 可以访问Display是Person的友元coutp._nameendl;// ❌ 报错Display不是Student的友元// 友元关系不继承即使Display是Person的友元// 也不能访问Student的protected/private成员couts._stuNumendl;// 编译错误不可访问}intmain(){Person p;Student s;Display(p,s);// 调用Display函数return0;}六、继承与静态成员//无论继承多少层大家用的始终是同一个变量// 基类classPerson{public:Person(){_count;}protected:string _name;// 姓名public:staticint_count;// 统计人的个数};intPerson::_count0;classStudent:publicPerson{protected:int_stuNum;// 学号};classGraduate:publicStudent{protected:string _seminarCourse;// 研究科目};voidTestPerson(){Student s1;Student s2;Student s3;Graduate s4;cout 人数 :Person::_countendl;Student::_count0;cout 人数 :Person::_countendl;}七、多继承及其菱形继承问题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._namePeter;// 编译报错ambiguous二义性// ✅ 解决二义性指定从哪个路径访问// 但数据冗余问题依然存在内存中有两份_namea.Student::_namexxx;// 访问从Student继承来的_namea.Teacher::_nameyyy;// 访问从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._namePeter;// 通过不同路径访问实际上是同一个变量// 因为虚继承让Person部分只有一份a.Student::_namexxx;// 通过Student路径访问a.Teacher::_nameyyy;// 通过Teacher路径访问// 最终a._name的值是yyy// 因为Student::_name和Teacher::_name指向同一块内存// 后赋值的yyy覆盖了之前的xxxreturn0;}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::_a1;d.C::_a2;d._b3;d._c4;d._d5;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 _brandMichelin;// 品牌size_t _size17;// 尺寸};classCar{protected:string _colour白色;// 颜色string _num粤DK0896;// 车牌号Tire _t;// 轮胎};