C++成员函数调用方式用法详解

前言

C++的成员函数分为静态函数、非静态函数和虚函数三种,在本系列文章中,多处提到static和non-static不影响对象占用的内存,而虚函数需要引入虚指针,所以需要调整对象的内存布局。既然已经解决了数据,函数等在内存中的布局问题,下一个需要考虑的就是如何调用,上述提到的三种函数的调用机制都不一样,其间的差异正是本篇博客需要讨论的。

非静态成员函数

C++的设计准则之一就是:非静态成员函数至少必须和一般的非成员函数有相同的效率。要达到这一点,成员函数的成员属性不会给其带来额外的负担。考虑以下两种函数调用:

int getAge(Animal *_this);//非成员函数
int Animal::getAge();//成员函数
//getNum函数定义如下:
int getAge(){
 return age;
}
前者需要传入一个类指针,属于非成员函数调用,后者直接指明Animal类的函数调用。本质上,这两个函数是一样的,因为编译器会将后者转换为前者,其转换步骤如下:

1.改写函数的原型,使得其接受一个额外的参数,这个额外的参数就是函数的this指针:

int Animal::getNum(Animal *this);//在函数内安插一个this指针
2.将每一个对非静态成员变量的存取操作改为经由this指针来存取:

{
 return this.age;
}
3.将成员函数重写成一个外部函数,函数名称经过“mangling”处理,使它在程序中称为独一无二的语汇,如上述函数可能被处理为:getNum_AnimalFv(p),这里需要保证名字不会有冲突!

这里引申一下,extern “C”操作会抑制函数名称的“mangling”效果,用于在C++中调用C函数。

所以,将一个成员函数改写成一个非成员函数的关键在于两点:一是能够提供给函数一个读写成员变量的通道,二是解决好有可能带来的名字冲突。前者通过传递一个this指针可以很好的解决,后者则通过一定的名字转换规则来确保名字的独一无二性。

虚拟成员函数

我们来回想一下如果一个类中存在虚函数,编译器会做以下三件事:

1.为该类分配一个虚函数表,它存有虚函数在执行器的地址

2. 在该类中安插一个虚指针,指向该类的虚表

3.将每一个虚函数的入口地址存放在虚函数表中相应的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()显示调用来处理。

单一继承下的虚函数调用

当一个类继承自一个基类时,其中的虚函数可能发生如下三种情况:

1.子类中的虚函数会重写父类的虚函数

2. 继承自基类的虚函数实体,也就是基类中存在,子类中没有重写

3. 一个纯虚函数,用来在虚函数表中“占座”,也可以当做执行器异常处理函数

针对如上三种情况,子类在构建自己的虚函数表时,会做如下处理:

1.当重写了父类的虚函数时,就将虚函数表中对应的slot改写成子类的虚函数入口地址

2. 当继承基类的虚函数实例时,只需要将实例的地址拷贝到子类的虚函数表中即可

3.子类可以定义自己的虚函数实例,存档在虚表的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()函数被调用。

多重继承下的虚函数调用

有了上述的了解之后,我们知道虚函数的调用无非是需要满足一下两点:

1.需要知道虚函数表的地址

2.需要知道该虚函数在虚函数表中的位置

但是,在多重继承中,这就变得有些复杂了,多重继承中存在多个虚表,如下面这样的继承关系和内存布局:

其内存布局如下:

还是以上面的Fun函数为例,考虑下面几个调用方式:

Dog *dog = new Dog();
dog->eat();//第一种调用方式,直接传入一个dog指针
dog->sleep();//第二种调用方式,传入一个animal指针
dog->jump();//第三种调用方式,传入一个canidae指针
针对前两种调用方式,其调用给方式与上一小节中基本相似,不需要改变this指针,因为第一顺位继承类的起点与子类对象的起点一致。对于第三种调用方式来说的话,就显得有些复杂了。如果继续传入一个没有经过调整的this指针的话,就难以获取Canidae的虚表地址了。这里首先来介绍一种Thunk方法。thunk的作用在于:

1.以适当的offset来调整this指针

2.跳到对应的虚函数中 按照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指针,其主要特性是:

1.它不能够直接存取其class中的非静态成员变量

2.它不能被声明为const、volatile或virtual

3.它不需要经由类对象才被调用

所以,对于静态成员函数的调用就几乎等同于非成员函数调用了。当然,为了指明他是一个类成员函数,在命名调整上必然会加上类的信息,如下:

nimal::getAge();//假设getAge是一个静态成员函数
//其经过命名调整后如下:
getAge_AnimalSFv();//SFv表示他是一个静态成员函数,static member Function,其拥有一个空白的参数列表(void)
总结

