单继承的虚函数重写
既然我们知道对于一个有虚函数的对象来说,它的头四个字节是虚函数表指针,那么如果我们拿到这个虚函数表指针就可以找到这个虚函数表。这四个字节中存储的是地址,我们可以取地址,然后将这个地址强转成int*,对这个指针解引用就可以取得前四个字节的内容也就是虚函数表地址。
PrintVirtable这个函数拿到虚函数表地址之后,开始从这个地址打印,直到虚函数表内容为nullptr。每拿到一个虚函数地址,将它调用赋值给FUNC函数指针对象,再通过调用该指针对象的构造函数,将函数显示打印出来。
#include<iostream>
using namespace std;
class Father{
virtual void fun1(){ cout << "Father fun1()" << endl; }
virtual void fun2(){ cout << "Father fun2()" << endl; }
};
class Son :public Father
{
virtual void fun1(){ cout << "Son fun1()" << endl; }
virtual void fun3(){ cout << "Son fun3()" << endl; }
};
typedef void(*FUNC) (); //函数指针
void PrintVirtable(FUNC table[])
{
cout << "Virtual Addr " << table<<endl;
for (int i = 0; table[i] != nullptr; ++i)
{
printf("第%d个虚函数 %p->", i, table[i]);
FUNC f = table[i];
f();
}
cout << endl;
}
int main()
{
Son s;
FUNC* table = (FUNC*)(*((int*)&s));
//取出f的地址,强制转换成int*得到f对象前四个字节,其内容是虚函数表地址
PrintVirtable(table);
system("pause");
return 0;
}
这些虚函数在子类中的顺序是:子类重写(覆盖)了父类的虚函数fun1(),继承了父类的虚函数fun2(),拥有自己虚函数的fun3()。不难发现虚函数表的的虚函数地址是按照函数声明顺序存放的。

多继承中的虚函数重写
#include<iostream>
using namespace std;
class Father{
virtual void fun1(){ cout << "Father fun1()" << endl; }
virtual void fun2(){ cout << "Father fun2()" << endl; }
};
class Mother{
virtual void fun1(){ cout << "Mother fun1()" << endl; }
virtual void fun2(){ cout << "Mother fun2()" << endl; }
};
class Son :public Father, public Mother
{
virtual void fun1(){ cout << "Son fun1()" << endl; }
virtual void fun3(){ cout << "Son fun3()" << endl; }
};
typedef void(*FUNC)();
void PrintVirtable(FUNC table[])
{
cout << "Virtual Addr " << table<<endl;
for (int i = 0; table[i] != nullptr; ++i)
{
printf("第%d个虚函数 %p->", i, table[i]);
FUNC f = table[i];
f();
}
cout << endl;
}
int main()
{
Son s;
FUNC* table1 = (FUNC*)(*((int*)&s));
PrintVirtable(table1);
FUNC* table2 = (FUNC*)(*(int*)((char*)&s+sizeof(Father)));
PrintVirtable(table2);
system("pause");
return 0;
}

两个父类Father、Mother都存在虚函数func1,子类对这两个虚函数都进行了重写。也就是说子类中不存在Father和Mother的虚函数func1了,因为这两个位置都被son的func1覆盖了。

通过面试题看多态
1、 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
为什么普通对象调用普通函数和虚函数是一样快的?
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "Virtual BuyTicket" << endl; }
void SaleTicket()
{
cout << "NonVirtual SaleTicket" << endl;
}
};
int main()
{
Person p;
p.BuyTicket(); //对象访问虚函数
p.SaleTicket(); //对象访问普通函数
system("pause");
return 0;
}

这里我们看到,对象在去调用虚函数和普通函数的时候,都是直接call对象的成员函数,也就是说,虚函数这样用,它就没有意义,与普通函数一样。
为什么对象指针/引用调用普通函数比调用虚函数快?
为了让大家体验对象指针调用虚函数时候的查找过程,我又增加了两个虚函数。
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket1() { cout << "Virtual BuyTicket1" << endl; }
virtual void BuyTicket2() { cout << "Virtual BuyTicket2" << endl; }
virtual void BuyTicket3() { cout << "Virtual BuyTicket3" << endl; }
void SaleTicket()
{
cout << "NonVirtual SaleTicket" << endl;
}
};
void Func(Person *p)
{
(*p).BuyTicket3();
(*p).SaleTicket();
}
int main()
{
Person p;
Func(&p);
system("pause");
return 0;
}

正因为对象对象指针调用虚函数时候需要到虚函数表去查找虚函数,所以,指针调用虚函数比普通函数慢。
2、虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的。利用下面这段代码测试虚函数表存储在哪里?
#include<iostream>
#include<cstdio>
using namespace std;
class Person
{
public:
virtual void BuyTicket(){}
};
class Student :public Person
{
virtual void BuyTicket(){}
};
int main()
{
int a = 0; //栈
int* b = new int(10); //*b存在堆
static int c = 0; //数据段
char* ptr = "Hello World"; //*ptr存储在代码段
Person p;
printf("虚函数地址:%p\n", *(int*)&p);
printf("栈:%p\n", &a);
printf("堆:%p\n", b);
printf("数据段:%p\n", &c);
printf("代码段:%p\n", ptr);
system("pause");
return 0;
}
虚函数表存储在代码段

动态联编 和 静态联编 都是多态性的一种体现。
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
本文详细探讨了C++中虚函数的实现原理,包括单继承和多继承场景下虚函数表的生成与使用,以及如何通过实例演示理解多态性的概念。文章通过代码示例解释了对象访问虚函数与普通函数的区别,揭示了虚函数表的存储位置,并对比了动态联编与静态联编的特点。
3333

被折叠的 条评论
为什么被折叠?



