virtual 函数

示例代码如下:

#include <stdio.h>
class base {
public:
virtual void name(){printf("base\n");};
virtual ~base(){};
}; class plus: public base {
public:
virtual void name(){printf("plus\n");};
}; void fv(base b){
b.name();
} void fp(base &b){
b.name();
} int main(){
base b;
plus p;
fv(b);
fv(p);
fp(b);
fp(p);
return 0;
}

程序输出:

    base
base
base
plus

这里涉及到一个c++知识点--向上强制转换:将派生类引用或指针转换为基类引用或指针。该规则使得公有继承不需要进行显示类型转化,它是is-a 规则的一部分。

相反的过程被称为--向下强制转换,向下强制类型转换必须是显示的。因为派生类可能对基类进行拓展,新增的成员变量和函数不能应用于基类。

隐式向上强制转换使得基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++ 使用虚成员函数函数满足这种需求。

动态联编

编译器在编译时要将调用的函数对应相应的可执行代码,此过程为函数联编(binding),在C++因为函数重载的原因,需要查看调用函数名和传入参数才能确认是哪一个函数。在编译的时候可以确认使用哪一个函数的联编被称为静态联编早期联编

同时因为virtual函数的存在,编译工作变得更加复杂,如示例函数所示,具体使用的哪个类型对象不能确认。为此编译器必须生成一些代码,使得在程序运行的时候选择正确的虚函数,这被称为动态联编,又被称为晚期联编

为了验证上面所述我们可以做一组对照,首先我们用 gnu 工具 nm 来查看 sysbols,可以发现如下的部分:

$ nm virtual.exe | grep -c -E "plus|base"
49

然后我们改造一下上面的代码:

class base {
public:
void name(){printf("base\n");}; // 修改
virtual ~base(){};
}; class plus: public base {
public:
void name(){printf("plus\n");}; // 修改
};

编译后重新执行nm命令:

nm virtual_.exe | grep -c -E "plus|base"
45

经过比对后我们会发现修改后缺少了以下symbols:

000000000040509c p .pdata$_ZN4plus4nameEv
0000000000402e00 t .text$_ZN4plus4nameEv
00000000004060a0 r .xdata$_ZN4plus4nameEv
0000000000402e00 T _ZN4plus4nameEv

动态联编在效率上要低于静态联编,在C++ 中默认使用静态联编。C++ 之父strousstup 认为 C++ 指导原则之一是不要为不使用的特性付出代价(cpu、memory等)。

所以在派生类不需要去重写基类函数时,则不要将其声明为virtual函数。

virtual 函数工作原理

虚函数表示每一个使用C++的开发者耳熟能详的东西,有一个道经典的试题如下:

#include <stdio.h>

class base
{
public:
base(){};
virtual ~base() { printf("base\n"); };
}; class plus : public base
{
public:
plus(/* args */){};
virtual ~plus() { printf("plus\n"); };
}; class plus2 : public base
{
public:
plus2(/* args */){};
~plus2() { printf("plus2\n"); };
}; class plus3 : public base
{
public:
virtual void name() { printf("plus3"); };
plus3(/* args */){};
virtual ~plus3() { printf("plus3\n"); };
}; class empty
{
private:
/* data */
public:
empty(/* args */){};
~empty() { printf("empty\n"); };
}; int main()
{
base b;
printf("base: %d\n", sizeof(b)); plus p;
printf("plus: %d\n", sizeof(p)); plus2 p2;
printf("plus2: %d\n", sizeof(p2)); plus3 p3;
printf("plus3: %d\n", sizeof(p3)); empty e;
printf("empty: %d\n", sizeof(e));
}

其最终输出的结果如下:

base: 8
plus: 8
plus2: 8
plus3: 8
empty: 1
empty
plus3
base
plus2
base
plus
base
base

ps: 由于操作系统位数的影响结果可能有变动,在x64位系统中指针内存分配大小为 8 字节,x86 系统中指针内存分配大小为 4。

我们可以清楚的看到,只要存在虚函数不论是成员函数异或是析构函数,是在类中定义或继承都会有包含一个虚函数表。而这里的8字节就是分配给了虚函数表的指针。

我们可以通过gnu tool gdb 指令进行验证,在触发断点之后通过info local命令去查看:

(gdb) info locals
b = {_vptr.base = 0x555555755d20 <vtable for base+16>}
p = {<base> = {_vptr.base = 0x555555755d00 <vtable for plus+16>}, <No data fields>}
p2 = {<base> = {_vptr.base = 0x555555755ce0 <vtable for plus2+16>}, <No data fields>}
p3 = {<base> = {_vptr.base = 0x555555755cb8 <vtable for plus3+16>}, <No data fields>}
e = {<No data fields>}

我们可以看到每一个对象内都有一个指针指向vtable。

当一个基类声明一个虚函数后,在创建对象的时候会将该函数地址加入虚函数列表中,如果派生类重写了该函数,则会用新函数地址替换,如果其定义了新函数,则会将新函数的指针加入虚表中。

示例代码如下:

#include <stdio.h>

class base
{
public:
base(){};
virtual const char* feature(){return "test";};
virtual void name() {printf("base\n");}
virtual ~base() { printf("~base\n"); };
}; class plus : public base
{
public:
plus(/* args */){};
virtual void name() {printf("plus\n");}
virtual void parant() {printf("base\n");}
~plus() { printf("plus\n"); };
}; int main()
{
base b;
printf("base: %ld\n", size_t(&b)); plus p;
printf("plus: %ld\n", size_t(&p));
}

