目录
一 继承的概念
代码:
总结:
二 继承中的关系
三 继承中的作用域问题
什么是域?
隐藏:
隐藏的场景:
总结
四 赋值兼容原则
什么是赋值兼容原则?
与平时强制类型转换的区别
这一个赋值兼容原则的底层实现是怎么样的?
五 关于继承关系下的六个默认成员函数
什么是默认成员函数?
对于初始化
1 构造函数
2 拷贝构造函数
3 赋值运算符的重载
对于析构
对于取地址
六 继承和友元
七 继承和静态成员
八 多继承
多继承导致的问题:
题外话:如何定义一个不能被继承的类?
如何解决菱形继承带来的问题?
虚继承的原理
九 继承和组合
一 继承的概念
C++有三大特性,分别是封装,继承和多态。
继承主要体现了类设计层次的一个复用关系。打个比方,比如:
如图所示,Person派生生成了Student和Teacher类。Student类和Teacher继承了Person类。那么对于Person类中的成员变量和成员函数都是可以复用的,也就是说_agem_name,_sex在子类中各自都有一份。不管访问限定符是什么,都被复用到对应的类中了
如上图,这样去复用了别人的成员的是子类(基类),提供成员给别人复用的是父类(基类)
代码:
class Person
{
public:
int _age;
string _name;
string _sex;
};
class Stduent:public Person
{
public:
string _sid;
};
class Teacher :public Person
{
public:
string _tid;
};
总结:
①复用:将共有的成员函数或者成员变量提取出来,作为父类,子类通过继承父类,就可以使用了。
②关系(一对对象,两种关系):父类和子类,基类和派生类。派生或者继承。
二 继承中的关系
根据基类的访问限定符和子类的继承方式,一共有如下的几种关系:
注意:这里最后的关系都是在派生类中的体现
我们最后通过排列组合可以发现,一共有九种关系。非常的复杂和繁琐。我这里归纳了一下
1 如果父类是private的成员,不管是什么方式继承,都是不可见的。
所谓不可见就是无论是在类内还是在类外都是不能访问的。区别于派生类中的protect关系,在子类中可见,在类外不可见。
2 除了第一条的,继承方式和访问限定符中取较小权限的
我们发现,基类中的私有成员,无论如何继承下去,在派生类中都是不可见的。那么如果我们只想在父类中访问的话,不想被外界继承和使用,可以定义成私有的。但是说实话,如果这样定义的话,继承就没啥意义了,因为继承就是想使用父类的东西。
另外,如果类class不写继承方式的话,默认的是私有的,struct也是可以定义成类的,但是是私有的。不算语法错误
class Teacher: Person
但是,我们日常生活中,最常用的也就只有public相关的继承
三 继承中的作用域问题
什么是域?
由{}括起来的,属于各自特定的域。可能会影响生命周期和访问。
比如:C语言中的域有全局域和局部域这样的概念,有些是会对生命周期造成影响的:局部域
但是c++涉及到的类域,只影响访问
基于此,在同一个域中,我们不能定义同名的函数,如果我们非要定义同名函数就会引发对应的问题。c++中也对此做了特定的处理。
隐藏:
隐藏的场景:
从c++的继承关系出发,如果父类有_name了的基础上,子类中也定义了_name的成员变量,这样子的话,由于子类继承父类的时候,会去复用父类的成员,在子类中,就会存在两个_name.
那么我们在子类中访问_name的时候,访问的是父类的还是子类的_name呢?
不妨自己验证一下
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
int _age;
string _name;
string _sex;
};
class Student:public Person
{
public:
string _sid;
string _name;
};
class Teacher: public Person
{
public:
string _tid;
};
int main()
{
Student s;
s._name = "张三";
Person p;
return 0;
}
发现确实,如果父类子类都有_name的话,访问的时候优先访问的是子类中的,因为局部优先原则。如果我们想通过子类对象去访问父类中的_name的话,我们需要限定域
举一个用子类访问父类同名成员的例子
总结
什么是隐藏:
在继承关系中,如果子类中有父类同名的成员(函数或者变量),访问子类的对应成员,优先去匹配子类域中的,除非特别指定父类的,否则就访问不到父类的。
对于函数:不在乎参数,只要函数名相同,就构成隐藏。
也就是说,如果构成隐藏,除非特定指定访问父类的,否则就默认访问子类的,访问不到父类的
隐藏是由于子类和父类都有独立的域导致的
区别隐藏(重定义),重载,不可见(隐身)
隐藏(重定义):在继承关系中,由于父类和子类中有相同名字的成员变量,访问的时候默认访问子类中的
重载:在同一个域中,两个函数同名但是参数列表不同(包括个数 顺序),并且不要求返回值。这样子编译器编译的时候,优先匹配最合适的
不可见(隐身):是因为基类的private成员导致的派生类中继承关系是不可见的,那么在类内或者类外都无法访问
四 赋值兼容原则
什么是赋值兼容原则?
子类的对象指针或者引用可以直接赋值给父类
Student s;
Person p = s;
Person* p = &s;
Person& p = s;
与平时强制类型转换的区别
我们平时写代码的时候,如果两个数据的数据类型不统一的话,要么编译器自动转换,要么就是手动写上强制类型转换才可以规避语法错误
但是,对于子类和父类的关系。为什么可以直接赋值?
这一种行为是完全不同于上述的行为的,因为他支持引用的直接赋值
正常情况下编译器转换的话,引用是不可以的。因为两个变量进行赋值的时候,中间会产生一个临时变量,是把这个临时变量赋值给另外的变量的但是临时变量具有常性。因此是属于权限的放大, 是会产生语法错误的
这一个赋值兼容原则的底层实现是怎么样的?
发生了一个切片行为。可以这样理解:子类中除了与父类共有的那一部分,还有自己特有的。所以可以用子类给父类赋值,只用取出父类中的就可以了。
同理,指针也是取出父类特有的。将子类的指针给父类,那么改变了指针+1的位置。
引用也是同理
五 关于继承关系下的六个默认成员函数
按照默认成员函数的功能分类,可以分成以上这三大类。
什么是默认成员函数?
默认成员函数就是即使当自己什么都不写的时候,编译器默认生成的成员函数。一旦自己针对自己的需求写了对应的成员函数,编译器就不会生成了
那么在继承体系下,这几类默认成员函数函数是怎么使用的呢?
总体的大规则就是:继承体系下,派生类中的默认成员函数如果自己去定义,需要遵循“合成”的规则。把父类和子类分别当做一个整体,来进行操作
对于初始化
1 构造函数
在一般的情况下(这里的一般情况是指没有继承),默认构造函数对内置类型不做处理,对自定义类型会去调用类中自定义类型的构造函数完成初始化。
继承体系下,在子类中自己特有的也遵循这样的规则。但是对于父类的需要去调用父类的构造函数完成初始化。
这时候有两种情况1.父类没有显示写出,那么编译器生成一个默认的,子类直接去调用。2 父类如果定义了 子类如果通过初始化列表来写,需要创建一个匿名对象写出
class Person
{
public:
int _age;
string _name="李老师";
string _sex;
/*void show()
{
cout << _name << endl;
}*/
Person(int age,const char*name,const char*sex)
:_age(age)
,_name(name)
,_sex(sex)
{
}
};
class Student:public Person
{
public:
string _sid;
Student(int age, const char* name, const char* sex, const char* sid)
:Person(age,name,sex)
, _sid(sid)
{
}
};
2 拷贝构造函数
一般情况下,对于内置类型进行值拷贝。对于自定义类型,调用自定义类型的拷贝构造函数(内置类型进行值拷贝是没有问题的。但是对于在栈上等地方开辟了空间的,如果进行值拷贝的话,不行,会两个内容指向同一空间,造成析构两次或者改变的时候错误改变的问题。所以自定义类型是要去调用自定义类型的拷贝构造函数的:比如用两个栈实现一个队列,此时初始化这个队列调用的就是栈的拷贝构造函数)
继承体系下,子类自己特有的遵循上述的一般规则。但是对于父类的,需要调用父类的拷贝构造函数完成初始化
对于父类的,如何在子类中取出父类的一部分呢?
赋值兼容原则。把子类传给父类,那么父类中接收到的其实也是子类切片之后和父类吻合的那一部分。
3 赋值运算符的重载
一般情况下,是和拷贝构造函数类似的
在继承体系中,子类中如何写?
对于父类的调用去赋值,而对于子类自己的,需要单独写出
代码
由于子类和父类都有operator=,构成了隐藏。但是这里需要使用父类的,因此需要单独写出。
对于析构
一般情况下,对于内置类型不做处理,但是对于自定义类型会去调用自定义类型的析构函数
在继承体系下,不需要写出
为什么子类不用自己显示写出?
这里涉及到两个知识点:隐藏和多态
由于后续多态的需要,析构函数会被统一处理成函数名是destructor的函数。因此父子类函数中构成了隐藏。如果要写的话,对于父类的那一部分需要指定域。但是自己写的时候,可能会有顺序的不一致:构造函数是先初始化父类的再初始化子类的,但是析构函数反之。如果写出反而可能会搞错顺序。因此不用写。
对于取地址
取地址就是取出自己的对应的地址,不需要分成子类和父类两个部分去考虑了。也不需要自己显示的写出。
六 继承和友元
友元关系不能继承。
七 继承和静态成员
被static修饰的成员存储在栈上,父类和子类访问到的是同一个,只有一份的。可以在父类中的构造函数中定义一个static count,就可以统计一共构造了几个父类和子类。
八 多继承
一个子类有两个及以上的直接父类就是多继承关系
多继承导致的问题:
代码冗余(D中由两份A,分别来自B和C)
二义性,由于是这么存储的,因此在D的对象中,调用A中成员,不指定域的话,就无法分别到底是B的还是C的。(但是即使指定了也无法解决代码冗余这个问题)
题外话:如何定义一个不能被继承的类?
1 父类构造函数私有化:因为后续子类都需要用到父类的 因此私有化之后 就不能被继承了
2 使用c++新增的关键字final最终类,标识该类不能被继承
如何解决菱形继承带来的问题?
使用虚继承:virtual关键字(注意与多态中virtual的区分,属于一个关键词两用)
如何使用?
在腰部的位置虚拟继承。从A继承的地方都要使用虚继承。将共同基类设置为虚基类。
代码:
class B : virtual public A
虚继承的原理
如果是普通继承是这样存储的
如果是虚拟继承
(由于我的编译器版本有点高 观察结果不太一样 去网络上找了一个比较标准的)
原博客链接:C++之继承相关问题——菱形继承&继承 - 腾讯云开发者社区-腾讯云
将共有的提取到最下面的位置。
虚拟继承的话,B和C中有一个虚表指针,指向虚表(本质上是一个函数指针数组),虚表再通过索引指向对应的函数。这里面记录了偏移量和距离,因此就可以通过这个方式找到对应的成员变量。
更复杂的场景:如果对于一个函数在A中,BC继承了,D再继承BC继承了该函数。D中的函数参数传参的时候传指针或者地址,如何找到对应的数据?
首先这个行为是被支持的,因为赋值兼容原理。那么如何找到呢?在汇编时期,汇编代码需要被确定下来,BC中都有一个虚基表,但是他们是不相同的。
因为需要支持多态行为,并且因此重写了函数,并把对应的函数地址放在虚函数表中。(虚函数仍然是存储在公共的代码段的)
那么我们就可以通过虚表指针找到对应的虚函数表,进而找到对应的成员变量和成员函数
注意,这种情况也是属于菱形继承。要解决相关的问题,需要在BC位置带上虚函数virtual标识
九 继承和组合
继承:是一种is a的关系,可以通过“是”来判断。比如学生和人:“学生是人”
组合:has a “有没有” 耦合度比较低。b类有a ,b中的成员除了a,还有其他的组成。
有一些既是组合又是继承。比如stack和queue/vector/deque
如果在这样的一个情况下的话,优先使用组合-》高内聚,低耦合,减少模块与模块之间的耦合度,便于后期的维护。否则一个模块修改了,后期如果复用了这个模块,需要修改的地方就很多