文章目录
继承的本质和原理
本质:代码的复用
派生类的构造过程
1、派生类(不能继承)基类的构造和析构,或者说只能在特定地区调用。
派生类如何初始化从基类继承来的成员?
答:派生类通过调用基类相应的构造函数。
派生类的成员,由派生类的构造和析构负责初始化和清理
基类的成员(派生类继承来的),由基类的构造和析构负责初始化和清理。
class Derive : public Base
{
Derive(int data) : Base(data), mb(data) {};
........
}
int main()
{
Derive d(20);
}
调用:
虚函数,静态绑定,动态绑定
静态绑定
class Base
{
public:
Base(int data = 10) :ma(data) {}
void show() { cout << "Base::show()" << endl; }
void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20) :Base(data), mb(data) {}
void show() { cout << "Derive::show()" << endl; }
private:
int mb;
};
int main()
{
Derive d(50);
Base *pb = &d;
pb->show(); // 静态(编译时期)的绑定(函数的调用)call Base::show (01612DAh)
pb->show(10); //静态绑定 call Base::show (01612B2h)
cout << sizeof(Base) << endl; // 4
cout << sizeof(Derive) << endl; // 8
cout << typeid(pb).name() << endl; // class Base*
cout << typeid(*pb).name() << endl; // class Base class Derive
return 0;
}
其中静态绑定,也就是编译期间的绑定,发生在函数的调用的时期:
pb->show();
编译生成的汇编码为call Base::show (01612DAh)
如果发现show是普通函数,就进行静态绑定
虚函数与动态绑定
基类
总结一:
一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(常量只读区)。
总结二:
一个类里面定义了函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表。
总结三:
一个类里面虚函数的个数,不影响对象内存大小(存的是指针vfptr),影响的是虚函数表的大小。
派生类
总结四:
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数,并覆盖虚函数表中原基类虚函数的地址。
重写=覆盖
总结五:
虚函数的依赖
1、虚函数能产生地址,存储在vftable当中
2、对象必须存在(vfptr->vftable->虚函数地址)
要写成虚函数=》把函数地址存到虚函数表中
调用虚函数过程:
获取对象的内存获取虚函数表的指针=》再访问虚函数表=》取得虚函数的地址
class Base
{
public:
Base(int data = 10) :ma(data) {}
// 虚函数
virtual void show() { cout << "Base::show()" << endl; }
// 虚函数
virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20) :Base(data), mb(data) {}
/*
总结四:
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,
而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数
重写《=》覆盖
*/
void show() { cout << "Derive::show()" << endl; }
private:
int mb;
};
int main()
{
Derive d(50);
Base *pb = &d;
/*
pb->Base Base::show 如果发现show是普通函数,就进行静态绑定 call Base::show (01612DAh)
pb->Base Base::show 如果发现show是虚函数,就进行动态绑定了
mov eax, dword ptr[pb]
mov ecx, dword ptr[eax]
call ecx(虚函数的地址) 动态(运行时期)的绑定(函数的调用)
*/
pb->show(); //静态(编译时期)的绑定(函数的调用)call Base::show (01612DAh)
/*
Base::show(int) 是一个虚函数,此时就得动态绑定了
mov eax, dword ptr[pb]
mov ecx, dword ptr[eax]
call ecx(虚函数的地址) 动态(运行时期)的绑定(函数的调用)
*/
pb->show(10); //静态绑定 call Base::show (01612B2h)
cout << sizeof(Base) << endl; // 4 8
cout << sizeof(Derive) << endl; // 8 12
cout << typeid(pb).name() << endl; // class Base*
/*
pb的类型:Base -> 有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型 *pb <=> Base类型
如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型
pb->d(vfptr)->Derive vftable class Derive
*/
cout << typeid(*pb).name() << endl; // class Base class Derive
/*
Base的两个show函数都是普通的函数
Base::show()
Base::show(int)
4
8
class Base *
class Base
*/
/*
给Base的两个show方法定义成virtual虚函数了
Derive::show()
Base::show(int)
8
12
class Base *
class Derive
*/
return 0;
}
虚函数问题
1、哪些函数不能实现为虚函数
- 构造函数不能成为虚函数。完成了构造函数,对象才会存在,在这之前根本没有虚函数表。
- 构造函数中调用任何函数都是静态绑定(包括在构造函数中调用的虚函数)
- 静态成员方法
2、是不是虚函数的调用一定是动态绑定? 不是!
- 上个问题提到,在类的构造函数中调用虚函数,也是静态绑定
- 用对象本身调用虚函数,是静态绑定。动态绑定必须由指针/引用调用虚函数
int main()
{
// 静态绑定
Base b;
Derive d;
b.show(); // 虚函数 call Base::show
d.show(); // 虚函数 call Derive::show
// 动态绑定
Base *pb1 = &b;
pb1->show();
Base *pb2 = &d; // 上到下,Derive *pb3 = &b不行(下到上)
pb2->show();
// 引用也一样,动态绑定
Base &rb1 = b;
rb1.show();
Base &rb2 = d;
rb2.show();
/*
mov eax, dword ptr [pb2] // 从对象的头取得虚函数表指针
mov ecx, dword ptr [eax] // 从虚函数表中取虚函数的指针
call ecx // call寄存器
*/
// 派生类指针引用也一样,动态绑定
Derive *pd1 = &d;
pd1->show();
Derive &rd1 = d;
rd1.show();
}
虚析构函数
1、为什么析构函数可以是虚?
析构函数调用的时候,对象是存在的
2、什么是时候必须将基类的析构实现为虚函数?
基类的指针(引用)指向堆上new出来的派生类对象,deleta pb(基类的指针)调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
换而言之,在父类中如果有任何一个虚函数,那么它的析构函数也必须是虚函数。否则,在删除子类对象时,只会调用父类的析构函数,而不会调用子类的析构函数,会造成内存泄漏。
如果基类的析构函数是虚函数,那么派生类的析构函数自动成为虚函数(虽然名字不一样)
多态
如何解释多态
静态(编译时期)的多态:函数重载、模板(函数模板和类模板)
bool compare(int, int) {};
bool compare(double, double) {}
compare(10,10)
call compare_int_int 在编译阶段就确定好调用的函数版本。
template<typename T>
bool compare(T a, T b) {};
campare<int>(10, 20); => 实例化一个 campare<int>
动态(运行时期)的多态:
在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。
多态底层是通过动态绑定来实现的:
pbase => 访问谁的虚函数表指针 => 访问谁的虚函数表 => 调用对应的派生类对象的方法
应用:
使用基类指针进行
//不满足开-闭原则,不这么写
void bark(Cat &cat)
{
cat.bark(): // Cat::bark()是虚函数,动态绑定
}
-----------------------------------------------------------------
void bark(Animal *p) // 满足开-闭原则,不会因为派生类的增加再增加API接口
{
p->bark(): // Animal::bark()是虚函数,动态绑定
/*
p->cat Cat vftable &Cat::bark
.......
*/
}
int main()
{
Cat cat("cat");
Dog dog("dog");
Pig pig("pig");
bark(&cat); // 取cat的虚函数表=》找到cat的虚函数指针
bark(&dog);
bark(&pig);
}
抽象类
本质
定义Animal类的初衷,并不是让Animal取实例化
初衷是:
1、让所有的动物实体类通过继承Animal直接复用该属性。如string _name
2、给所有的派生类保留统一的覆盖/重写接口
拥有纯虚函数的类叫抽象类。抽象类不能实例化对象,但是可以定义指针和引用变量。
问题
1 虚函数表指针的交换
2 虚函数形参
函数调用,参数压栈是在编译时期就确定好的。如果基类和派生类虚函数的默认输入形参不同,则编译器看不到派生类虚函数的参数。所以会出现调用派生类的虚函数,但是用的是基类的默认输入形参。
show结果:
call Derive::show i : 10
3 虚函数的权限
如果基类虚函数权限为public,派生类虚函数的权限为private,以下代码可以正常运行:
访问权限的确定都是发生在程序编译阶段。在程序运行期间才会确认调用到Dervie::show。
4 虚函数指针写入虚函数表的时机
虚函数指针在编译类的构造函数时写入虚函数表。
调用
int main()
{
Base *pb1 = new Base();
/*
mov eax, dword ptr [pb1] // 从对象的头取得虚函数表指针
mov ecx, dword ptr [eax] // 从虚函数表中取虚函数的指针 eax:0x00000000,已经不再是Base::vftable了
call ecx // call寄存器
*/
pb1->show(); // 出错
delete pb1;
Base *pb2 = new Derive();
/*
mov ecx, dword ptr [pb2] // 从对象的头取得虚函数表指针
mov eax, dword ptr [edx] // 从虚函数表中取虚函数的指针
call eax // call寄存器
*/
pb2->show(); // 正确,因为在派生类的构造函数,汇编码重新将show的地址写到了Derive::vftable中
delete pb2;
}