C++对象模型的那些事儿之六:成员函数调用方式
前言
C++的成员函数分为静态函数、非静态函数和虚函数三种,在本系列文章中,多处提到static和non-static不影响对象占用的内存,而虚函数需要引入虚指针,所以需要调整对象的内存布局。既然已经解决了数据,函数等在内存中的布局问题,下一个需要考虑的就是如何调用,上述提到的三种函数的调用机制都不一样,其间的差异正是本篇博客需要讨论的。
非静态成员函数
C++的设计准则之一就是:非静态成员函数至少必须和一般的非成员函数有相同的效率。要达到这一点,成员函数的成员属性不会给其带来额外的负担。考虑以下两种函数调用:
int getAge(Animal *_this);//非成员函数
int Animal::getAge();//成员函数
//getNum函数定义如下:
int getAge(){
return age;
}
前者需要传入一个类指针,属于非成员函数调用,后者直接指明Animal类的函数调用。本质上,这两个函数是一样的,因为编译器会将后者转换为前者,其转换步骤如下:
- 改写函数的原型,使得其接受一个额外的参数,这个额外的参数就是函数的this指针:
int Animal::getNum(Animal *this);//在函数内安插一个this指针
- 将每一个对非静态成员变量的存取操作改为经由this指针来存取:
{
return this.age;
}
- 将成员函数重写成一个外部函数,函数名称经过“mangling”处理,使它在程序中称为独一无二的语汇,如上述函数可能被处理为:getNum_AnimalFv(p),这里需要保证名字不会有冲突!
这里引申一下,extern “C”操作会抑制函数名称的“mangling”效果,用于在C++中调用C函数。
所以,将一个成员函数改写成一个非成员函数的关键在于两点:一是能够提供给函数一个读写成员变量的通道,二是解决好有可能带来的名字冲突。前者通过传递一个this指针可以很好的解决,后者则通过一定的名字转换规则来确保名字的独一无二性。
虚拟成员函数
我们来回想一下如果一个类中存在虚函数,编译器会做以下三件事:
- 为该类分配一个虚函数表,它存有虚函数在执行器的地址
- 在该类中安插一个虚指针,指向该类的虚表
- 将每一个虚函数的入口地址存放在虚函数表中相应的slot
所以,要想正确调用虚函数,只需找到该虚函数在虚函数表的相应位置即可,于是,考虑到以下示例。
class Animal{
public:
char name[10];//动物名字
int weight;//体重
virtual void eat(){}
};
Animal *animal;
animal->eat();
当调用虚函数eat的时候,编译器会自动转换成以下代码:
//vptr为指向虚函数表的指针,eat存放在虚函数的第一位,
//由于是成员函数,所以函数还必须传入一个this指针参数
(* animal->vptr[0])(animal);
只有在指针和引用才能展现出多态的形式,如果我们显示调用或者直接用类对象来调用的话会是什么样呢?
//显示调用eat函数
Animal::eat();
//直接用对象来调用
Animal animal;
animal.eat();
在上述两种调用中,前者会抑制掉虚拟机制,直接将eat()作为非静态成员函数一样调用。对于后者,假设编译器将其转换成如下形式:
(* animal.vptr[0])(&animal);
这样虽然在语意上正确,但是完全没有必要这样做,所以编译器会直接当成Animal::eat()显示调用来处理。
单一继承下的虚函数调用
当一个类继承自一个基类时,其中的虚函数可能发生如下三种情况:
- 子类中的虚函数会重写父类的虚函数,
- 继承自基类的虚函数实体,也就是基类中存在,子类中没有重写
- 一个纯虚函数,用来在虚函数表中“占座”,也可以当做执行器异常处理函数
针对如上三种情况,子类在构建自己的虚函数表时,会做如下处理:
- 当重写了父类的虚函数时,就将虚函数表中对应的slot改写成子类的虚函数入口地址
- 当继承基类的虚函数实例时,只需要将实例的地址拷贝到子类的虚函数表中即可
- 子类可以定义自己的虚函数实例,存档在虚表的slot中,虚表的尺寸会增大
这里还是引用之前博文中讲过的一个实例来说明一下,考虑到如下继承关系:
其内存布局如下:
可见在子类中重新改写了虚函数表,那么,针对这类继承,虚函数时怎么调用的呢?我们可以观察到父类的虚函数表中函数的相对位置在子类中是没有发生变化的,,针对于如下调用:
void Fun(Dog *dog){
dog->eat();
}
Dog* dog = new Dog();
Animal* animal = new Dog();
Fun(dog);//第一种调用方式,直接传入一个dog指针
Fun(animal);//第二种调用方式,传入一个animal指针
如果传入的是一个Dog类的对象指针,那么直接利用上一小节的方法即可,如果传入的是一个Animal类的对象指针,我们可以看到,还是一如既往的可以采用上一小节中的方法,因为eat()在虚函数表中的位置并没有发生变化,唯一在执行期才能知道的是:哪个的eat()函数被调用。
多重继承下的虚函数调用
有了上述的了解之后,我们知道虚函数的调用无非是需要满足一下两点:
- 需要知道虚函数表的地址
- 需要知道该虚函数在虚函数表中的位置
但是,在多重继承中,这就变得有些复杂了,多重继承中存在多个虚表,如下面这样的继承关系和内存布局:
其内存布局如下:
还是以上面的Fun函数为例,考虑下面几个调用方式:
Dog *dog = new Dog();
dog->eat();//第一种调用方式,直接传入一个dog指针
dog->sleep();//第二种调用方式,传入一个animal指针
dog->jump();//第三种调用方式,传入一个canidae指针
针对前两种调用方式,其调用给方式与上一小节中基本相似,不需要改变this指针,因为第一顺位继承类的起点与子类对象的起点一致。对于第三种调用方式来说的话,就显得有些复杂了。如果继续传入一个没有经过调整的this指针的话,就难以获取Canidae的虚表地址了。这里首先来介绍一种Thunk方法。thunk的作用在于:
- 以适当的offset来调整this指针
- 跳到对应的虚函数中
按照thunk的思想,再调用jump()函数时,其this指针需要做如下调整:
thunk:
this+=sizeof(Animal);
Dog::eat(this);
好,我们的问题就变成多重继承关系中,除继承顺序的第一位外,其他位的类实现虚函数调用都需要做一些调整。这种调整发生在以下两种情况:
//一、将一个基类指针指向一个子类,当然是继承顺序第一位以后的基类
Canidae *canidae = new Dog();
//二、使用子类指针来调用基类的函数,当然是继承顺序第一位以后的基类函数
Dog dog = new Dog();
dog->jump();
前一种情况中,需要将canidae指针向后调整sizeof(Animal)位,指向子类中对应的基类部分。
第二种情况,需要调整dog指针向后sizeof(Animal)位,指向dog中Canidae基类部分。
这样一来,对于多重继承下的虚函数调用就比较容易理解了,你理解了吗?
虚拟继承下的虚函数调用
针对于虚继承来说,其虚基类的地址在内存布局中存放的位置对于不同的编译器来说都不一样,书中直接说像进了迷宫一样。好吧,我是怀着向探究本源的目的来的,被作者的这一句话着实给吓到了。
在虚拟继承下的虚函数调用中,其复杂点依旧在于如何调整this指针,虚拟继承在多重继承上又多了一个虚基类指针,这使得情况就变得复杂多变了。
作者最后给了一个定义:不要在虚基类中定义非静态成员成员变量,想来也是怕这些会影响虚基类指针在内存中的布局位置,从而增加了决定适当的offset的复杂度。
静态成员函数
静态成员函数相比于其他成员函数来说,最大的不同就是它没有this指针,其主要特性是:
- 它不能够直接存取其class中的非静态成员变量
- 它不能被声明为const、volatile或virtual
- 它不需要经由类对象才被调用
所以,对于静态成员函数的调用就几乎等同于非成员函数调用了。当然,为了指明他是一个类成员函数,在命名调整上必然会加上类的信息,如下:
Animal::getAge();//假设getAge是一个静态成员函数
//其经过命名调整后如下:
getAge_AnimalSFv();//SFv表示他是一个静态成员函数,static member Function,其拥有一个空白的参数列表(void)
总结
本篇博客讲解了三类成员函数(非静态、静态、虚函数)的底层调用机制,以及C++对函数命名,this指针的调整规则等。我们可以知道,C++在成员函数调用上,对于静态,非静态成员函数在函数调用效率上基本等同于非成员函数,而虚函数的调用上为了满足多态性,需要调整this指针,找到虚表地址等等操作,影响了其函数调用效率,不过这些也是值得的!
About Me
由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。
最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me。
另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode
欢迎持续关注!Thx!
C++对象模型的那些事儿之六:成员函数调用方式的更多相关文章
- 【深度探索c++对象模型】Function语义学之成员函数调用方式
非静态成员函数 c++的设计准则之一就是:非静态成员函数至少和一般的非成员函数有相同的效率.编译器内部已将member函数实体转换为对等的nonmember函数实体. 转化步骤: 1.改写函数原型以安 ...
- C++对象模型的那些事儿之五:NRV优化和初始化列表
前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...
- C++对象模型的那些事儿之三:默认构造函数
前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...
- C++对象模型的那些事儿之四:拷贝构造函数
前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...
- C++对象模型的那些事儿之二:对象模型(下)
前言 上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承.多重继承和虚继承后内存布局上又有哪些变化呢?多态真正 ...
- C++对象模型的那些事儿之一:对象模型(上)
前言 很早以前就听人推荐了<深入理解C++对象模型>这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久.近期由于找工作对C++的知识做了一个全面系统的学习, ...
- (转)函数调用方式与extern "C"
原文:http://patmusing.blog.163.com/blog/static/13583496020103233446784/ (VC编译器下) 1. CALLBACK,WINAPI和AF ...
- C++运算符重载(成员函数方式)
一.运算符重载 C++中预定义的运算符的操作对象只能是基本数据类型,实际上,对于很多用户自定义类型,也需要有类似的运算操作.如果将C++中这些现存的运算符直接作用于用户自定义的类型数据上,会得到什么样 ...
- 函数调用方式--__thiscall调用方式和__cdecl,__stdcall有什么区别
函数调用方式--__thiscall调用方式和__cdecl,__stdcall有什么区别 首先,__thiscall是关于类的一种调用方式,它与其他调用方式的最大区别是: __thiscall ...
随机推荐
- hdu 2243 考研路茫茫——单词情结(AC自动+矩阵)
考研路茫茫——单词情结 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total ...
- hdu 5130(2014广州 圆与多边形相交模板)
题意:一个很多个点p构成的多边形,pb <= pa * k时p所占区域与多边形相交面积 设p(x,y), (x - xb)^2+(y - yb)^2 / (x - xa)^2+(y ...
- typedef的基本用法
1. 四个用途 用途一: 定义一种类型的别名,而不只是简单的宏替换.可以用作同时声明指针型的多个对象.比如: char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针 ...
- 《java技术》第二次作业
(一)学习总结 1.什么是构造方法?什么是构造方法的重载? 1)没有返回值,名字与类名相同,当新对象被创建的时候,构造函数会被调用,要想构造函数,必须声明对象并对其初始化.每一个类都有构造函数,如果没 ...
- Windows 2008 R2_NLB网络负载均衡(图文详解)(转)
目录 前言 软件环境 DNS域名服务器 DNS服务器原理 DNS域名空间 DNS区域 DNS服务器的类别 DNS查询模式 缓存文件 配置DNS服务器 DNS服务的应用 新建子域 在DNS正向解析中新建 ...
- combobox数据绑定
jquery easyui datagrid 可编辑行 combobox数据绑定问题 将带有参数的url地址赋值给变量,然后将变量赋值给url <script type="text/j ...
- MS SQL Server 2008 R2 常规操作
列出当前数据库使用的所有文件 DBCC showfilestats 转移当前文件组中某个文件的数据到同文件组中的其他文件中 DBCC SHRINKFILE ('file', EMPTYFILE); 移 ...
- C语言关闭日志文件时忘了将日志文件全局变量指针置为NULL
C语言写了一个write_log函数以写日志,写了一个close_log_file函数以关闭日志,声明了一个日志文件全局变量文件指针plogFile. write_log中首先判断plogFile是否 ...
- PHP Switch 语句
PHP Switch 语句 switch 语句用于根据多个不同条件执行不同动作. PHP Switch 语句 如果您希望有选择地执行若干代码块之一,请使用 switch 语句. 语法 switch ( ...
- Java程序员的现代RPC指南
Java程序员的现代RPC指南 1.前言 1.1 RPC框架简介 最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦.于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大 ...