目录
静态绑定和动态绑定实际上是针对C++动多态机制而言的。所谓的绑定指的是函数的绑定,对于静态绑定,其函数的地址是在编译时期就已经确定了,而动态绑定是在运行时期才能确定,在汇编层次实际上是call一个寄存器。静态绑定也是对非虚函数的绑定,动态绑定是对虚函数的绑定。
什么是虚函数?对于类的成员方法在其前面加上virtul关键字进行修饰,那么该成员函数就变成了虚函数。先看一下代码:
class Base {
public:
Base(int data) :_data(data) {}
void show() {cout << "Base show()" << endl;}
private:
int _data;
};
class Derive :public Base {
public:
Derive(int data) :Base(data), _datab(data) {};
void show() { cout << "Derive show()" << endl; }
private:
int _datab;
};
int main() {
Derive d(10);
Base* p = &d;
p->show();
/*
没有虚函数,因此是静态绑定
p->show();
mov ecx,dword ptr [p]
call Base::show (033325Ch)
可以看出来在编译时期就知道了函数的地址033325Ch
*/
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
return 0;
}
其输出结果为:
再看下面代码:
class Base {
public:
Base(int data) :_data(data) {}
virtual void show() {cout << "Base show()" << endl;}
private:
int _data;
};
class Derive :public Base {
public:
Derive(int data) :Base(data), _datab(data) {};
void show() { cout << "Derive show()" << endl; }
private:
int _datab;
};
int main() {
Derive d(10);
Base* p = &d;
p->show();
/*
这里由于show是虚函数,所以是静态绑定
mov ecx,dword ptr [p]
mov eax,dword ptr [edx]
call eax
这里call的是eax,是一个寄存器,具体的值需要在运行时期才能确定
*/
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
return 0;
}
其输出结果为:
上面两段程序唯一的不同之处就在于将基类的成员方法show加上了virtual关键字使其变成了虚函数。但其结果大为不同,到底是为什么呢?我们来一个一个分析,针对第一个程序,基类的大小是4,而基类只有一个int型的变量,这是符合逻辑的,派生类从基类继承来了一个变量加上自己的私有变量其大小为8,这也是符合逻辑的。p是基类的指针,所以其类型为class Base*,对其解引用得到的类型为class Base,自己调用自己的成员方法那肯定是没问题的。那对于第二个程序呢?为啥就不一样了,注意这里有虚函数,首先通过VS所提供的工具查看一下类的内存布局是什么。
其命令为:cl 文件名/d1reportSingleClassLayout类名称
基类的内存布局:
派生类的内存布局:
看到这里就很清晰了基类的大小之所以是8不是4,是因为多了一个vfptr,那么派生类从基类多继承了一个vfptr,加上变量的大小,自然而然就是12了。那下面的vftable呢?让我用一张图来说明一下vfptr和vftable的关系:
这里画出的派生类的对应关系,实际上基类一样如此,只是虚函数表中虚函数地址和RTTI信息不同罢了。虚函数指针(vfptr)指向的是虚函数表中函数的首地址,RTTI(run_time type information)指的是运行时的类型信息,0表示的是虚函数指针的一个偏移量。而此时调用成员方法之所以是派生类的成员方法是因为,基类的成员方法被定义是虚函数,那么此时该函数的绑定就发生就运行时期,即动态绑定。基类的指针指向派生类对象,那么派生类的虚函数指针就要访问自己的虚函数表看有没有相同的成员函数,如果有就访问自己的成员函数,如果没有那么依然还是访问的基类的成员方法。至于此时*p的类型是什么,由于派生类指针访问的使自己的虚函数表,那么RTTI信息就是自己的类类型,即class Derive。
做出总结:
1、一个类里面定义了虚函数,那么在编译阶段,编译器给这个类产生了一个唯一的虚函数表,虚函数表里主要存储了RTTI信息和虚函数的地址,每一张虚函数表都存在内存的.rodata区。
2、一个类里面定义了虚函数,那么这个类定义的对象,在运行时,内存的起始部分,多存储了一个虚函数指针,指向对应类型的虚函数表,一个类定义n个对象,虚函表只有一个,n个对象均指向这一张虚函数表。
3、一个类里面虚函数的个数,不改变对象的大小,改变的是虚函数表的大小;
4、派生类里面有和基类函数名相同、返回值、参数列表均相同的函数,如果基类中该函数是虚函数,那么派生类中该函数自动转化为虚函数。