c++: 继承(上)
继承的概念与定义继承的概念继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段它允许我们在保持原有类特性的基础上进⾏扩展增加⽅法(成员函数)和属性(成员变量)这样产⽣新的类称派⽣类(也叫子类)。继承呈现了⾯向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤继承是类设计层次的复⽤。下⾯我们看到没有继承之前我们设计了两个类Student和TeacherStudent和Teacher都有姓名/地址/电话/年龄等成员变量都有identity⾝份认证的成员函数设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数⽐如⽼师独有成员变量是职称学⽣的独有成员变量是学号学⽣的独有成员函数是学习⽼师的独有成员函数是授课classStudent{public:////进⼊校园//图书馆//实验室刷⼆维码等⾝份认证voididentity(){// ...}//学习voidstudy(){// ...}protected:string _namepeter;// 姓名string _address;// 地址string _tel;int_age18;int_stuid;};classTeacher{public://进⼊校园/图书馆/实验室刷⼆维码等⾝份认证voididentity(){// ...}// 授课voidteaching(){//...}protected:string _name张三;// 姓名// 年龄int_age18;string _address;// 地址string _tel;// 电话string _title;};intmain(){return0;}我们简单看一下这两个类是不是发现有些成员函数和成员变量都是一样的这样就很冗余所以接下来的讲的继承就能解决这个冗余提高代码的质量继承的定义继承就是把上面学生和老师的一些相同重复的属性统一放在一个类里这个类也叫做基类(也可以叫父类)。然后学生类和老师类里放它们独有的成员它们就做派生类(也叫做子类)。为了理解可以类比函数的复用比如我们以前写的STL容器中vector和string里的扩容函数里面的插入函数里不就复用了扩容函数类比到这里我们把上面学生和老师的一些相同重复的属性统一放在一个类叫做person它就相当于是要复用的类学生类和老师类继承它的所有成员如下面的person类//基类classperson{public:voididentity(){coutidentity()_nameendl;}protected://private:string _name张三;string _address;//地址string _tel;//电话号码int_age18;//年龄};我这里就拿学生类来打比,现在我们的学生类就只需要包含下面这些:classStudent:publicPerson{public:// 学习voidstudy(){// ...}protected:int_stuid;};intmain(student s;)我们可以看到s的成员里面有person的和自己独有的语法的讲解继承方式的不同可能会影响基类被派生类继承的效果不同意思是基类类的成员在派生类类的访问限定符会变化这个变化的公式就是基类成员在派生类类的访问限定符min(在基类的限定符继承方式)访问限定符的比较为 public protectedprivate举个例子就如上面的图片 class student : public person 以public继承在public里的成员继承到student类里也是public, protected的成员在student类里是protected.private成员与protected的区别如果在基类是public成员继承方式为public那没话说在派生类里基类继承的所有public成员在派生类里和类外都能被使用但是private不同如果是private成员不管派生类以什么方式继承基类继承到派生类的成员在派生类里和类外都不能使用,这样可不行呐那我继承到你派生类不能用可不行我的结果是你继承到我里面的成员要在类里能使用在类外我受保护不能被使用才好所以祖师爷就新加了一个protected的访问限定符这个限定符被继承到派生类就能达到在类里能使用在类外不能使用。但注意你的继承方式如果是private那在派生类就变成了private所以继承方式很重要我们实际开发中大部分情况都是用public这样继承下来的限定符就是基类的限定符你可以以你的初衷去设计我想让派生类访问就用public和protected不想让任何人访问就用prviate继承类模板namespaceTAO{//templateclass T//class vector//{};// stack和vector的关系既符合is-a也符合has-atemplateclassTclassstack:publicstd::vectorT{public:voidpush(constTx){// 基类是类模板时需要指定⼀下类域vectorT::push_back(x)// 否则编译报错:error C3861: “push_back”: 找不到标识符............//还有其他函数这里省略这里实现的栈就是继承vector的所有成员这里使用基类vector的函数时需要指定类域虽然vector类被实例化了但它的函数没有被实例化所以这里的插入函数push需要如上那样写要显示写vector基类和派⽣类间的转换通常情况下我们把⼀个类型的对象赋值给另⼀个类型的指针或者引⽤时存在类型转换中间会产 ⽣临时对象所以需要加const如 int a 1; const double d a;inta1;//double b a;//报错因为a会产生临时的double对象并且临时对象是具有常性这里的引用本身是权限的放大constdoubleba;//加const就是符合左右两边都是常性对象的引用public继承中就是⼀个特殊处理的例外派⽣类对象可以赋值给基类的指针/基类的引⽤⽽不需要加const这⾥的指针和引⽤绑定是派⽣类对象中的基类部分如下图所⽰。也就意味着⼀个基类的指针或者引⽤可能指向基类对象也可能指向派⽣类对象。student s;person*p1s;//父类对象指向的成员会是父类原本拥有的派生类自己的不会给父类personp2s;//并且类型转换不会产生临时对象这是对继承的特殊处理person p3s;//student s p3;//目前的知识暂时认为父类不可以作为子类的对象以后可以用安全转换类型后面再说这里的派生类转换父类的指针和引用和对象中的成员只包含基类中有的派生类独有的成员不会被指向并且你现在可以认为只有派生类类-基类 基类-派生类类不行后面需要学安全转换的知识才能实现所以我们这里暂时认为不行。这种特殊处理对下面要讲的在派生类写默认构造函数有大用继承中的作用域隐藏规则1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。2. 派⽣类和基类中有同名成员派⽣类成员将屏蔽基类对同名成员的直接访问这种情况叫隐藏。在派⽣类成员函数中可以使⽤基类::基类成员显⽰访问3. 需要注意的是如果是成员函数的隐藏只需要函数名相同就构成隐藏。4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员而且如果调用函数或要使用成员变量它会就近原则先在当前的派生类找没找到才去基类找所以如果有同名函数和变量会优先使用派生类的独有的函数和成员变量派⽣类的默认成员函数4个常⻅默认成员函数6个默认成员函数默认的意思就是指我们不写编译器会变我们⾃动⽣成⼀个那么在派⽣类中这⼏个成员函数是如何⽣成的呢构造函数如果我们不在派生类写构造函数编译器会自动生成这里的构造域类与对象的构造高度相似派生类中的基类的成员要在基类的构造函数构造派生类独有的就在自己这里构造与类与对象中的构造方式一样这里就简单写个person类,它是基类//基类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://默认构造函数//构造函数分为两部分//1. 基类的成员(整体构造用它自己的构造函数)//2. 派生类剩下的成员跟以前类与对象构造一样student(constchar*name,intid,constchar*address)// :_name(name)不行需要整体构造:Person(name),_id(id),_address(address){}protected:int_id;string _address;};而且是基类的成员变量的构造要是个整体所以在初始化列表中就用基类的匿名对象构造就行基类的成员有什么参数写上去就行而且初始化列表的构造顺序是先构造继承基类的成员再是派生类自己的拷贝构造函数Person(constPersonp)//基类的拷贝构造:_name(p._name){coutPerson(const Person p)endl;}student(conststudents):Person(s),_id(s._id),_address(s._address){cout student(const student s)endl;}这里就能体现在特殊化类型转换在继承中的效果这里的初始化复制基类的成员会去基类这个类里去调用它的拷贝构造这里的p就是s不是临时对象如果这里是临时对象那就会无限递归就会出错。赋值重载 studentoperator(conststudents){Person::operator(s);_ids._id;_addresss._address;return*this;}理解了前面几个这个就很好理解派生类中的基类成员那就调用基类的赋值重载,但是注意这里有个隐形的坑那就是函数名隐藏这里的派生类的函数名与派生类的函数名构成隐藏,所以这里调用基类的operator()需要指定类域不然会内存泄漏析构函数~student(){// Person:: ~Person();//不用显示写编译器默认会有而且是先析构子类在析构父类显示写了就会先析构父类这种有些情况会有问题_id0;//所以编译器就默认自己生成析构基类的析构函数_address;}析构要格外注意以我们顺下来的思路我们肯定会认为要先写基类的析构再析构派生类的成员,但这里不是这里编译器会自己生成默认的基类的析构函数而且析构的顺序为先析构派生类成员再析构基类成员因为析构中如果要用到基类的成员去当条件时如果析构了基类那就会有矛盾所以就为了避免这种情况那干脆编译器就默认再派生类的析构函数中生成基类的析构函数所以不用写如果写了就会析构两次就错误了实现⼀个不能被继承的类⽅法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;}继承与友元友元关系不能继承也就是说基类友元不能访问派⽣类私有和保护成员 。classstudent;classPerson{public:friendvoidDisplay(constPersonp,constStudents);protected:string _name;// 姓名};classStudent:publicPerson{protected:int_stuNum;// 学号};voidDisplay(constPersonp,constStudents){coutp._nameendl;couts._stuNumendl;}这里会报错Display是person的友元函数不是student的友元函数所以Display不能访问student的成员变量一个形象的例子是:你爸爸的朋友不是你的朋友所以你爸爸的财产可以继承你朋友的你不能继承所以这里的Display不能使用student中的成员变量