尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【C++】继承|从单继承到菱形虚拟继承,小白入门必看

【C++】继承|从单继承到菱形虚拟继承,小白入门必看 文章目录前言一、为什么要有继承二、继承怎么写三种继承方式有什么区别1、基本语法2、子类继承父类之后成员的访问权限怎么改变三、切片切割子类对象赋值给父类四、隐藏重定义父子类有同名成员怎么办1、成员变量的隐藏2、成员函数的隐藏五、派生类的默认成员函数重点1、构造函数2、拷贝构造3、赋值运算符重载4、析构函数5、完整代码流程6、怎么写一个不能被继承的类六、继承里的两个小知识点1、友元关系不能继承2、静态成员全体系共享一份七、多继承和菱形继承难点1、什么是多继承2、菱形继承是什么有什么问题3、虚拟继承解决菱形继承八、继承和组合1、两种关系2、优先推荐组合的原因总结前言我们敲代码经常遇到这样一种情况写一个教学管理系统我们已经写了教师 Teacher 类里面有姓名、年龄、学院、工号这些后面又要写学生 Student 类发现里面也需要有姓名、年龄……等。两个类内容大差不大难道要复制粘贴改一改那也太蠢了以后改个东西还得改好几处。继承就是来解决这个问题的 —— 把公共的东西抽出来放父类里子类直接拿来用自己只写与父类不同的部分。所以继承的本质是子承父业代码复用。一、为什么要有继承先看没有继承的时候有多难受。假设要写 Student 和 Teacher 两个类classStudent{public:voididentity(){/* 刷脸进校园 */}voidstudy(){/* 学习 */}protected:string _name;// 姓名string _address;// 地址string _tel;// 电话int_age;// 年龄int_stuid;// 学号};classTeacher{public:voididentity(){/* 刷脸进校园 */}voidteaching(){/* 授课 */}protected:string _name;// 姓名——跟Student重复了string _address;// 地址——重复string _tel;// 电话——重复int_age;// 年龄——重复string _title;// 职称};姓名、地址、电话、年龄还有identity()函数两个类里写了两遍。以后要是identity()逻辑改了我们还得改两个地方漏一个就会出bug。先了解术语基类又叫父类被继承的类比如下面的 Person派生类又叫子类继承别人的类比如下面的 Student、Teacher用继承就方便多了。把公共的抽到 Person 父类里classPerson{public:voididentity(){cout身份认证_nameendl;}protected:string _name张三;string _address;string _tel;int_age18;};// 学生继承人类classStudent:publicPerson// 继承的语法格式public也可以换成 protected、private{public:voidstudy(){/* 学习 */}protected:int_stuid;// 学号学生独有};// 老师也继承人类classTeacher:publicPerson// 继承的语法格式public也可以换成 protected、private{public:voidteaching(){/* 授课 */}protected:string _title;// 职称老师独有};intmain(){Student s;Teacher t;s.identity();// 直接用父类的不用自己写t.identity();return0;}公共的写一遍就行子类直接继承过来用也就是子类里面其实也有 Person 父类中的成员变量和成员函数。二、继承怎么写三种继承方式有什么区别1、基本语法class子类名:继承方式 父类名{// 子类自己的成员};继承方式有三种public、protected、private。基类本身成员有三种权限public公开任意地方都能访问protected保护本类 子类可以访问外部不能访问private私有只有本类内部可以访问子类、外部都不能直接访问重点基类成员本身的访问属性 子类继承方式 (public/protected/private)两个一起决定派生类里面、以及外部对象能怎么访问。2、子类继承父类之后成员的访问权限怎么改变子类里成员的最终权限 父类中权限和继承方式取更严格的那个。权限宽松程度public protected private举个例子父类里是 public 的成员你用 protected 继承取更严格的 protected所以到子类里成员就变成 protected 了。因此我们就可以做如下表格父类成员 / 继承方式public 继承protected 继承private 继承publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可见不可见不可见这里有几个重点第一父类的 private 成员子类是 “不可见” 的。注意不可见不是说没继承下来而是继承了但你访问不到。就像你爸把存折锁保险柜里了钱是家里的但你拿不到。第二protected 这个关键字就是为继承而生的。如果你想让成员在类外面不能访问但子类里能访问那就用 protected。private 子类访问不到public 又太暴露了protected 刚好卡在中间。这里的 public/protected/private 指的是访问限定符也就是父类内部的 public/protected/private不要和继承方式搞混了。第三实际开发中基本都用 public 继承方式。protected 和 private 继承很少见继承下来的东西只能在子类里用扩展性很差。除非有特殊需求否则一律 public 就完事了。#includeiostreamusingnamespacestd;classPerson{public:voidPrint(){cout_nameendl;}protected:string _name继承;private:int_age;// 子类访问不到};classStudent:publicPerson// public 继承{public:// Print()函数是public的voidtest(){cout_nameendl;// 继承过来的 protected// cout _age endl; // 报错_age 不可见}protected:int_stunum;};intmain(){Student s;s.Print();// Print是 publics.test();// test是 public// s._name; // _name是 protected外部不能访问// s._stunum; // _stunum是 Student自己的 protected外部不能访问return0;}三、切片切割子类对象赋值给父类public 继承下子类对象可以直接赋值给父类的指针、引用、对象。这个过程叫切片或者切割。为啥叫切片可以这样理解子类对象里面包含了两部分从父类继承来的那部分加上自己独有的那部分。赋值给父类的时候相当于把子类对象里属于父类的那一块 “切” 出来给父类子类独有的部分直接丢掉了。#includeiostreamusingnamespacestd;classPerson{protected:string _name;int_age;};classStudent:publicPerson{public:int_No;// 学号};intmain(){Student sobj;// 下面三种都可以都是切片Person*ppsobj;// 子类对象地址给父类指针Personrpsobj;// 子类对象给父类引用Person pobjsobj;// 子类对象给父类对象调用父类拷贝构造// 反过来不行// sobj pobj; // 编译报错父类对象没有子类的学号等成员return0;}子类可以给父类父类不能给子类。因为子类是 “大” 的包含了父类所有东西父类是 “小” 的缺了子类独有的部分赋值过去也填不满。四、隐藏重定义父子类有同名成员怎么办父类和子类是两个独立的作用域。如果子类里定义了跟父类同名的成员子类的成员会把父类的同名成员 “藏” 起来直接访问默认访问子类的这就叫隐藏也叫重定义。总结就是只要子类作用域里出现和父类同名的名字变量名 / 函数名就会构成隐藏和返回值类型、参数列表无关。1、成员变量的隐藏想访问父类被隐藏的成员加个父类名::就行。#includeiostreamusingnamespacestd;classPerson{protected:string _name柿子酱;int_num111;// 身份证号};classStudent:publicPerson{public:voidPrint(){cout姓名:_nameendl;cout身份证号:Person::_numendl;// 想访问父类的需要加类域cout学号:_numendl;// 默认访问子类的}protected:int_num999;// 学号跟父类_num同名};intmain(){Student s1;s1.Print();return0;}2、成员函数的隐藏#includeiostreamusingnamespacestd;classA{public:voidfun(){coutfunc()endl;}};classB:publicA{public:voidfun(inti){coutfunc(int i):iendl;}};intmain(){B b;b.fun(10);// 调用B的fun(int)// b.fun(); // 编译报错A的无参fun被隐藏了b.A::fun();// 加类域就能调用了return0;}很多人会搞混成重载注意重载必须在同一个作用域里。父子类是两个作用域所以这不是重载是隐藏。实际写代码的时候继承体系里最好别搞同名成员真的容易把人绕晕也容易出 bug。五、派生类的默认成员函数重点类的 6 个默认成员函数在继承里派生类的这几个函数有特殊的规矩。但核心其实就一条子类处理自己的部分父类的部分交给父类的函数去处理。1、构造函数子类构造的时候必须先调用父类的构造函数把从父类继承来的成员初始化好再初始化自己的成员。如果父类有默认构造无参或全缺省子类可以不写编译器自动调。但如果父类没有默认构造子类必须在初始化列表里显式调用。#includeiostreamusingnamespacestd;classPerson{public:Person(constchar*namepeter):_name(name){coutPerson()endl;}protected:string _name;};classStudent:publicPerson{public:Student(constchar*name,intnum):_num(num),Person(name)// 初始化列表里调用父类构造{coutStudent()endl;}protected:int_num;};intmain(){Students(Louis,1);return0;}看运行结果确实也是先打印 Person() 后打印 Student()。当然由于父类构造函数是全缺省子类构造函数也可以不传 name 参数的。2、拷贝构造子类的拷贝构造要先调父类的拷贝构造把父类那部分拷过去。这里有个巧妙的地方子类对象传给父类拷贝构造自动切片。Student(constStudents):Person(s)// s是Student传给Person的拷贝构造自动切片,_num(s._num){coutStudent(const Student s)endl;}3、赋值运算符重载赋值重载也要先调父类的赋值重载。但注意因为父子类的 operator 函数名相同构成隐藏所以必须显式加 父类名::。Studentoperator(constStudents){coutStudent operator(const Student s)endl;if(this!s){Person::operator(s);// 必须加类域否则递归调用自己_nums._num;}return*this;}这里要是忘了加Person::写成 operator(s)会无限递归调用自己直接栈溢出。4、析构函数析构比较特殊子类析构函数执行完之后编译器会自动调用父类的析构函数不用你手动调。所以析构顺序跟构造是反的构造先父类构造 → 再子类构造析构先子类析构 → 再父类析构~Student(){cout~Student()endl;// 不用写 ~Person()编译器自动调}5、完整代码流程把上面四个放一起大家可以跑一下看看输出顺序#includeiostreamusingnamespacestd;classPerson{public:// 构造Person(constchar*namepeter):_name(name){coutPerson()endl;}// 拷贝构造Person(constPersonp):_name(p._name){coutPerson(const Person p)endl;}// 赋值运算符重载Personoperator(constPersonp){coutPerson operator(const Person p)endl;if(this!p)_namep._name;return*this;}// 析构~Person(){cout~Person()endl;}protected:string _name;};classStudent:publicPerson{public:// 构造Student(constchar*name,intnum):Person(name),_num(num){coutStudent()endl;}// 拷贝构造Student(constStudents):Person(s),_num(s._num){coutStudent(const Student s)endl;}// 赋值运算符重载Studentoperator(constStudents){coutStudent operator(const Student s)endl;if(this!s){Person::operator(s);_nums._num;}return*this;}// 析构~Student(){cout~Student()endl;}protected:int_num;};intmain(){Students1(Louis,18);Students2(s1);Students3(dumpling,2);s1s3;return0;}6、怎么写一个不能被继承的类两种办法C98 老办法把父类构造函数设成 private子类调不到构造自然没法实例化C11 新办法加个final关键字简单直接classBasefinal// 加了finalC11新增表示这个类不能被继承{public:voidfunc(){coutBase::funcendl;}};// class Derive : public Base {}; // 编译报错六、继承里的两个小知识点1、友元关系不能继承基类的友元函数不能访问派生类的私有成员。举例你爸的朋友不一定是你的朋友。#includeiostreamusingnamespacestd;classStudent;// 前置声明classPerson{public:friendvoidDisplay(constPersonp,constStudents);protected:string _name;};classStudent:publicPerson{protected:int_stuNum;};voidDisplay(constPersonp,constStudents){coutp._nameendl;// 可以是Person的友元//cout s._stuNum endl; // 不行不是Student的友元}想访问子类的就得在子类里也声明成友元。2、静态成员全体系共享一份父类里定义了 static 静态成员那不管派生多少个子类整个继承体系里就这一份大家共用。#includeiostreamusingnamespacestd;classPerson{public:string _name;staticint_count;// 普通静态成员变量类里面只能写声明除非是整型的 const static 才能类内赋值C11};intPerson::_count1;// 类外全局域定义初始化classStudent:publicPerson{protected:int_stuNum;};intmain(){Person p;Student s;coutp._countendl;// 地址一样couts._countendl;// 地址一样说明是同一份coutPerson::_countendl;coutStudent::_countendl;return0;}父类子类的静态成员地址都是一样的说明其实都是同一个变量。这个也容易理解静态成员本来就不属于某个对象是整个类共享的继承下来自然还是那一份。七、多继承和菱形继承难点1、什么是多继承一个子类只有一个父类叫单继承。一个子类同时继承好几个父类就叫多继承。C 是支持多继承的像 Java 就不支持只能单继承 接口。多继承虽然灵活但也带来了一个头疼的问题 ——菱形继承。2、菱形继承是什么有什么问题菱形继承是多继承的一种特殊情况长这样学生和老师都继承了人然后助教同时继承学生和老师。这就出问题了Assistant 对象里会有两份 Person 成员一份从 Student 来一份从 Teacher 来。#includeiostreamusingnamespacestd;classPerson{public:string _name;};classStudent:publicPerson{protected:int_num;};classTeacher:publicPerson{protected:int_id;};classAssistant:publicStudent,publicTeacher// 多继承用逗号隔开{protected:string _majorCourse;};intmain(){Assistant a;// 1. 二义性编译报错不知道访问 Student::Person::_name 还是 Teacher::Person::_name// a._name peter; // 编译报错对_name的访问不明确// 必须显式指定类域才能访问a.Student::_namexxx;// 加类域可以指定用哪份a.Teacher::_nameyyy;// 2. 数据冗余内存里存在两份Person对象两份_namecout(a.Student::_name)endl;cout(a.Teacher::_name)endl;// 地址不一样两份数据冗余return0;}菱形继承有两个问题二义性直接访问_name编译器不知道你要 Student 那份还是 Teacher 那份数据冗余_name 存了两份浪费空间加类域能解决二义性但冗余的问题解决不了。这时候就得用虚拟继承了。3、虚拟继承解决菱形继承在菱形继承中间的子类Student 和 Teacher继承父类 Person 的时候加上virtual关键字就是虚拟继承。#includeiostreamusingnamespacestd;classPerson{public:string _name;};classStudent:virtualpublicPerson// 虚继承{protected:int_num;};classTeacher:virtualpublicPerson// 虚继承{protected:int_id;};classAssistant:publicStudent,publicTeacher// 多继承用逗号隔开{protected:string _majorCourse;};intmain(){Assistant a;a._namepeter;// 解决了二义性// 解决了数据冗余cout(a.Student::_name)endl;cout(a.Teacher::_name)endl;return0;}虚拟继承的底层原理比较复杂原理是通过虚基表指针来保证最终派生类里只有一份虚基类成员。平时知道虚拟继承是用来解决菱形继承的、知道怎么写基本就够了。底层细节有兴趣可以自己再深挖。说句实在话实际开发中尽量别设计出菱形继承这种结构。多继承本身就够复杂了再加上菱形继承和虚拟继承维护起来很痛苦。八、继承和组合1、两种关系继承is-a 关系“是一个”。比如宝马是一辆车学生是一个人。组合has-a 关系“有一个”。比如车有四个轮胎电脑有 CPU。// 组合车有轮胎classTire{protected:string _brandMichelin;size_t _size17;};// 车classCar{protected:string _colour白色;Tire _t1,_t2,_t3,_t4;// 车有四个轮胎};// 继承宝马是车classBMW:publicCar{public:voidDrive(){cout好开-操控endl;}};实际设计代码时优先推荐组合。2、优先推荐组合的原因继承是 “白箱复用”—— 父类的内部细节对子类是可见的一定程度上破坏了封装。父类一改子类可能全受影响耦合度很高。组合是 “黑箱复用”—— 被组合的对象内部细节不可见只通过接口交互耦合度低维护起来更灵活。所以有个设计原则优先使用组合而不是继承。当然也不是绝对的明确是 is-a 关系 → 用继承明确是 has-a 关系 → 用组合两种都说得通 → 优先组合要实现多态 → 必须用继承总结继承这东西概念多但不难核心就是代码复用。如果这篇文章对你有帮助点个赞收藏一下后续持续更新 C 系列内容有问题评论区交流
返回列表