一封虚函数的介绍信

写在前边

这篇文章准备讲讲什么是虚函数,我们知道C++语言的三个特性:抽象、继承和多态。其中谈到多态不得不说的就是虚函数了,那么虚函数究竟应该怎样去认识呢?

一个简洁的例子

在一个镇上,大家为了节约资源,采取了资源公有制制度,由镇上的政府负责监管这样制度的实施。制度是这样的:每一个家族(按照姓氏来划分),在镇上的仓库里有一个家族仓库,在里边放了各种资源,大到什么奔驰宝马呀,飞机游艇什么的,小到锅碗瓢盆的都有,仓库门口有安检,比如张家仓库,那么只能是姓张的人才能进去使用,姓王的人就不能进去,当然,姓王的改名字姓张了,那他就能进去了,但是他就进不去王家的仓库了。但是呢,大家又觉得一起共享这些资源吧,又不太好,比如手机电脑这些关系到隐私的,不能共享,因此呢,每个人在仓库里又会有一个专有的保险柜,只有自己拿着钥匙才能去保险柜里取到东西。这样的制度大家觉得很合理。

再看虚函数

为什么要讲这个故事呢?因为我觉得,家族仓库其实就可以理解为一个类,每一个人是一个对象,保险柜其实就是一个虚表,而钥匙就是虚指针。暂且这么理解,有bug的也懒得填了。

类的内存布局

在这里给介绍一下VS的一个功能,在编译的时候可以看到类的内存情况,这里具体演示一下:
在VS的[项目]->[project属性]->[C/C++]->[命令行]下添加下边两条命令之一:

/d1reportSingleClassLayoutAAA     查看类AAA的内存布局(AAA改成自己想看的类名即可)
/d1reportAllClassLayout       查看所有类的内存布局
image.png

我在这里演示查看类AAA的内存情况。
接下来点击确定后重新编译,在[输出]中选择[生成],可以看到如下情况:


image.png

这里把我写的类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的内存.png

这就是类AAA的内存布局,我们可以分析出两个点:

  • 内存对齐
  • 只有成员变量,没有成员函数

首先内存对齐就是蓝色的那一块,讲道理a是4字节,b是1字节,c是2字节,总共应该是7个字节,为什么会是8字节呢?为什么要空一块出来呢?这个是另一个知识点,我们先挖一个坑之后再填。
然后是为什么没有成员函数的位置呢?联系之前我讲的故事,成员函数就像是家族仓库里的奔驰宝马一样,每个人都可以用一样的,没必要放在类的内存中去占地方,那么实际上C++的成员函数是怎样放置的呢?
我们知道,虽然每一个类的对象使用的函数的代码都是一样的,但是实际上在使用函数时可能会调用到对象本身的成员变量,那么这是怎样实现的?
这里就要提到this指针。其实在对象调用自己的成员函数时,编译器会把对象的this指针作为一个参数传到函数中去,这样函数在执行的时候就会知道访问哪个对象中的成员变量。所以有了这个机制以后,所有的成员共用一套函数代码是可行的,这样可以节省内存空间。

虚函数

首先我们看看虚函数的声明。
虚函数是函数声明时带有virtual声明的函数,比如:

virtual double area();

另外还有一种叫纯虚函数,声明如下:

virtual double area()=0;

百度百科:虚函数

百度百科把虚函数解释为上图,纯虚函数解释为将声明与实现分开。我对虚函数的解释是:
纯虚函数是在基类中定义的接口,而虚函数是基类定义了接口并定义了接口的默认实现方式。
来看一段代码:

#include<iostream>

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 <<','<<b<< std::endl;
    }
};

class CCC :public AAA
{
public:
    CCC(int aa) { a=aa; }
    ~CCC() {}
};

int main()
{
    BBB b(3, 5);
    CCC c(2);

    b.print();
    c.print();

    system("pause");
    return 0;
}

这是一段完整的代码,我们可以看到AAA作为基类,成员函数print为虚函数,并且有具体的实现,BBB类继承了AAA类,但是没有使用AAA类的print实现方式,而是重新实现了print,CCC类继承了AAA类,并且沿用了AAA类中print的实现,这样其实输出结果也能猜到了,b.print()会调用BBB类中print打印a和b的值,c.print()会调用AAA类的实现打印a的值:


image.png

这里插入一段新程序,这个是普通函数的继承:

#include<iostream>

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 <<','<<b<< std::endl;
    }
};

class CCC :public AAA
{
public:
    CCC(int aa) { a=aa; }
    ~CCC() {}
    void print()
    {
        std::cout << "class CCC:" << a << std::endl;
    }
};

int main()
{
    BBB b(3, 5);
    CCC c(2);

    b.print();
    c.print();
    ((AAA*)&b)->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中的,我先把输出结果放出来:


image.png

可以看到最后两个都是调用了AAA的print实现,也就是说,普通函数的调用时随着类类型变化而调用对应的类中的函数实现的。这个其实就和前文的故事的人员改姓一样,姓王的人改姓了张,那么只能去张家仓库中去寻找共有资源,不能去王家仓库了。
那么虚函数有什么特别的呢?我们对比一下相同的操作,接下来的代码是虚函数版本的print:


image.png

然后输出会是怎样呢?我们先看一下:

image.png

是不是很奇怪,强制转换以后,b和c依旧调用的自己的函数。怎么理解呢?就是我之前讲的保险柜,每个人有一把钥匙,虽然姓王改成了姓张,但是钥匙还在,还是能从保险柜里拿到自己的东西。
什么意思呢?我们来看看类的内存布局:
class AAA

class BBB

class CCC

从AAA看,很容易发现AAA中多了一个{vfptr}的东西,还占了4个字节,在BBB和CCC中也有,这是个什么东西呢?这就是保险柜钥匙--虚表指针
虚表是什么?
虚表是存放虚函数入口的表。
那么我们来解释一下刚才为什么((AAA)&b)->print()和((AAA)&c)->print()两条语句会调用BBB和CCC中的print,我们来看一张图:
虚表指针

b中的虚表指针在声明时就指向了类BBB的虚表,在把它强制转化成AAA以后,这个指针依旧指向的是BBB,那么在调用print()时还是会调用BBB中的实现。

总结

虚函数的基础杂谈就大概这些,当然虚函数肯定不止这一点内容,比如还有多继承时,虚表指针的指向,以及虚表中的实际内容等等,这些还有很多知识点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容