目录
引入
继承介绍
概念
优点
分类
公有继承
保护继承
私有继承
特点
单继承
多继承
赋值
介绍
分类
对象之间赋值(拷贝构造)
验证普通赋值需要创建临时变量
指针/引用赋值
赋值原理
继承中的作用域
介绍
隐藏 / 重定义
前提
介绍
派生类的默认成员函数
构造
显式构造父类
拷贝构造
operator=重载
析构
特殊成员的继承
友元
静态成员
菱形继承
引入
介绍(存在的问题)
虚拟继承
介绍
原理
不使用virtual时
加入了virtual关键字后
使用场景
引入
c++是面向对象的语言,那么不同对象是不是有可能拥有相同特征呢?
那这个相同特征也算是一种对象(比较宽泛定义的对象),那就也得写一个类(假设为A),来囊括它的行为
拥有相同特征的两个对象就都需要有A(基类),有多种方法可以实现它:
- 内部类(基类在子类内部定义)
- 直接实例化(基类在子类外部定义)
- 继承(复用基类的代码)
各有各的优缺点,具体使用什么可能看情况吧,目前还不清楚(跪),反正这里就只讲继承噜
继承介绍
概念
是面向对象程序设计使代码可以复用的最重要的手段,它允许一个类(称为子类或派生类)继承另一个类(称为父类、基类或超类)的属性和方法
继承使得子类能够拥有父类的特性,同时也可以在此基础上添加自己的特性或修改继承来的特性
优点
分类
公有继承
- 子类从父类继承的成员在子类中的访问权限不受限制,父类成员的权限在子类中不变,保留了接口的一致性
- 是最常用最常用的继承方式
保护继承
- 子类从父类继承的成员在子类中的访问权限变为保护(protected)
- 子类可以访问父类的保护成员,但外部无法访问子类从父类继承的保护成员
- 保护成员限定符是因继承才出现的 (因为平时的时候,私有和保护的权限是一样的,但在继承父类的成员时就有所差异)
私有继承
- 子类从父类继承的成员在子类中的访问权限变为私有(private)
- 这意味着子类可以访问父类的成员,但外部无法访问子类从父类继承的成员
特点
- 父类的私有成员可以被继承,但处于"不可见"状态,子类和子类外部都无法访问,但可以通过父类的成员函数间接访问
- 父类成员的权限 和 子类继承父类的方式,取其权限小的作为父类成员在子类中的权限
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public
单继承
一个子类只有一个直接父类时称这个继承关系
多继承
一个子类有两个或以上直接父类时称这个继承关系多个基类之间用逗号隔开
赋值
介绍
一般来说,不同类之间无法赋值,但继承不一样,父类和子类可以赋值,但只局限于子类赋给父类
子类的对象可以赋值给父类的对象/指针/引用
分类
对象之间赋值(拷贝构造)
- 上面的结果很奇怪,只有一次构造,但却有两次析构
- 看过发现是tmp也析构了,但奇怪的是,tmp被创建的时候并没有走我写的构造函数
- 于是我查了查,加上了拷贝构造,结果就对了
class person { public: person() { cout << "person1" << endl; _a = 1; } person(person& tmp) { _a = tmp._a; cout << "person2" << endl; } void work() { cout << "person::work()" << endl; } ~person() { cout << "~person" << endl; _a = 0; } public: int _a; }; class student :public person { public: void work() { cout << "person::work()" << endl; } public: int _tmp = 0; }; int main() { student s; s._a = 2; person tmp = s; cout << tmp._a << endl; return 0; }
结论是:赋值转换的过程中,应该是先进行隐式类型转换,将子类看作父类(抛弃掉子类多出的部分),然后调用子类的拷贝构造,进行数值拷贝
验证普通赋值需要创建临时变量
不同的普通类型之间的赋值是 -- 先创建一个临时变量,然后再赋值
int a=0; double& b=a;
- 这样的代码无法编过,但在double前+const就行,原因是:
编译器会在赋值前先创建一个临时变量tmp,临时变量具有常性,所以它是const double类型,需要和const double&类型绑定
指针/引用赋值
同理,指针/引用也是可以赋值的,也同样是用子类的指针/对象赋值给父类的指针/引用
- 这样父类的那个指针/引用的范围只是子类的一部分
- 其中,父类指针可以通过强制转换类型来赋值给子类指针,但可能会越界
赋值原理
直接切割
- 如果是少到多赋值,那子类多出来的成员变量该怎么办捏,所以采用多到少赋值
继承中的作用域
介绍
每一个类都有独立的作用域,所以父类和子类也都拥有自己的作用域
作用域的本质 -- 限定编译器查找的范围
隐藏 / 重定义
前提
c++允许父类和子类拥有同名成员
其中,函数只要同名就可以达成隐藏的条件,而不用考虑参数列表
注意:父类和子类中的同名函数不构成重载(重载要在同一作用域的啊摔)
介绍
当子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问
- 意思就是,当出现命名冲突,优先使用子类中的成员
- 如果是函数的话,即使子类中的函数不符合调用时的参数,也不会去父类找,而是直接报错
- 可以想成:编译器很懒,只要按照查找顺序找到名字一样的,就到此为止,它不管是否符合语法
在实际中,最好不要定义同名的成员,没意义,而且看的不清楚
派生类的默认成员函数
在使用过程中,将父类看作一个对象
构造
- 当父类没有默认构造,而子类也没有为父类显式构造,就会出错(编译器不会为类中的自定义类型生成默认构造)
显式构造父类
像定义一个匿名对象一样,传入参数即可
- 父类有默认构造,子类没有默认构造时,系统会自动生成子类的默认构造,它会调用父类默认构造(按照基类声明顺序调用,因为可能有多继承),但对其他成员不做初始化
- 按照声明的顺序的话,类的开头肯定是父类(类名那里有写对父类的继承方式),然后才是自己
拷贝构造
- c++规定,子类的拷贝构造必须显式调用父类的拷贝构造,而不能在子类中完成对父类成员的初始化
- 写法和上面的图里一样
- 这里的person的拷贝构造,参数应该是person类型,但是可以用student的直接传,是因为 -- 子类对象可以赋值给父类对象/引用(前面的知识堂堂登场噜)
operator=重载
和拷贝构造类似,必须要显式调用基类的operator=
- 而且要注意,不能直接这么写,父类和子类的operator=会构成隐藏,所以这里应该使用类域来指明是哪个operator=(不然堆栈就爆了,疯狂自己调用自己)
析构
- 前面的构造有说明,先是父类构造,再是子类
- 所以按照堆栈建立顺序,应该是先析构子类,再析构父类
- 除此之外,因为子类可以用父类成员,那么如果先析构父类,当子类析构中出现析构完父类后还要使用父类成员
这种歹毒的情况就完蛋- 所以,综合来看,必须得先析构子类
- 而且父类的析构是可以不用显式调用嘟,编译器会在子类析构结束时自动调用父类析构
特殊成员的继承
友元
友元关系是无法继承的,也就是基类的友元函数无法访问子类的保护/私有成员
如果需要使用,需要在子类也声明一下这个友元关系
静态成员
- 在类和对象中,我们知道静态成员是属于类的,而不是每个对象
- 所以在继承中,静态成员也很特殊,它只被子类继承了使用权
- 无论有多少个子类,他们之中都只有一份静态成员,就像所有被实例化出的对象中,也只有一份
菱形继承
引入
前面提到,继承有单继承和多继承,而菱形继承就是其中的一个特殊情况
其形状也不一定是菱形,只要最终有交汇,就属于这样的情况
介绍(存在的问题)
- 像这样的,一个子类继承的两个基类中,都继承了同一种基类
- 就会导致一个assistant中有两个person
- 但很显然,作为面向对象的语言,一个对象不可能存在两种作为person的状态(比如person里会有的名字,性别啥的),所以出现了数据冗余(这个还真不好解决)
- 而且调用的时候,因为有两份person,就会出现二义性,需要指明类域(有点麻烦的捏)
为了解决这个问题,c++提出了虚拟继承这一概念
虚拟继承
介绍
是一种用于处理多重继承中的二义性和问题的技术,它允许你在继承关系中使用虚拟基类,从而解决由于多个派生类共享同一个基类实例而导致的问题
通过在继承时使用virtual关键字来解决这个问题
当一个类通过 虚拟继承 继承一个基类时,无论多少个派生类都会共享同一个基类实例
可以看到,d中的 B类和C类的 A类中的_a是共享的
原理
不使用virtual时
class A { public: int _a; }; class B : public A { public: int _b; }; class C : public A { public: int _c; }; class D : public B, public C { public: int _d; };
可以看出来,是很规整的按照顺序存放在内存中,且都有一份A
加入了virtual关键字后
class A { public: int _a; }; class B : virtual public A { public: int _b; }; class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; };
会发现,在原先A所在位置,被一段看起来很像地址的数字代替了
如果在内存中查找(注意普通电脑一般是小端存储哦,数字倒过来才是地址)
(这是重新生成了的)
跳转过去后:
第一行是0,第二行是20(16进制)
C中的地址跳转过去后:
第一行仍然是0,第二行是12
- 20和12都是4的倍数,而内存中不会莫名其妙存放这么个数字,它肯定和A类有关联
- 所以,真相只有一个 -- 存储的数字代表距离A类地址的字节数
- 而且,由于D的数据结构已经确定,那么那份偏移量表可以被多个实例化后的D对象共用(因为里面是偏移量)
但其实,对于d来说,找_a其实不需要用到偏移量,因为他知道A被自己放在最后,直接拿就行
使用场景
当需要切割时,偏移量会起到大大的作用
- 当pb指向d中的B类时,B类既有自己的成员,也有A的成员
- 可是A被放在了d的底层,和B隔开了
- 这里就需要偏移量,来为B类找A使用
一旦使用virtual,单独实例化B/C时,它里面的构造也会改变成使用偏移量表的形式
- 那么,当我们拿取A中成员时,无论是谁在拿,在汇编上都是一样的
- 拿到偏移量,然后访问
- 所以,它统一了子类的访问方法
- 且证明了在切割时 不需要处理原对象,直接按照规定方法拿就行