本篇博客讲解了三类成员函数(非静态、静态、虚函数)的底层调用机制,以及C++对函数命名,this指针的调整规则等。我们可以知道,C++在成员函数调用上,对于静态,非静态成员函数在函数调用效率上基本等同于非成员函数,而虚函数的调用上为了满足多态性,需要调整this指针,找到虚表地址等等操作,影响了其函数调用效率,不过这些也是值得的!

时间: 2024-08-04 05:24:27

C++成员函数调用方式用法详解的相关文章

mysql双向加密解密方式用法详解_Mysql

如果你使用的正是mysql数据库,那么你把密码或者其他敏感重要信息保存在应用程序里的机会就很大.保护这些数据免受黑客或者窥探者的获取是一个令人关注的重要问题,因为您既不能让未经授权的人员使用或者破坏应用程序,同时还要保证您的竞争优势.幸运的是,MySQL带有很多设计用来提供这 种类型安全的加密函数.本文概述了其中的一些函数,并说明了如何使用它们,以及它们能够提供的不同级别的安全. 双向加密 就让我们从最简单的加密开始:双向加密.在这里,一段数据通过一个密钥被加密,只能够由知道这个密钥的人来解密.

C++ Vector用法详解_C 语言

vector是C++标准模版库(STL,Standard Template Library)中的部分内容.之所以认为是一个容器,是因为它能够像容器一样存放各种类型的对象,简单的说:vector是一个能够存放任意类型的动态数组,能够增加和压缩数据. 使用vector容器之前必须加上<vector>头文件:#include<vector>; vector属于std命名域的内容,因此需要通过命名限定:using std::vector;也可以直接使用全局的命名空间方式:using nam

C++中CSTRINGLIST用法详解_C 语言

CStringList类成员 构造 CStringList 构造一个空的CString对象列表 首/尾访问 GetHead 返回此列表(不能是空的)中头部的元素 GetTail 返回此列表(不能是空的)中尾部的元素 操作 RemoveHead 从列表的头部删除元素 RemoveTail 从列表的尾部删除元素 AddHead 在列表的头部添加一个元素(或者是另一个列表中的所有元素),即产生一个新的头部 AddTail 在列表的尾部添加一个元素(或者是另一个列表中的所有元素),即产生一个新的尾部 R

java synchronized用法详解_java

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块.

C++中auto_ptr智能指针的用法详解_C 语言

智能指针(auto_ptr) 这个名字听起来很酷是不是?其实auto_ptr 只是C++标准库提供的一个类模板,它与传统的new/delete控制内存相比有一定优势,但也有其局限.本文总结的8个问题足以涵盖auto_ptr的大部分内容. auto_ptr是什么? auto_ptr 是C++标准库提供的类模板,auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者.当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有

c++ 中__declspec 的用法详解_C 语言

c++ 中__declspec 的用法如下,想要了解的继续往下看吧. 语法说明: __declspec ( extended-decl-modifier-seq ) 扩展修饰符: 1:align(#) 用__declspec(align(#))精确控制用户自定数据的对齐方式 ,#是对齐值. e.g __declspec(align(32)) struct Str1{ int a, b, c, d, e; }; 它与#pragma pack()是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最

C++ Iostreams用法详解(四)缓冲区

在前面说到过每一个iostream对象都有一个缓冲区,我们称之为流缓冲区,那个这个所谓的流缓冲区是怎 么存在的呢?iostreams中将该流缓冲区抽象为一个类,即streambuf类. 每个iostream的类都会包含 一个指向streambuf对象的指针,这也就意味着我们可以直接的去访问到这个指针,并向该streambuf对象发送 消息等(但是一般情况下我们并不需要这样做). 既然说我们可以得到这个指针,那isotreams类当然 会提供访问的接口了,这就是const成员函数rdbuf(),它

js replace 与replaceall实例用法详解

这篇文章介绍了js replace 与replaceall实例用法详解,有需要的朋友可以参考一下   stringObj.replace(rgExp, replaceText) 参数 stringObj 必选项.要执行该替换的 String 对象或字符串文字.该字符串不会被 replace 方法修改. rgExp 必选项.为包含正则表达式模式或可用标志的正则表达式对象.也可以是 String 对象或文字.如果 rgExp 不是正则表达式对象,它将被转换为字符串,并进行精确的查找;不要尝试将字符串

JQuery中DOM事件绑定用法详解_jquery

本文实例讲述了JQuery中DOM事件绑定用法.分享给大家供大家参考.具体分析如下: 在文档加载完成后,如果打算为元素绑定事件来完成某些操作,则可以使用bind()方法来对匹配元素进行特定事件的绑定,bind()方法的调用格式为: bind( type [, data] , fn); bind()方法有3个参数,说明如下. 第1个参数是事件类型,类型包括:blur.focus.load.resize.scroll.unload.click.dblclick.mousedown.mouseup.m