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命令下查看: