深度探索C++对象模型(17)——函数语义学(1)——函数调用方式

1.普通函数调用方式

代码:

#include <iostream>
#include <stdio.h>

using namespace std;
class MYACLS
{
public:
	int m_i;
	void myfunc(int abc)
	{
		m_i += abc;
	}
};


void gmyfunc(MYACLS *ptmp, int abc)
{
	ptmp->m_i += abc;
}
int main()
{
	MYACLS myacls;
	myacls.myfunc(18);//调用成员函数
	gmyfunc(&myacls, 18); //调用全局函数
        printf("MYACLS::myfunc的地址=%p\n", &MYACLS::myfunc);
}

(1)c++语言设计的时候有一个要求:要求对这种普通成员函数的调用不应该比全局函数效率差

基于这种设计要求,编译器内部实际上是将对成员函数myfunc()的调用转换成了对全局函数的调用;

上述代码中类 MYACLS的成员函数myfunc在编译器视角下如下所示:

//编译器视角
void myfuncEi(MYACLS *const this, int abc) //编译器会额外安插一个this指针,一般会扔到参数的开头
{
	this->m_i + abc;
}

a)编译器额外增加了一个this指针形参,指向的其实就是生成的对象;
b)常规成员变量的存取,都通过this形参来进行,比如上述this->m_i + abc;

(2)成员函数有独立的内存地址,是跟着类走的,并且成员函数的地址 是在编译的时候就确定好的;

比如上述代码打印成员函数myfunc的地址为:

然后对myacls.myfunc(18);这一行加断点,调试进行反汇编:

可以发现这里的调用函数的地址在编译后是确定的了

注:在VS中,如果代码没有被修改,多次执行,不会被重新编译,即这些在编译时产生的地址在这多次执行中不会改变,但只要代码被修改过,那么会重新编译,产生新的地址

(3)打印编译后的普通成员函数的调用地址

VS下:

不加&:报错

加&:打印出的是正确的函数地址

Linux下:

同VS

nm命令下查看

2.虚成员函数调用方式

代码:

#include <iostream>
#include <stdio.h>
using namespace std;

class MYACLS
{
public:
	int m_i;
	void myfunc(int abc)
	{
		m_i += abc;
	
	}
	virtual void myvirfunc()
	{
		printf("myvirfunc()被调用,this = %p\n", this);
		//myvirfunc2(); 居然走虚函数表指针调用
		MYACLS::myvirfunc2(); //直接调用虚函数,效率更高。这种写法压制了虚拟机制,不再通过查询虚函数表来调用
						      //这种用类名::虚函数名()明确调用虚函数的方式等价于直接调用一个普通函数;
	}
	virtual void myvirfunc2()
	{
		printf("myvirfunc2()被调用,this = %p\n", this);
	}
};


void gmyfunc(MYACLS *ptmp, int abc)
{
	ptmp->m_i += abc;
}
int main()
{
	MYACLS myacls;
	myacls.myvirfunc(); //用对象调用虚函数,就像调用普通成员函数一样,不需要通过虚函数表
        myacls.myvirfunc2();

	MYACLS *pmyacls = new MYACLS();
	pmyacls->myvirfunc(); //要通过虚函数表指针查找虚函数表,通过虚函数表在好到虚函数的入口地址,完成对虚函数的调用
    
    //VS下打印vcall的地址
	printf("MYACLS::myvirfunc()地址 = %p\n", &MYACLS::myvirfunc);

	printf("MYACLS::myvirfunc2()地址 = %p\n", &MYACLS::myvirfunc2);

	//Linux下打印虚函数地址的写法
	//MYACLS  myobj;
	//void (MYACLS::*vfp)() = &MYACLS::myvirfunc2;
	//printf("MYACLS::myvirfunc2虚函数的地址为%p", (void*)(myobj.*vfp));
}

(1)对象调用和对象指针或引用调用虚函数

<1>   对于像如下用对象调用虚函数,就像调用普通成员函数一样,不需要通过虚函数表

反汇编:

<2>   对于像如下用对象指针(或对象引用)调用虚函数,要通过虚函数表指针查找虚函数表,通过虚函数表在好到虚函数的入口地址,完成对虚函数的调用

反汇编:

上述调用在编译器视角下:

//编译器视角
(*pmyacls->vptr[0])(pmyacls);

//a)vptr,编译器给生成的虚函数表指针,指向虚函数表
//b)[0] 虚函数表中第一项。代表myvirfunc()地址
//c)传递一个参数进去,就是this,也是编译器给加的
//d)*就得到了虚函数的地址;

这里也有种看成全局函数的感觉

(2)在类内一个虚函数中调用类内的另一个虚函数

<1>   在类的一个虚函数中使用函数名直接调用类的另外一个虚函数,走的是使用虚函数表指针调用虚函数

如上述代码中在myvirfunc()中直接使用下面语句调用,反汇编:

<2>   在类的一个虚函数中使用 类名::函数 调用类的另外一个虚函数,等价于调用普通函数

如上述代码中在myvirfunc()中采用如下语句调用,反汇编:

(3)打印虚函数地址

VS下:

不加&:报错

加&:结果不正确

反汇编

 

使用像普通函数那样在VS下打印虚函数的地址不正确?

实际这里打印的是vcall地址,通过vcall中转间接调用虚函数,所以打印的不是虚函数的地址

这是VS编译器下的设计,其他不适用

vcall是一堆代码,是编译器生成的代码,叫vcall thunk,有两个作用:

1)多重继承的时候能够调整this指针的偏移

2)调整好之后就可以跳转到真正的虚函数中去

关于vcall thunk的分析见后续博客

Linux下:

代码:

运行结果:

使用nm命令查看内存布局:

3.静态成员函数调用方式

代码:

#include <iostream>
#include <stdio.h>
using namespace std;

class MYACLS
{
public:
	int m_i;
	//static int m_si;

	void myfunc(int a)
	{
		//m_i += abc; //这里需要用到this指针,而this指针为空,则会报告异常
		printf("myfunc()被调用\n");
	}

	static void mystfunc() //不需要this参数
	{
		printf("mystfunc()被调用\n");
		//m_si = 1;
	}
};

int main()
{
	MYACLS myacls;
	MYACLS *pmyacls = new MYACLS();

	myacls.mystfunc();
	pmyacls->mystfunc();
	MYACLS::mystfunc();

	((MYACLS *)0)->mystfunc();  //能够正常调用静态成员函数
	((MYACLS *)0)->myfunc(12);  //有些成员函数希望支持独立于类对象之外的存取操作;
	
	printf("MYACLS::mystfunc函数的地址为%p\n", MYACLS::mystfunc);

}

运行结果:

反汇编:

(1)可以发现,不管通过对象,对象指针,还是类调用,或是通过((MYACLS *)0)->mystfunc();这种特殊的写法的调用,在编译器角度都是一样的结果

说明:

((MYACLS *)0)->mystfunc();

这种写法本质上是向mystfunc中传递空的this指针,这种写法为了支持有些成员函数独立于类对象之外的存取操作;

((MYACLS *)0)->myfunc(12);

如上述的普通成员函数myfunc()中未使用需要通过this指针调用的成员变量,所以传入空的this指针就不会报异常

而如果有这句代码,就会报异常,因为成员变量m_i需要this指针来调用,这也是普通成员函数传递this指针的目的,而这种写法是传递了空的this指针

m_i += abc; //这里需要用到this指针,而this指针为空,则会报告异常

(2)静态成员函数特性

a)静态成员函数在编译器角度不向参数中传递this指针

b)无法直接存取类中普通的非静态成员变量,因为非静态成员变量需要this指针来调用,而静态函数不传递this指针;

c)静态成员函数不能在最后使用const,也不能设置为virtual 

d)可以用类对象调用,但不非一定要用类对象调用。

e)静态成员函数等同于非成员函数,有的需要提供回调函数的这种场合,可以将静态成员函数作为回调函数;

(3)打印静态成员函数地址

VS下:

不加&:正常打印静态成员函数地址

  

加&:也能正常打印静态成员函数地址
 
    

Linux下:

加&和不加&:都是这个结果,重新编译后地址都不曾变,能正常打印静态成员函数地址

nm命令下查看:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值