下面程序输出什么?
#include <iostream> using namespace std; class A { public: virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val = 0) { std::cout << "B->" << val << std::endl; } }; int main(int argc, char* argv[]) { B* p = new B; p->test(); return 0; }
首先,创建的是子类对象,子类对象去调用虚函数test(),然后里面是调用func(),这里要注意,是一个多态调用,因为test成员函数是属于A类的,调用func函数是通过this指针去调用(就算是test函数被子类继承了,内部的this指针也不会被更换,还是A类的this指针),并且func函数也进行了重写,在main函数中调用的也是子类对象,所以走向的是B类中的func函数。
这里最让我们疑惑的就是为什么是1不是0,这里就涉及到了只继承接口,所以val的缺省值还是1。
但是子类的缺省参数并不是一点用处都没有,当普通调用的时候这个缺省参数就可以使用了。
再看一个程序:选哪个?
#include <iostream> using namespace std; class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
数据模型大概是这样的:
所以选C。
这里注意一下:其实继承的对象在内存里是从下面开始放,因为下面是低地址,上面是是高地址,我们经常能看到一个数组,用数组名+n就能到对应的位置,这就是为什么从低地址放的原因,加就代表要到高地址。
多态原理
虚函数表
先来研究一下这个类的大小:32位环境下
#include<iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; }; int main() { cout << sizeof(Base) << endl; return 0; }
这里明明只有一个成员变量,之前说过成员函数并不在类中,可是为什么结果是8呢?
这里多出来了一个_vfptr,这个叫做虚表/虚函数表,里面储存的是虚函数的地址。
#include<iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } void Func3() { cout << "Func3()" << endl; } private: int _b = 1; }; int main() { cout << sizeof(Base) << endl; Base a; return 0; }
原理与动静态绑定
多态的原理一定跟虚表有着千丝万缕的联系。
再来看看完成重写有什么区别;
#include<iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
这里虚表也变了,之前重写也可以叫做覆盖,这里就是覆盖的部分。
其实重写只是语法上的,继承了父类的接口,重写了实现部分。覆盖就是覆盖了父类继承过来重写的虚函数的地址。
那么我们这样调用试一下:
多态调用更长。
这里差别就在于,根本不在乎是指向哪里,因为有虚表的存在,如果指向父类就去父类的虚表中找,如果指向子类就去子类的虚表中找。
在汇编当中eax里面存的就是虚表指针数组。
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
那么虚表是放在哪一个位置呢?
打印出来的地址和常量区非常接近,所以是在常量区。
单继承与多继承关系的虚函数列表
单继承的虚函数表
#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; int main() { Base b; Derive d; return 0; }
在VS当中其实并不能看到虚表当中所有的虚函数,这时VS编译器的一个优化,也可以看作是一个BUG。
这个时候我们可以用内存窗口去看。
这里也将func3和func4的函数地址给显示出来,顺便说一下,在VS编译器下,虚表是以空指针结尾的。
但是这样看有些麻烦,我们想个办法给他打印出来。
#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; typedef void(*p)(); void PrintVFTbale(p vft[])//打印虚表 { for (int i = 0; vft[i]; i++) { printf("[%d]:%p->", i, vft[i]);//打印虚表当中每个数组的内容,也就是每个虚函数的地址 vft[i]();//调用对应的函数 } } int main() { Base b; Derive d; PrintVFTbale((p*)(*(int*)&b));//将虚表的地址传过去 PrintVFTbale((p*)(*(int*)&d)); return 0; }
这里还可以改进,因为有时候是64位和32位,到时候64位就是取头8个字节了。
其实只需要将里面的变成二级指针就行了(任何类型的二级指针都可以),因为二级指针是储存一级指针的,解引用之后再去看解引用多大时,剩下的就是一级指针,一级指针就可以根据平台位数变化了,到时候就对应了64位和32位的平台大小了。