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;
}

判断何时静态绑定合适动态绑定:

  1. 指针op是基类的指针,如果基类Occupation的show()不是一个虚函数,那么将进行静态绑定,即在编译期就确定调用的是基类的show().
call Occupation::show()

因为指针op的类型是Occupation,所以能访问的内容是派生类继承来的基类的那部分内容。

  1. 如果基类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"。