class Occupation
{
public:
Occupation(){};
virtual void show() { std::cout << "Occupation::show()" << std::endl; }
virtual void show(int) { std::cout << "Occupation::show(int)" << std::endl; }
private:
int name_;
};
虚函数
被virtual关键字修饰的函数称为虚函数。
虚函数表 vftable
一个类中,存在被virtual修饰的成员方法,那么,编译器在编译该类时,会产生一张虚函数表,用来记录类中虚函数的地址;
虚函数表是属于该类型的,该类型实例化的所有对象共享该虚函数表;
虚函数表存放在进程虚拟地址空间的.rodata段,可读不可写。
虚函数表地址 vfptr
当我们使用该类实例化一个对象时,该对象的内存中前会多出四个字节,存放vfptr,而vfptr指向的是该类的虚函数表vftable。
int main()
{
Occupation o;
std::cout << sizeof o << std::endl; // 8
return 0;
}
虚函数表中的内容
- RTTI: run-time type information,属于哪个类的虚函数表,就存储了哪个类的类型的常量字符串的地址,比如Occupation类的RTTI为
"Occupation"
的地址; - vfptr在对象内存中的偏移值:一般为0
- 该类的虚函数的函数地址
继承体系中:重写/覆盖
class Student : public Occupation
{
public:
Student(){};
void show() { std::cout << "Student ::show()" << std::endl; }
std::string name() { return name_; }
};
如果派生类中存在方法A和基类的虚函数B的函数名,返回值,以及参数列表一致时,派生类的该方法会自动被处理成虚函数。
并且,将继承来的基类的虚函数表中的B的函数入口地址,覆盖为A的入口地址,该过程称为覆盖,或者也叫重写。
Occupation类的虚函数表的内容:
Student类的虚函数表的内容:
动态绑定:
int main()
{
Student s;
Occupation *op = &s;
op->show();
op->show(100);
return 0;
}
判断何时静态绑定合适动态绑定:
- 指针op是基类的指针,如果基类Occupation的show()不是一个虚函数,那么将进行静态绑定,即在编译期就确定调用的是基类的show().
call Occupation::show()
因为指针op的类型是Occupation,所以能访问的内容是派生类继承来的基类的那部分内容。
- 如果基类Occupation的show()是一个虚函数,那么将进行动态绑定,此时,生成的汇编指令大概是:
move eax, dword ptr[op]
move ecx, dword ptr[eax]
call exc
寄存器ecx中值是多少,只有在运行的时候才能知道,所以称之为动态绑定。
存在虚函数是对于指针变量的类型识别的影响:
int main()
{
Student s;
Occupation *op = &s;
std::cout << typeid(op).name() << std::endl;
std::cout << typeid(*op).name() << std::endl;
return 0;
}
输出结果:
P10Occupation
7Student
结果分析:
第一个打印:
C++是静态语言,定义时是什么类型,运行时就是什么类型,不会变,所以没问题op是Occupation类型的指针;
第二个打印:
上面我们知道op是Occupation类型的指针,我们需要判断*op是什么类型时,需要看Occupation中有没有虚函数
如果没有虚函数,那么*op识别的就是编译期的类型,即Occupation类型;
如果有虚函数,*pb识别的时运行时的类型,即RTTI类型。op指向的是一个派生类对象s,根据s的前四个字节访问到的时派生类的虚函数表,派生类的虚函数表中存放的是派生类的类型,即"class Student"。