目录
多态原理
class A
{
public:
virtual void func()
{
cout << "func" << endl;
}
void work() {
int a = 0;
}
int _a = 1;
};
class B : public A
{
public:
virtual void func()
{
cout << "func" << endl;
}
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
虚表重写
在编译时,子类中的虚表就已经被重写完成
查看内存
可以看出来,a的虚表放在a的开头,然后是_a
-
b也是这样,但b虚表存放的A类中的func已经是重写后的地址了(是B类域的func),然后是A中的_a,再然后是_b
原理
重写是语法的叫法,在原理上我们可以把这叫做覆盖
- 因为实际上就是将子类函数的地址覆盖到原先父类函数的位置
运行时
示例
- 注:这里的work()是普通成员函数,func()具有多态性
查看汇编
可以看出来,具有多态性的函数和不具有的函数,在汇编上有很大区别
普通函数调用时
- 直接去对应地址找就行
多态性的函数调用时
- 需要在运行时去对应类型的虚表中拿地址
汇编代码的步骤:
- 将a的指针放在eax中
- [eax] -- 取eax中内容的头4个字节,也就是将a中存放的虚表指针放在edx中
- 然后调用edx里的地址(也就是去虚表中查找了)
以此可以证明:
- 多态的调用,是运行起来后,去对象的虚表中查找的
为什么不能用父类对象调用具有多态性的函数
通过上面两个可以看出来
- 调用多态性函数,需要在它当前指向的类型(也就是实际类型)的虚表中查找函数
如果该对象需要完成该操作
- 内部需要拥有被重写后的虚表
但是子类向父类对象赋值时:
- 是调用了基类的拷贝构造进行的,该基类对象并不和赋值给他的派生类有瓜葛
- 所以,如果要有虚表的话,就得将派生类的拷贝给他
- 但这样一来,原基类对象的虚表就被覆盖了
- 之后,如果有一个单纯的父类对象需要调用自己的函数该怎么办?它会调到子类的函数的
- 所以不能使用父类对象调用
虚函数表
虚表存放位置
虚表的指针是放在对象中某个特定位置的,那虚表本身存放在哪呢?
- 我们可以通过下面的代码来验证一下:
- (一般虚表放在对象的开头,所以我们这里取前四个字节作为虚表指针)
int main() { B b; printf("%p\n", *((int *)&b)); int a = 0; printf("%p\n",&a); cout << "栈:" << &a << endl; // 栈 int *p = new int; cout << "堆:" << p << endl; // 堆 static int c = 2; cout << "静态区:" << &c << endl; const char *str = "hello"; printf("常量区:%p\n", str); return 0; }
结果如上
- 会发现该地址和常量区的变量地址最为接近
- 基本就可以说明,虚表存放在常量区(linux下)
单继承
示例
class A { public: virtual void func() { cout << "func" << endl; } virtual void work() { int a = 0; } int _a = 1; }; class B : public A { public: virtual void func() { cout << "func" << endl; } virtual void work1() { int a = 0; } int _b = 2; }; int main() { A a; B b; return 0; }
当基类和派生类都各自有自己独立的虚函数时
- 会发现在a的虚表中,是有work地址的
- 但素,在b的监视窗口中,只能看到A类的,却没有b自己的虚函数
这里其实可以被认为是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug
- 我们也可以用其他方法,来看到b中虚函数的地址
打印虚函数表
typedef void(*function); //将void返回类型,无参数的函数类型重命名为function void print_vf(function vb[]) { cout << "虚表指针:" << vb << endl; for (int i = 0; vb[i] != nullptr; ++i) { cout << i + 1 << ":" << vb[i] << endl; } } int main() { A a; B b; print_vf((function*)(*(int*)&b)); //先将b对象地址强转成int*,拿到前4个字节的空间 //然后解引用,拿到前4个字节的内容(也就是虚表指针) //再将其转成 存储function类型的数组(虚表本身就是函数指针数组) return 0; }
可以看到,虚表指针正是在b对象的开头位置:
也成功打印出了三个虚函数,前两个是A中的虚函数,第三个是B中的那个:
多继承
示例
下面是一个多继承,C继承了A和B,且A和B中均有func,且在C中进行了重写
class A { public: virtual void func() { cout << "A::func" << endl; } virtual void work() { int a = 0; } int _a = 1; }; class B { public: virtual void func() { cout << "B::func" << endl; } void work1() { int a = 0; } int _b = 2; }; class C : public A,public B { public: virtual void func() { cout << "C::func" << endl; } virtual void work1() { int a = 0; } int _b = 2; }; int main() { A* pa = new A; B* pb = new B; pa = new C; pa->func(); pb = new C; pb->func(); return 0; }
结果也是对的,调出来都是c的func:
但是,当我们看一下两个指针指向对象的虚表:
- 也就是在C类对A类和B类的虚表重写完成后,两个func的地址是不一样的
- 虽然确实调到了同一个函数,但地址却不一样?
- 可以猜测是编译器做出了什么处理,不然没有理由让地址不同
查看汇编
这是调用函数时的汇编(遇事不决看汇编):
- 左边是pa调用的,右边是pb调用的
- 会发现,其实最后他们殊途同归,都会jmp到c中func函数的地址
- 但是,区别就在于,pb调用时,多了很多步骤
ecx中存放的是this指针
- 两个函数调用时,都会将this指针放在ecx中
- 然后左边可以直接call到跳转指令处
- 但是右边call的地方是另一个地址的func,然后将ecx-8,再去往真正的func地址处
- 所以,绕了很大的圈子,就是为了将pb的this指针-8
为什么要修改this的位置
按照c的对象模型,pb指向c中B的部分:
因为此时需要调用C中的func函数(构成了重写,实现是用C类中的)
- 所以需要一个合法的,指向c的this指针
- 所以需要将此时的pb向上挪sizeof(A),也就是汇编中的8(一个int成员,一个虚表指针)
- (ps:如果不使用c中的函数,该指针就不需要挪动)
this指针的作用
- this指向当前对象的地址,用于访问当前对象的成员变量和成员函数,而不需要指定对象的名称
- 它会根据当前对象的实际类型来引用正确的函数实现
- 所以我们需要传入正确的this指针,防止引用错误
派生类[未重写虚函数指针]的存放位置
我们通过打印虚表,可以看到A类和B类的虚表内容:
typedef void(*function); //将void返回类型,无参数的函数类型重命名为function void print_vf(function vb[]) { cout << "虚表指针:" << vb << endl; for (int i = 0; vb[i] != nullptr; ++i) { cout << i + 1 << ":" << vb[i] << endl; } } int main() { C c; print_vf((function*)*((int*)&c)); //拿到A的虚表地址,它在c的开头 //将指针指向b的位置(要先转成char*,[指针移动]会移动[指向的类型大小]的大小 char* pb = ((char*)&c) + sizeof(A); int* p1 = (int*)pb; //拿到这个位置的前4个字节,也就是虚表地址 function* p = (function*)(*p1); print_vf(p); return 0; }
可以看出来,本来A中只有两个虚函数的,虚表内却存了三个虚函数:
- 说明,C中独立的虚函数放在了A的虚表里
所以,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中