继承 继承的本质:
实现代码的复用
在基类中提供统一的虚函数接口,可以让派生类进行重写,就可以使用多态了。(具体看本文多态章节)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class A { public : A (int a1 = 1 , int a2 = 2 , int a3 = 3 ) :a1_ (a1), a2_ (a2), a3_ (a3) {} ~A (){} int a1_; protected : int a2_; private : int a3_; }; class B :public A{ public : B (int b1 = 10 , int b2 = 20 , int b3 = 30 ) :b1_ (b1), b2_ (b2), b3_ (b3) {} ~B () {} int b1_; protected : int b2_; private : int b3_; };
B类继承了A中的成员变量和方法,如下图所示。并且由于在派生的时候会在变量前面加入基的限定符,如A:a1_,所以即使A类和B类的成员变量和方法同名,也不会冲突。
继承与访问限定比较 public:可以在类的内部和外部访问。 protected:只能在类的内部以及派生类(子类)中访问。 private:只能在类的内部访问。Tips:在c++中,默认情况下,类的成员(属性和方法)的访问权限是private。 class(默认private)、struct(默认public)。 访问权限顺序:public>protected>private 继承方式:public、protected、private
重点: 基类到派生类的访问权限是不能大于基类的访问权限的,比如说protected继承方式,那么基类中的public成员变量只能变成protected方式
派生类继承访问权限分析 这里以公有继承为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> using namespace std;class A { public : A (int a1 = 1 , int a2 = 2 , int a3 = 3 ) :a1_ (a1), a2_ (a2), a3_ (a3) {} ~A (){} int a1_; protected : int a2_; private : int a3_; }; class B :public A{ public : B (int b1 = 10 , int b2 = 20 , int b3 = 30 ) :b1_ (b1), b2_ (b2), b3_ (b3) {} ~B () {} void show () { cout << "基类公有变量a1_:" <<a1_; cout << "基类保护变量a2_:" << a2_; cout << "基类私有变量a3_:" << a3_; } int b1_; protected : int b2_; private : int b3_; }; int main () { B b; cout << "基类公有变量a1_:" << b.a1_; cout << "基类保护变量a2_:" << b.a2_; cout << "基类保护变量a3_:" << b.a3_; return 0 ; }
依次改变B的继承方式,总结可以得到下表 注意:派生类从基类继承private的成员,但是派生类无法直接访问。
class定义派生类,默认继承方式是private继承; struct定义派生类,默认继承方式是public继承。
派生类和基类关系 派生类的构造顺序
派生类的构造和析构函数,负责初始化和清理派生类部分
派生类从基类继承来的成员,由基类的构造和析构函数负责初始化和清理工作
下面是代码验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Base { public : Base (int data) :dataa_ (data) { cout << "Base()" << endl; } ~Base () { cout << "~Base()" << endl; } protected : int dataa_; }; class Derive :public Base{ public : Derive (int data) :datab_ (data), Base (data) { cout << "Derive()" << endl; } ~Derive () { cout << "~Derive()" << endl; } private : int datab_; }; int main () { Derive b (200 ) ; return 0 ; }
执行结果为 派生类的完整生命流程如下 1、派生类调用基类的构造函数,初始化从基类继承的成员 2、调用派生类自身的构造函数,初始化派生类成员 3、调用派生类的析构函数,析构派生类成员 4、调用基类析构函数,释放派生类中从基类继承来的成员
基类对象(指针)派生类对象(指针)的转换 一般来说,派生类相对基类来说是占用更大的内存空间的,基于这一点理解以下结论。
将派生类对象赋值给基类对象,可以,赋值后的基类对象只能返回基类的成员
将基类对象赋值给派生类对象,错误,(因为基类可能没有包含派生类独有的那部分数据)
将派生类对象指针(引用)给基类对象指针(引用),可以,(基类指针是指向派生类对象的,但是由于基类指针的限定,只能访问派生类中的基类成员。)
将基类对象指针(引用)给派生类对象指针(引用),错误
总结:在继承结构的转换,一般只支持从派生类到基类的转换
重载和隐藏 重载关系:一组函数要重载,必须要处在同一个作用域当中;并且函数名字相同,参数列表不同 隐藏关系:在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Base { public : void show () { cout << "Base::show()" << endl; } void show (int ) { cout << "Base::show(int)" << endl; } }; class Derive :public Base{ }; int main () { Derive b; b.show (); b.show (10 ); return 0 ; }
代码的执行结果为,调用的是基类的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Base { public : void show () { cout << "Base::show()" << endl; } void show (int ) { cout << "Base::show(int)" << endl; } }; class Derive :public Base{ public : void show () { cout << "Derive ::show()" << endl; } }; int main () { Derive b; b.show (); return 0 ; }
因为重载是要处于同一作用域内才起作用,而基类和派生类处于不同作用域,当派生类定义了与基类同名的函数,基类中的与其同名及其重载函数就被隐藏了。
虚函数 ①如果一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类产生一个唯一的vftable,即虚函数表,其中主要存储的就是RTTI(Run-Time Type Information,运行时类型识别)指针和虚函数的地址。 当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区),不可更改
1 2 3 4 5 6 7 8 9 10 class Base { public : Base (int data=10 ):dataa_ (data){} virtual void show () { cout << "Base::show()" << endl; } virtual void show (int ) { cout << "Base::show(int)" << endl; } protected : int dataa_; };
如Base类中定义了两个虚函数,其生成的虚函数表如下,使用命令查看Base类的内存分布情况 c++打印类的内存布局 vs studio中打印信息
1 2 //进入到包含test.cpp的文件夹下,使用这个命令单独查看Base类的内存分布 cl test.cpp /d1reportSingleClassLayoutBase
打印的结果为:注意:由于这里打印Base类的内存布局,发现存在{vfptr}这个字段,以为类中也存在这个虚函数指针。其实不是的, 用{}强调这是隐式生成的成员,不是用户显示声明的。
类(Class)会生成一份虚函数表 vtable(静态只读) 对象(Object)里才有虚函数表指针 vptr(vfptr)指向这张表
这里的偏移量指的是vfptr指针在内存的偏移量为零,即基类内存中的第一个成员变量为vfptr。 ②如果使用带有虚函数的类定义一个对象,其大小为成员变量的大小+虚函数指针的大小(一般为8字节),该虚函数指针指向虚函数表。(该Base类因为定义了虚函数,所以有一个用户不可见的vfptr指针)。 一个类型定义的多个对象,他们的vfptr都是指向同一个虚函数表。
1 2 3 4 Base b1; Base b2; Base b3;
③一个类里面的虚函数的个数,不会影响对象的内存大小(不是说多个虚函数,就需要多个vfptr,vfptr始终指向的是虚函数表);虚函数的个数影响的是虚函数表的大小
基类是虚函数对派生类的影响
如果派生类中的方法,和从基类中继承的某个方法,满足:
返回值、函数名、参数列表都相同
基类的方法是虚函数
则派生类的这个方法,会被处理成虚函数
1 2 3 4 5 6 7 8 class Derive :public Base{ public : Derive (int data=20 ):Base (data),datab_ (data){} void show () { cout << "Derive ::show()" << endl; } protected : int datab_; };
这里派生类的show()方法满足上述条件,发生覆盖(在虚函数中,派生类的Derive::show()覆盖了基类中的Base::show())。
基类和派生类的内存变化
静态绑定与动态绑定 指针调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Base { public : virtual void show () { cout << "Base::show()" << endl; } private : int dataa_; }; class Derive :public Base{ public : void show () { cout << "Derive::show()" << endl; } private : int datab_; }; int main () { Derive d; Base* pb = &d; pb->show (); cout << "基类Base大小:" << sizeof (Base) << endl; cout << "派生类Derive大小:" << sizeof (Derive) << endl; cout << typeid (pb).name () << endl; cout << typeid (*pb).name () << endl; return 0 ; }
pb是基类 指针,但其指向的是派生类对象,由于基类指针的限定,当调用pb->show()方法时,就自动去派生类中的基类部分去找show方法的实现。 1.如果Base::show()是普通方法,则进行静态绑定(call Base::show())
2.如果Base::show()是虚函数,则进行动态绑定,反汇编代码如下
具体步骤为:
根据pb指向的对象的前四个字节获取虚函数指针vfptr的值(其指向的对象是一个派生类Derive对象)
根据vfptr获取其指向的虚函数表(这里的虚函数表为,Derive的虚函数表)
根据虚函数表得到其对应的虚函数(这里的&Derive::show()虚函数重写了&Base::show(),所以最后调用的是Derive::show()方法)
Base::show()方法是否是virtual的输出比较
对于普通方法,Base类中有一个int类型成员变量,占4字节;Derive继承基类的成员变量+自身定义的int类型变量,共8字节。
对于Base::show()方法是虚函数,则有一个虚函数指针vfptr,8字节,则基类大小为4+8=12(我这里是64位系统,8字节对齐,变成了16个字节大小);派生类在基类的16字节基础上加上本身的int成员变量4字节,再次内存对齐,共24字节。
对于pb指向类型的理解
1 2 3 4 5 6 7 8 9 Derive d; Base* pb = &d; pb->show (); cout << typeid (*pb).name () << endl;
前提:pb为Base类型的指针,指向的是Derive的派生类对象:
Base没有虚函数,派生类中没有虚函数,则识别的是编译时期的类型,即Base类。
Base存在虚函数,存在Derive类的虚函数表,则识别的就是运行时候的RTTI类型,即为Derive类。
其他方式调用的绑定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Base { public : virtual void show () { cout << "Base::show()" << endl; } private : int dataa_; }; class Derive :public Base{ public : void show () { cout << "Derive::show()" << endl; } private : int datab_; }; int main () { Base b; Derive d; b.show (); d.show (); Base *pb1=&b; pb1->show (); Base *pb2=&d; pb2->show (); Base &ref1=b; ref1. show (); Base &ref2=d; ref2. show (); return 0 ; }
总结:动态绑定必须当通过指针(引用)调用虚函数,才会发生。
虚函数的默认参数问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> using namespace std;class Base { public : int a = 1 ; virtual void print (int n = 2 ) { cout << "base:" << a + n << endl; } }; class Derive :public Base{ public : int b = 3 ; void print (int n = 10 ) { cout << "derive:" << b + n << endl; } }; int main () { Base* ptr = new Derive (); ptr->print (); delete ptr; }
执行结果: derive:5
分析:在编译阶段,基类虚函数的默认参数就已确定(发生静态绑定)。当调用子类函数时会发生多态行为,但编译器仍会采用基类的默认参数。
虚函数实现的依赖
虚函数能产生地址,存储与vftable中
对象必须存在(通过vfptr—>>>vftable—>>>虚函数地址)
构造函数不存在虚函数,因为这个时候还没有对象产生(不满足虚函数依赖的条件二)。即使在类的构造函数中,调用了虚函数,也是静态绑定。 static静态成员方法也不能被实现成虚函数方法,(其不依赖于对象,不满足条件二)
虚析构函数的实现 情景:基类的指针(引用)ptr指向堆上new出来的派生类对象时,delete ptr;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Base { public : Base (int data) :dataa_ (data) { cout << "Base()" << endl; } ~Base () { cout << "~Base()" << endl; } protected : int dataa_; }; class Derive :public Base{ public : Derive (int data) :datab_ (data), Base (data) { cout << "Derive()" << endl; } ~Derive () { cout << "~Derive()" << endl; } private : int datab_; }; int main () { Base* pb = new Derive (10 ); delete pb; return 0 ; }
在本篇 派生类的构造顺序 一小节中提到,派生类的构造顺序是基类构造–派生类构造–派生类析构–基类析构。 但是这里却没有调用派生类的析构函数,如果派生类有指向外部资源,就会造成内存泄露。
这里为什么没有调用派生类的析构函数?
这里pb是Base类型的指针,当调用delete pb的时候,在Base类中找到其对于的析构函数,Base::~Base(),这里发生的是静态绑定
解决方法 1.使用Derive 类型的指针指向开辟的内存(不是本节重点)
1 2 Derive* pb = new Derive (10 ); delete pb;
2.将基类的析构函数定义为virtual(发生动态绑定)重点:基类的析构函数是virtual函数,那么派生类的析构函数自动成为virtual函数。
1 2 3 4 5 6 7 8 class Base { public : Base (int data) :dataa_ (data) { cout << "Base()" << endl; } virtual ~Base () { cout << "~Base()" << endl; } protected : int dataa_; };
这里发生的是动态绑定 ,pb是Base类型指针,指向派生类对象(存在虚函数),所以这里发生动态绑定;由于派生类的虚析构函数重写了基类的虚析构函数,所以这里调用的是派生类的析构函数。
虚继承与虚基类 普通继承
1 2 3 4 5 6 7 8 9 10 11 12 class Base { public :private : int ma_; }; class Derive :public Base{ public :private : int mb_; };
虚继承
1 2 3 4 5 6 7 8 9 10 11 12 class Base { public :private : int ma_; }; class Derive :virtual public Base{ public :private : int mb_; };
virtual方法修饰:
修饰的是成员方法则是虚函数
可以修饰继承的方式,是虚继承。被虚继承的类,成为虚基类。
调用函数查看这两个类的内存分布情况
1 2 cl test.cpp /d1reportAllClassLayout
总结: 当一个类中有虚函数存在时,则有vfptr(虚函数指针),指向vftable(虚函数表),其中包含RTTI信息、虚函数地址等信息; 当派生类从基类虚继承数据,则存在vbptr(虚基类指针),指向vbtable(虚基类表),其中分别表示vbptr指针和虚基类数据在派生类对象的偏移量。 并且可以观察到,当有虚基类存在的时候,虚基类部分是存在于派生类的末尾部分。 从上图分析可知,普通继承到虚继承的过程(派生类中),将虚继类移到派生类的末尾,并在原来虚基类的地方加上一个vbptr,指向虚基类表。
虚继承与虚函数结合 虚函数+公有继承函数 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Base { public : virtual void show () { cout << "Base::show()" << endl; } private : int ma_; }; class Derive : public Base{ public : void show () { cout << "Derive::show()" << endl; } private : int mb_; }; int main () { Base* p = new Derive (); p->show (); delete p; return 0 ; }
执行上述代码是没有问题的
虚函数+虚继承 : 当加入虚继承后,就报错 有打印信息可以知道,p->show()是被成功调用的,出错的地方在delete p部分; 查看Derive虚继承前后的内存布局,如下图所示(这里我是根据打印结果绘制的一份示意图,便于理解) 可以看到,当存在虚继承是,虚基类的部分是被放到了派生类的末尾部分,而这时候定义的Base类型指针(始终指向基类部分),因为基类部分存在vfptr,所以这里可以成功的动态绑定,调用Derive::show()函数; 但是当析构的时候,由于基类指针p2指向的是派生类末尾的基类部分数据,析构的时候只能析构(p2到末尾的数据),而派生类的真正起始位置到p2位置的内存数据,无法被有效析构。 我们在基类中重载delete函数,在派生类中重载new函数,并打印对应的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Base { public : virtual void show () { cout << "Base::show()" << endl; } void operator delete (void * ptr) { cout << "Base类删除的内存空间起始地址为:" << ptr << endl; free (ptr); } private : int ma_; }; class Derive : virtual public Base{ public : void show () { cout << "Derive::show()" << endl; } void * operator new (size_t size) { void * p = malloc (size); cout << "Derive类开辟的内存空间起始地址为:" << p << endl; return p; } private : int mb_; }; int main () { Base* p = new Derive (); cout << "main p:" << p << endl; p->show (); delete p; return 0 ; }
打印的结果为: 这里表明派生类开辟的实际内存空间和删除的实际起始空间相差16个字节,vbptr+mb_(8+4=12—>>>内存对齐,16字节)。这是导致这里错误的主要原因。
vfptr指针在派生类中的哪个位置 总结 不存在虚继承 ①基类存在虚函数,vfptr位于派生类的基类对象部分,且位于派生类的起始地址; ②基类不存在虚函数,派生类中有虚函数,vfptr位于派生类的独有部分,且位于派生类的起始地址;
存在虚继承 ③基类存在虚函数,vfptr位于派生类的虚基类部分,位于派生的末尾部分; ④ 基类不存在虚函数,派生类中有虚函数,vfptr位于派生类的独有部分,且位于派生类的起始地址;
多继承 菱形继承 多继承的好处是可以更多的代码复用;缺点是派生类对象有多份间接基类的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class A { public : A (int data) :ma_ (data) { cout << "A()" << endl; } ~A (){ cout << "~A()" << endl; } private : int ma_; }; class B : public A{ public : B (int data) :A (data),mb_ (data) { cout << "B()" << endl; } ~B () { cout << "~B()" << endl; } private : int mb_; }; class C : public A{ public : C (int data) :A (data), mc_ (data) { cout << "C()" << endl; } ~C () { cout << "~C()" << endl; } private : int mc_; }; class D : public B,public C{ public : D (int data) :B (data), C (data), md_ (data) { cout << "D()" << endl; } ~D () { cout << "~D()" << endl; } private : int md_; }; int main () { D d (10 ) ; return 0 ; }
查看D的内存布局发现确实有两个dataa_变量,调用了两次A的构造函数。 如何解决呢,对所有基类的继承采用虚继承,即将A变成虚基类
并且由于类B、类C采用的是虚继承,使得D只包含一个A的子对象,D必须直接调用A类的构造函数才能初始化A的成员变量。 即
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class B : virtual public A{ public : B (int data) :A (data), mb_ (data) { cout << "B()" << endl; } ~B () { cout << "~B()" << endl; } private : int mb_; }; class C : virtual public A{ public : C (int data) :A (data), mc_ (data) { cout << "C()" << endl; } ~C () { cout << "~C()" << endl; } private : int mc_; }; class D : public B, public C{ public : D (int data) :A (data), B (data), C (data), md_ (data) { cout << "D()" << endl; } ~D () { cout << "~D()" << endl; } private : int md_; };
构造与析构顺序为
半圆形继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class A { public : A (int data) :ma_ (data) { cout << "A()" << endl; } ~A (){ cout << "~A()" << endl; } private : int ma_; }; class B : public A{ public : B (int data) :A (data),mb_ (data) { cout << "B()" << endl; } ~B () { cout << "~B()" << endl; } private : int mb_; }; class C : public A,public B{ public : C (int data) :A (data) ,B (data),mc_ (data) { cout << "C()" << endl; } ~C () { cout << "~C()" << endl; } private : int mc_; }; int main () { C c (10 ) ; return 0 ; }
将B和C改为虚继承A,即使得A变为虚基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class B : virtual public A{ public : B (int data) :A (data), mb_ (data) { cout << "B()" << endl; } ~B () { cout << "~B()" << endl; } private : int mb_; }; class C : virtual public A, public B{ public : C (int data) :A (data), B (data), mc_ (data) { cout << "C()" << endl; } ~C () { cout << "~C()" << endl; } private : int mc_; };
构造和析构顺序为:
多态 静态(编译时期)多态:
函数重载,bool compare(int a,int b),bool compare(double a,double b);
模板(函数模板、类模板)
动态(运行时期)多态: 虚函数机制,调用哪个函数在运行时决定。 基类指针(引用)调用哪个派生类对象,就会调用该派生类对象方法,称为多态。 如代码所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Animal { public : Animal (string name):name_ (name){} virtual void bark () {} protected : string name_; }; class Cat :public Animal{ public : Cat (string name) :Animal (name) {} void bark () { cout << name_ << "bark:miao miao~~~" << endl; } }; class Dog :public Animal{ public : Dog (string name) :Animal (name) {} void bark () { cout << name_ << "bark:wang wang~~~" << endl; } }; class Pig :public Animal{ public : Pig (string name) :Animal (name) {} void bark () { cout << name_ << "bark:heng heng~~~" <<endl; } }; void bark (Animal* animal) { animal->bark (); } int main () { Cat cat ("加菲猫" ) ; Dog dog ("汪汪队" ) ; Pig pig ("佩奇" ) ; bark (&cat); bark (&dog); bark (&pig); return 0 ; }
这里bark函数根据传入的派生类类型,通过基类指针指向,从而实现调用不同派生类的函数。
抽象类和普通类的区别
普通类是用于抽象一个实体的类型,比如这里的Cat类、Dog类、Pig类等; 而这里的Animal作为抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Animal { public : Animal (string name):name_ (name){} virtual void bark () =0 ; protected : string name_; };
拥有纯虚函数的类,叫做抽象类。 抽象类不能实例化对象(抽象类并不是为了抽象某个类型而存在的。),但是可以定义指针和引用变量。
if (hexo-config('comment') && hexo-config('comment.enable') == true && hexo-config('comment.use')) {
if (hexo-config('comment.use') == "valine") {
@import "./valine.styl"
}
else if (hexo-config('comment.use') == "gitalk") {
@import "./gitalk.styl"
}
else if (hexo-config('comment.use') == "twikoo") {
@import "./twikoo.styl"
}
else if (hexo-config('comment.use') == "waline") {
@import "./waline.styl"
}
}
.comments-container {
display inline-block
width 100%
margin-top var(--component-gap)
.comment-area-title {
width 100%
color var(--text-color-3)
font-size 1.38rem
line-height 2
i {
color var(--text-color-3)
}
+keep-tablet() {
font-size 1.2rem
}
}
.configuration-items-error-tip {
display flex
align-items center
margin-top 1rem
color var(--text-color-3)
font-size 1rem
i {
margin-right 0.3rem
color var(--text-color-3)
font-size 1.2rem
}
}
.comment-plugin-fail {
display none
flex-direction column
align-items center
justify-content space-around
width 100%
padding 2rem
.fail-tip {
color var(--text-color-3)
font-size 1.1rem
}
.reload {
margin-top 1rem
}
}
.comment-plugin-loading {
flex-direction column
padding 1rem
color var(--text-color-3)
.loading-icon {
color var(--text-color-4)
font-size 2rem
}
.load-tip {
margin-top 1rem
color var(--text-color-4)
font-size 1.1rem
}
}
}