C++ 继承和多态
目录
- 继承的本质和原理
- 派生类的构造过程
- 重载,覆盖,隐藏
- 静态绑定 动态绑定
- 多态的vfptr 和 vftable
- 抽象类设计原理
- 多重继承以及问题
- 虚基类 vbptr 和vbtable
- RTTI
一 继承的本质和原理
- 基类给派生类提供统一的公共属性(成员方法,成员属性),通过继承达到代码复用
- 基类可以给派生类提供统一的虚函数接口,派生类通过函数重写,达到多态调用的目的
继承方法,访问权限 public protected private 结论:
- 基类的private,无论哪种继承方式,在派生类都可以继承下来,但是无法访问
- 基类的protected,派生类可以访问,外部不能访问
- 默认继承方式: 如果是class 定义的 默认为private 若是struct 则是public
注意 :
class A { public: int ma; protected: int mb; private: int mc; }; class B : private A { public: int md; protected: int me; private: int mf; }; class C : public B { public: int mg; protected: int mh; private: int mi; };
此时 类C中 对ma的访问不再是 public 而是要看直接基类的访问权限,因为B 对A 的继承是private 因此 在类C 中ma 访问就是private。
二 派生类的构造过程
必须调用基类的构造函数构造 并且优先构造基类
构造与析构 的顺序
1.先调用基类构造函数,构造从基类继承来的成员
2.再调用派生类自己的构造函数,构造派生类自己的成员
3.先调用派生类自己的析构函数,释放派生类自己占用的外部资源
4.调用基类的析构函数,释放基类部分成员占用的外部资源
三 重载 隐藏 覆盖
重载 : 在同一个作用域,函数名相同,参数列表不同,可构成重载函数
隐藏 :如果派生类与基类的同名函数,当调用派生类的函数时,默认会调用派生类的函数,发生了派生类函数对基类的函数隐藏,如果调用基类的同名函数,需要加上基类作用域
覆盖 :指的是基类和派生类的同名函数,不但函数名相同,参数列表也相同,返回值也相同。并且基类是virtual 虚函数,那么派生类的函数会被处理成虚函数,覆盖是指在函数表中的函数地址的覆盖
四 静态绑定 动态绑定
class A { public : void show() {cout<<"A::show"<<endl;} virtual void show(int a) { cout<<"A::show(int)<<endl;} private: int ma; }; class B : public A { public : void show() {cout<< " B:show()"<<endl}
void show(int a) { cout<<"B::show(int)<<endl;}
private: int mb; }; int main() { B b; A * p = &b; p ->show(); // 静态绑定 p->show(10); // 动态绑定 }
那么 当调用show()时,此时p 是A* 类型 当调用方法show 时 就会去类A中 找相应的函数,在汇编层面就是call 0x1234 函数地址,并且在编译时期就确定了函数地址,因此发生了静态的绑定
当调用 show(10)时,先去类A 中找show(int )函数 但是A中的show(int) 是虚函数,那么就访问p指向的对象前4字节 vfptr,然后访问到vfptr指向的vftable,然后在vftable中找到虚函数地址在调用,并且只有在运行的时候才能获取到虚函数地址,因此是发生的是动态绑定
p 是A类型 *p 识别的是RTTI 类型,因为A 中有虚函数,因此获取vftable 获取RTTI类型,如果A 中没有虚函数,那么*p 识别的是编译期时期的类型
五 虚函数,vfptr,vftable
如果基类中Base有虚函数,那么编译期在编译时期会为该类型生成一张虚函数表,虚函数表中放着虚函数的地址,还有RTTI 指针信息,这张表在运行时被加载到.rodata,并且只能读 不能写。
那么用这个Base类实例化的对象,前4 字节会放着vfprt 虚函数指针,指向虚函表的地址
总结
1.类中出现虚函数,编译阶段会给该类型产生虚函数表,里面存放了虚函数的地址和RTTI指针。
2.有虚函数的类实例化的对象,内存都多了一个vfptr虚函数指针,指向该对象类型的虚函数表,同类型对象都有自己的vfptr,但是它们共享一个vftable。
3.派生类如果提供了同名覆盖函数,那么在虚函数表中,需要把基类继承来的虚函数地址给覆盖掉。
4.一个类里面出现多个虚函数,对象的内存只增长4个字节(vfptr),但是虚函数表的大小会逐渐增大。
多态:
静态时期的多态 : 模版,函数重载
运行时期的多态: 指基类指针指向派生类,调用派生类同名覆盖函数,基类指针指向哪个派生类,就会调用该派生类的同名覆盖函数,因为基类指针调用派生类的方法时,发生动态绑定,访问了该对象指向的虚函数表,并且取出对应的虚函数地址,因此指向谁调用谁的方法
六 ,抽象类
class Animal { public: Animal(string name) :_name(name) {} virtual void bark() = 0; // 纯虚函数 protected: string _name; };
理论上类中拥有纯虚函数的类就是抽象类,抽象类不能定义对象,但是可以定义指针和引用,
1.基类给所有派生类提供公共的属性(成员变量)和方法(成员函数),通过继承达到代码复用的目的。
2.基类可以给所有派生类提供统一的纯虚函数接口,派生类通过函数重写,达到多态调用的目的。
虚析构函数
class Base // 基类定义 { public: Base(int data=10):_ptrb(new int(data)) { cout << "Base()" << endl; } ~Base() { delete _ptrb; cout << "~Base()" << endl; } protected: int *_ptrb; }; class Derive : public Base // 派生类定义 { public: Derive(int data=20):Base(data), _ptrd(new int(20)) { cout << "Derive()" << endl; } ~Derive() { delete _ptrd; cout << "Derive()" << endl; } private: int *_ptrd; }; int main() { Base *p = new Derive(); delete p; // 只调用了Base的析构函数,没有调用Derive派生类的析构函数 return 0; }
在delete p 的时候,因为基类的析构函数时普通函数,因此只是发生了静态绑定,只调用了Base的析构函数,没有调用Derive 的析构函数 尤其这种在堆上的对象,因此将基类析构函数修改成虚析构函数,这样就是动态绑定,因此堆上的内存就能释放
七,多重继承
一 虚基类
class A { public: private: int ma; }; class B : virtual public A { public: private: int mb; };
被虚继承的类就是虚基类 A为虚基类
B内存布局
如果不被虚继承 那么B内存就是 A::ma , mb 那么被虚继承之后 将基类中成员搬到最后的位置,并且在原位置添加vbptr 指向虚基类表,在虚基类表(vbtable 记录了虚基类的偏移量)
当基类指针指向派生类时,指针指向的永远是基类成员的内存地址,因此存在虚基类 ,并且指向派生类对象在堆上,那么不同编译期析构可能出现内存泄漏
C++ 多重继承
菱形继承
这样类D 具有了两份类A 中 成员变量 存在问题
解决办法 :
所以从A继承的都要虚继承 virtual 这样 D中只具有A 中的一份数据 这样A的构造函数 也是在D中调用,不会再是B,C调用