首先说明,所谓绑定,就是指函数的调用
接下来,我们直接看一段代码来说明问题
class Base
{
public:
Base(int data=10):m_a(data){}
void show(){cout<<"Base::show()"<<endl;}
void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int m_a;
};
class Derive:public Base
{
public:
Derive(int data=20):Base(data),m_b(data){}
void show(){cout<<"Derive:show()"<<endl;}
private:
int m_b;
};
上述代码中,定义了一个Base类和一个Derive类,并且Derive类继承了Base类,其中
Base类中有一组互为重载关系的成员函数show
Derive类中有一个与Base类中同名的成员函数,因此,Derive::show()与Base::show()、Base::show(int)构成了隐藏关系。
静态绑定
接下来,我们写一段测试代码来说明问题,这段测试代码包括
- 定义一个基类指针,并指向其子类对象
- 并使用基类指针调用show成员函数,观察运行结果
- 查看基类指针的类型和基类指针所指对象的类型
void test()
{
Derive d(50);
Base* pb=&d;
pb->show();
pb->show(11);
cout<<"Base size:"<<sizeof(Base)<<endl;
cout<<"Derive size:"<<sizeof(Derive)<<endl;
cout<<typeid(pb).name()<<endl;
cout<<typeid(*pb).name()<<endl;
}
通过实验结果可以看到,尽管基类指针(Base* pb)指向的是基类对象,但是通过pb所调用的函数仍旧是基类作用域下的成员函数。
静态绑定就是指在编译期间就确定好了函数的具体实现版本,由于pb的类型是Base*(也被称为静态类型),因此通过pb所调用的成员函数show在编译期间就被确定在Base作用域下的show成员函数。
- 静态绑定适用于非虚函数和静态函数
- 在静态绑定中,函数调用的实现版本在编译期间就已经确定,无法在运行期间改变
为加深理解,我们再写一例
class Base {
public:
void func() {
cout << "Base::func()" << endl;
}
};
class Derived : public Base {
public:
void func() {
cout << "Derived::func()" << endl;
}
};
int main() {
Base b;
Derived d;
b.func(); // 静态绑定,调用 Base::func()
d.func(); // 静态绑定,调用 Derived::func()
Base* p = &d;
p->func(); // 静态绑定,调用 Base::func(),因为 p 的静态类型是 Base*
return 0;
}
虚函数
接下来,我们对Base类的代码做一点小小的改动
class Base
{
public:
Base(int data=10):m_a(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int m_a;
};
在Base类的成员函数前加一个virtual关键字,此时Base的成员函数就被称之为虚函数
一个类里如果定义了虚函数,那么
- 在编译期间,编译器就会为该类产生一个唯一的vftable虚函数表
- 该vftable虚函数表中主要存储的内容就是RTTI指针和虚函数的地址
- 当程序运行时,每一张虚函数表都被加载到rodata区.(只读)
注:RTTI(run-time type infomation),即运行时的类型信息
以上Base类的虚函数表(vftable)为
除此以外,如果一个类里定义了虚函数,那么
- 该类所定义的对象,其运行时,内存中的开始部分,会多存储一个vfptr虚函数指针,指向该类的虚函数表(存储该虚函数表的首地址)
- 该类所定义的每个对象,都会有一个vfptr指针,但虚函数表只有一张
因此,一个类里虚函数的个数,不影响内存的大小(对象内存中只有一个虚函数指针vfptr),影响的是虚函数表的大小
此外,如果派生类中的某个成员函数和基类中某个成员函数完全相同(包括函数名、函数类型),只有函数体的实现不同,那么该成员函数也将自动被处理为虚函数。
class Base
{
public:
Base(int data=10):m_a(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int m_a;
};
class Derive:public Base
{
public:
Derive(int data=20):Base(data),m_b(data){}
void show(){cout<<"Derive:show()"<<endl;}
private:
int m_b;
};
void test()
{
Derive d(50);
Base* pb=&d;
pb->show();
pb->show(11);
cout<<"Base size:"<<sizeof(Base)<<endl;
cout<<"Derive size:"<<sizeof(Derive)<<endl;
cout<<typeid(pb).name()<<endl;
cout<<typeid(*pb).name()<<endl;
}
再次观察上述修改后的代码,在测试函数中查看运行结果
可以看到,无论是Base还Derive,其大小都是16B,而不再是4B,其原因就在于,Base类中除了有一个int类型的成员变量外还有一个占8B的vfptr,又根据内存对齐原则,故而Base类的大小就是8(vfptr)+4(int)+4(内存对齐)=16B
覆盖
接下来,我们将目光转向上述代码的Derive类。
上边说到,由于Derive子类中出现了与Base父类完全相同的成员函数(void show()),因此编译器自动将其声明为虚函数,因此我们知道,编译器也将在编译阶段为Derive生成一张唯一的虚函数表vftable,因此在测试代码中定义子类对象d时,d也将拥有一个虚函数指针vfptr
动态绑定
接下来,我们再来看测试代码中这行代码的运行结果的差异
pb->show();
可以看到,在成员函数show没有被声明为virtual之前,该行代码执行的是Base::show(),而当其被声明为虚函数后,执行结果就成为了Derive::show()
这是因为,代码在执行到pb->show()时,如果发现show不是虚函数,就进行静态绑定,如果发现show是虚函数,就进行动态绑定。
所谓动态绑定,实质上是因为其汇编过程为
mov eax dword ptr[pb]
mov ecx dword ptr[eax]
call ecx(虚函数的地址)
第一行汇编代码执行:将指针pb指向的地址(虚函数表的首地址)放到寄存器eax中
第二行汇编代码执行:将eax中的前四个字节的地址(也就是对应show()函数的地址)放到ecx寄存器中
第三行汇编代码执行:执行ecx寄存器中的代码
从上述汇编过程可以看到,由于我们执行的是ecx寄存器中的代码,但是ecx中保存的地址需要等到运行时期才能确定。
这种在程序运行时需要根据对象的实际类型来确定调用哪个方法或函数的机制就叫动态绑定
接下来,我们再来看指针pb和*pb的类型变化
cout<<typeid(pb).name()<<endl;
cout<<typeid(*pb).name()<<endl;
可以看到,
- pb的类型:无论是否有虚函数,基类指针pb的类型永远都是Base
- *pb的类型:
- 如果Base有虚函数,*pb识别的就是运行时期的类型(RTTI类型)
- 如果Base没有虚函数,*pb识别的就是编译时期的类型(Base类型)