写在前边
这篇文章准备讲讲什么是虚函数,我们知道C++语言的三个特性:抽象、继承和多态。其中谈到多态不得不说的就是虚函数了,那么虚函数究竟应该怎样去认识呢?
一个简洁的例子
在一个镇上,大家为了节约资源,采取了资源公有制制度,由镇上的政府负责监管这样制度的实施。制度是这样的:每一个家族(按照姓氏来划分),在镇上的仓库里有一个家族仓库,在里边放了各种资源,大到什么奔驰宝马呀,飞机游艇什么的,小到锅碗瓢盆的都有,仓库门口有安检,比如张家仓库,那么只能是姓张的人才能进去使用,姓王的人就不能进去,当然,姓王的改名字姓张了,那他就能进去了,但是他就进不去王家的仓库了。但是呢,大家又觉得一起共享这些资源吧,又不太好,比如手机电脑这些关系到隐私的,不能共享,因此呢,每个人在仓库里又会有一个专有的保险柜,只有自己拿着钥匙才能去保险柜里取到东西。这样的制度大家觉得很合理。
再看虚函数
为什么要讲这个故事呢?因为我觉得,家族仓库其实就可以理解为一个类,每一个人是一个对象,保险柜其实就是一个虚表,而钥匙就是虚指针。暂且这么理解,有bug的也懒得填了。
类的内存布局
在这里给介绍一下VS的一个功能,在编译的时候可以看到类的内存情况,这里具体演示一下:
在VS的[项目]->[project属性]->[C/C++]->[命令行]下添加下边两条命令之一:
/d1reportSingleClassLayoutAAA 查看类AAA的内存布局(AAA改成自己想看的类名即可)
/d1reportAllClassLayout 查看所有类的内存布局
我在这里演示查看类AAA的内存情况。
接下来点击确定后重新编译,在[输出]中选择[生成],可以看到如下情况:
这里把我写的类AAA代码先贴出来:
class AAA
{
private:
int a;
char b;
short c;
public:
AAA(int aa, char bb, short cc) :a(aa), b(bb), c(cc) {}
~AAA() {}
void print()
{
std::cout << a << ',' << b << ',' << c << std::endl;
}
};
对比[生成]中的类分布图,我们来具体分析一下。
首先,类成员a是一个int型变量,它被放在类内存起始地址偏移为0的地方(前边那个数字是偏移地址,相对于类的起始地址),类成员b是一个char型的变量,它被放在偏移为4的地方,,然后类成员c是一个short型变量,它被放在偏移地址为6的地方。另外,我在64位操作系统下,char型变量占1字节,short型变量占2字节,int型占4字节。整个类AAA的大小为8字节,在AAA后边的那个size(8)中可以看到。这样我们画一个图出来看看:
这就是类AAA的内存布局,我们可以分析出两个点:
- 内存对齐
- 只有成员变量,没有成员函数
首先内存对齐就是蓝色的那一块,讲道理a是4字节,b是1字节,c是2字节,总共应该是7个字节,为什么会是8字节呢?为什么要空一块出来呢?这个是另一个知识点,我们先挖一个坑之后再填。
然后是为什么没有成员函数的位置呢?联系之前我讲的故事,成员函数就像是家族仓库里的奔驰宝马一样,每个人都可以用一样的,没必要放在类的内存中去占地方,那么实际上C++的成员函数是怎样放置的呢?
我们知道,虽然每一个类的对象使用的函数的代码都是一样的,但是实际上在使用函数时可能会调用到对象本身的成员变量,那么这是怎样实现的?
这里就要提到this指针。其实在对象调用自己的成员函数时,编译器会把对象的this指针作为一个参数传到函数中去,这样函数在执行的时候就会知道访问哪个对象中的成员变量。所以有了这个机制以后,所有的成员共用一套函数代码是可行的,这样可以节省内存空间。
虚函数
首先我们看看虚函数的声明。
虚函数是函数声明时带有virtual声明的函数,比如:
virtual double area();
另外还有一种叫纯虚函数,声明如下:
virtual double area()=0;
百度百科把虚函数解释为上图,纯虚函数解释为将声明与实现分开。我对虚函数的解释是:
纯虚函数是在基类中定义的接口,而虚函数是基类定义了接口并定义了接口的默认实现方式。
来看一段代码:
#include
class AAA
{
public:
int a;
AAA(int aa=0) :a(aa){}
~AAA() {}
virtual void print()
{
std::cout <<"class AAA:"<< a << std::endl;
}
};
class BBB:public AAA
{
public:
int b;
BBB(int aa, int bb) :b(bb) { a=aa; }
~BBB() {}
void print()
{
std::cout <<"class BBB:"<< AAA::a <<','<
这是一段完整的代码,我们可以看到AAA作为基类,成员函数print为虚函数,并且有具体的实现,BBB类继承了AAA类,但是没有使用AAA类的print实现方式,而是重新实现了print,CCC类继承了AAA类,并且沿用了AAA类中print的实现,这样其实输出结果也能猜到了,b.print()会调用BBB类中print打印a和b的值,c.print()会调用AAA类的实现打印a的值:
这里插入一段新程序,这个是普通函数的继承:
#include
class AAA
{
public:
int a;
AAA(int aa=0) :a(aa){}
~AAA() {}
void print()
{
std::cout <<"class AAA:"<< a << std::endl;
}
};
class BBB:public AAA
{
public:
int b;
BBB(int aa, int bb) :b(bb) { a=aa; }
~BBB() {}
void print()
{
std::cout <<"class BBB:"<< AAA::a <<','<print();
((AAA*)&c)->print();
system("pause");
return 0;
}
这里把print函数作为一个普通的函数,在BBB和CCC继承以后分别也重新重载了print函数,那么在最后b.print()和c.print()时都会调用BBB和CCC各自的print函数,接下来两条强制把b转换成AAA类型并调用时,会调用BBB中的print还是会调用AAA中的print呢?答案时AAA中的,我先把输出结果放出来:
可以看到最后两个都是调用了AAA的print实现,也就是说,普通函数的调用时随着类类型变化而调用对应的类中的函数实现的。这个其实就和前文的故事的人员改姓一样,姓王的人改姓了张,那么只能去张家仓库中去寻找共有资源,不能去王家仓库了。
那么虚函数有什么特别的呢?我们对比一下相同的操作,接下来的代码是虚函数版本的print:
然后输出会是怎样呢?我们先看一下:
是不是很奇怪,强制转换以后,b和c依旧调用的自己的函数。怎么理解呢?就是我之前讲的保险柜,每个人有一把钥匙,虽然姓王改成了姓张,但是钥匙还在,还是能从保险柜里拿到自己的东西。
什么意思呢?我们来看看类的内存布局:
从AAA看,很容易发现AAA中多了一个{vfptr}的东西,还占了4个字节,在BBB和CCC中也有,这是个什么东西呢?这就是保险柜钥匙-- 虚表指针。
虚表是什么?
虚表是存放虚函数入口的表。
那么我们来解释一下刚才为什么((AAA )&b)->print()和((AAA)&c)->print()两条语句会调用BBB和CCC中的print,我们来看一张图:
b中的虚表指针在声明时就指向了类BBB的虚表,在把它强制转化成AAA以后,这个指针依旧指向的是BBB,那么在调用print()时还是会调用BBB中的实现。
总结
虚函数的基础杂谈就大概这些,当然虚函数肯定不止这一点内容,比如还有多继承时,虚表指针的指向,以及虚表中的实际内容等等,这些还有很多知识点。