在C++中,多态性的实现和联编(也称绑定)这一概念有关。一个源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运行之前就完成的联编成为静态联编(前期联编);而在程序运行之时才完成的联编叫动态联编(后期联编)。
静态联编支持的多态性称为编译时多态性(静态多态性)。在C++中,编译时多态性是通过函数重载和模板实现的。利用函数重载机制,在调用同名函数时,编译系统会根据实参的具体情况确定索要调用的是哪个函数。
动态联编所支持的多态性称为运行时多态(动态多态)。在C++中,运行时多态性是通过虚函数来实现的。
01,动态多态性的实现
#include<iostream>
using namespace std;
class father
{
public:
virtual void show();
};
class son:public father
{
public:
virtual void show();
};
void father::show()
{
cout << "father" << endl;
}
void son::show()
{
cout << "son" << endl;
}
int main()
{
father f1;
father* pt = &f1;
pt->show();
son s1;
pt = &s1;
pt->show();
}
可以看到,虽然pt为指向father类型的指针,但调用的虚函数所属的类是根据当前pt指向的类决定的
2.为什么C++ 中空类的大小是1个字节?
1、对于结构体和空类大小是 1个字节 这个问题,首先这是一个C++问题,在C语言下空结构体大小为0 (当然这是编译器相关的)。这里的空类和空结构体是指类或结构体中没有任何成员。
2、在C++下,空类和空结构体的大小是1(编译器相关),这是为什么呢?为什么不是0?
3、这是因为,C++标准中规定,“no object shall have the same address in memory as any other variable” ,就是任何不同的对象不能拥有相同的内存地址。 如果空类大小为0,若我们声明一个这个类的对象数组,那么数组中的每个对象都拥有了相同的地址,这显然是违背标准的。
3.查看空类大小,然后添加虚函数再查看
定义一个空类(没有显式的任何成员)
class kong
{
};
int main()
{
kong k1;
cout << sizeof(k1)<<endl;
cout << sizeof(kong)<<endl;
}
可以看到类与其实例化后的对象所占空间大小一样,都为一个字节
为其添加一个非虚函数,并使用同样的main函数:
class kong
{
void kong1() {};
};
结果也是如此
添加虚函数:
class kong
{
void kong1() {};
virtual void kongv() {};
};
添加int型(64位系统下为4字节)数据成员:
class kong
{
void kong1() {};
virtual void kongv() {};
int a;
};
由此我们可以猜想类的内存模型为虚函数表指针与数据成员:
其中vtptr占了四个字节,i为其数据成员
4.验证内存模型
先为father类添加一个数据成员a和一个虚函数show2()
class father
{
public:
virtual void show1();
virtual void show2();
int a=1;
};
void father::show2()
{
cout << "father2" << endl;
}
int main()
{
father f1;
father* pt = &f1;
pt->show1();
cout << *(unsigned int*)(&f1) << endl;
father f2;
cout << *(unsigned int*)(&f2) << endl;
cout << *((unsigned int*)(&f2)+1) << endl;
}
(&f1)为取内存模型的地址,再解引用即是取其中第一个成员,+1再解引用就是取第二个成员
f1内存模型中第一个成员,即虚函数表指针,显示出来的即为虚表的首地址
可以看到,同一个类的不同实例化的对象,其虚表的地址一样
int main()
{
father f1;
father* pt = &f1;
cout << (unsigned int*)(&f1) << endl;
father f2;
cout << (unsigned int*)(&f2) << endl;
}
可以看到,即使是同一个类的对象,他们模型存放的地址也不同,其中放的指向虚表的指针也不是同一个
由此我们得出结论,虚函数表指针与对象一一对应,虚表与类一一对应
示意图如下:
5.查看虚函数表内存模型
由于void类型函数的地址无法转换并打印,因此先将上述show1,show2换成int型的
并在父类中添加虚函数show2()
int main()
{
father f1;
father* pt = &f1;
son s1;
cout << "f1内存模型地址"<<(unsigned int*)(&f1) << endl;
cout << "s1内存模型地址" << (unsigned int*)(&s1) << endl;
cout <<"father类虚表地址"<< *(unsigned int*)(&f1) << endl;
cout << "son类虚表地址" << *(unsigned int*)(&s1) << endl;
cout << "通过father类虚函数表查看father类中show1()函数地址" << *(unsigned int*)*(unsigned int*)(&f1)<< endl;
cout << "通过father类虚函数表查看father类中show2()函数地址"<<*(unsigned int*)(*(unsigned int*)(&f1)+1) << endl;
cout << "通过son类虚函数表查看father类中show1()函数地址" << *(unsigned int*)*(unsigned int*)(&s1) << endl;
cout << "通过son类虚函数表查看father类中show2()函数地址" << *(unsigned int*)(*(unsigned int*)(&s1)+1) << endl;
}
可以通过访问虚函数表的第二个元素查看的父类和派生类对象的虚函数表访问的同一个虚函数地址相同
示意图如下:
6.虚函数的使用
再回头分析虚函数的使用
int main()
{
father f1;
father* pt = &f1;
pt->show();
son s1;
pt = &s1;
pt->show();
}
pt指向f1时,此时调用虚函数show1()将先通过f1的虚函数表指针找到虚函数表,再通过虚函数表的show1()的地址找到Father类的show1
当pt指向s1时,此时的虚函数表为son类的虚函数表,将调用son的show1();
7.虚析构