仍然用 gdb 来验证,断点后通过 info vtbl 命令查看:

(gdb) info vtbl p
vtable for 'plus' @ 0x555555755d08 (subobject @ 0x7fffffffe010):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554bf8 <plus::name()>
[2]: 0x555555554c30 <plus::~plus()>
[3]: 0x555555554c66 <plus::~plus()>
[4]: 0x555555554c14 <plus::parant()>
(gdb) info vtbl b
vtable for 'base' @ 0x555555755d40 (subobject @ 0x7fffffffe008):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554b5c <base::name()>

当调用虚函数的时候,会在虚函数表中寻找对应的函数地址,因此它每一次调用动会多做一步匹配,相比静态联编的非虚函数要更加耗时。

需要注意的是构造函数不能声明为虚函数,而如果一个类作为除非不作为基类,否则建议声明一个虚析构函数。

C++ 基础--虚构函数的更多相关文章

  1. python基础——匿名函数

    python基础——匿名函数 当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便.  在Python中,对匿名函数提供了有限支持.还是以map()函数为例,计算f(x)=x2时 ...

  2. python基础——返回函数

    python基础——返回函数 函数作为返回值 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回.  我们来实现一个可变参数的求和.通常情况下,求和的函数是这样定义的: def calc_ ...

  3. python基础——sorted()函数

    python基础——sorted()函数 排序算法 排序也是在程序中经常用到的算法.无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小.如果是数字,我们可以直接比较,但如果是字符串或者两个d ...

  4. python基础——filter函数

    python基础——filter函数 Python内建的filter()函数用于过滤序列. 和map()类似,filter()也接收一个函数和一个序列.和map()不同的是,filter()把传入的函 ...

  5. Java之--Java语言基础组成—函数

    Java语言基础组成-函数 Java语言由8个模块构成,分别为:关键字.标识符(包名.类名.接口名.常量名.变量名等).注释.常量和变量.运算符.语句.函数.数组. 本片主要介绍Java中的函数,函数 ...

  6. 数据分析与展示——Matplotlib基础绘图函数示例

    Matplotlib库入门 Matplotlib基础绘图函数示例 pyplot基础图表函数概述 函数 说明 plt.plot(x,y,fmt, ...) 绘制一个坐标图 plt.boxplot(dat ...

  7. JavaSE语法基础(3)---函数、数组

    JavaSE语法基础(3)---函数.数组 函数的概念:实现特定功能的一段代码,可反复使用. 函数的出现减少代码冗余,提高代码的复用性,可读性,可维护性,可以使每个功能模块独立起来,方便分工合作. 函 ...

  8. python基础——匿名函数及递归函数

    python基础--匿名函数及递归函数 1 匿名函数语法 匿名函数lambda x: x * x实际上就是: def f(x): return x * x 关键字lambda表示匿名函数,冒号前面的x ...

  9. Go语言基础之函数

    Go语言基础之函数 函数是组织好的.可重复使用的.用于执行指定任务的代码块.本文介绍了Go语言中函数的相关内容. 函数 Go语言中支持函数.匿名函数和闭包,并且函数在Go语言中属于“一等公民”. 函数 ...

随机推荐

  1. Docker系列-第七篇Docker构建SpringBoot应用

    1.基于Dockerfile构建SpringBoot镜像 1.1准备工作 将SpringBoot项目通过maven打成jar包 mvn clean package #使用maven打包项目 1.2使用 ...

  2. TensorFlow——Graph的基本操作

    1.创建图 在tensorflow中,一个程序默认是建立一个图的,除了系统自动建立图以外,我们还可以手动建立图,并做一些其他的操作. 下面我们使用tf.Graph函数建立图,使用tf.get_defa ...

  3. python关系(比较)运算符

    关系运算符 就是 比较运算符 a.对象的值进行比较 数字间的比较运算符连着使用: 数字与True.False的比较True 表示 1 , False 表示 0 数字与字符串的比较(不能比较) 字符串间 ...

  4. 《【面试突击】— Redis篇》--Redis Cluster及缓存使用和架构设计的常见问题

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>--Redis Cluster及缓存使用和架构设计的 ...

  5. 史上最简约的vi教程,复制和粘贴

    上一篇博客,讲了"新手"如何"入门"vi,解决了"两眼一抹黑"的情况.知道在vi下如何进行基本的操作,如部署在Linux下的项目,修改配置文 ...

  6. leetcode 最大水池

    leetcode 11题 水池最大容积 题目描述 给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) .在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 ( ...

  7. session跨域丢失问题

    配置一个 filter package com.psm.filter; import javax.servlet.*; import javax.servlet.http.HttpServletReq ...

  8. 【Java并发基础】管程简介

    前言 在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的.除了Java之外,C/C++.C#等高级语言也都是支持管程的. 那么什么 ...

  9. 使用Azure CLI创建Linux虚拟机

    Azure提供了三种方式创建虚拟机,分别是Azure CLI.Azure PowerShell和Azure门户.本文介绍使用Azure CLI来创建Linux虚拟机. 使用Azure CLI创建Lin ...

  10. [JLOI2014]松鼠的新家(树链剖分)

    [JLOI2014]松鼠的新家(luogu) Description 题目描述 松鼠的新家是一棵树,前几天刚刚装修了新家,新家有n个房间,并且有n-1根树枝连接,每个房间都可以相互到达,且俩个房间之间 ...