如果对ESP,EBP,栈帧等概念比较陌生,推荐先去了解一下函数调用的过程。
推荐使用OD+VS自己实现一遍加深印象。(OD用来查看内存,VS用来看汇编代码和源代码的对应情况)
mov与lea的区别
指令:mov 操作对象:变量 有无[]没有区别,都是取值
指令:mov 操作对象:寄存器 有[]表示取地址,没有[]表示取值
指令:lea 操作对象:变量 有无[]没有区别,都是取地址
指令:lea 操作对象:寄存器 有[]表示取值,没有[]表示取地址
代码中把对于ebp的偏移操作当作变量
内置类型
代码
#include<cstdio>
int addv(int a,int b)
{
int x=a,y=b;
return x+y;
}
int addr(int&a,int&b)
{
int x=a,y=b;
return x+y;
}
int main()
{
int a=1,b=2;
printf("%d\n",addv(a,b));
return 0;
}
传值方式
调用addv函数之前的栈情况
可见连续的两个mov,push把addv的参数a,b的值压入了栈中,此时调用函数的准备已经完成。
执行call,call分为两个过程,把当前EIP(指令指针寄存器)所指指令(即call指令)下一条语句的地址push到栈中,jmp到调用的函数入口。此时栈内情况。
addv函数内
可以直接使用mov取到参数的值,然后赋值给栈内局部变量。
传引用方式
调用addr前栈情况
传递的是参数的地址
addr函数内
由于参数是通过地址的形式传递给函数的,所以在函数内使用它们时需要额外的取地址操作。
自定义类型
代码
#include<cstdio>
class A
{
public:
int x,y,z;
A():x(1),y(2),z(3)
{ }
A(A&b):x(b.x),y(b.y),z(b.z)
{ }
};
void addv(A a,A b)
{
A x=a,y=b;
}
void addr(A&a,A&b)
{
A x=a,y=b;
}
int main()
{
A a,b;
addv(a,b);
return 0;
}
定义变量
把变量首地址保存在ecx中,调用构造函数,函数中会按着顺序依次赋值,结束后内存状态
传值方式
我们要在栈顶构造参数,方法是调用拷贝构造函数,注意这里和构造函数的调用方法是不一样的,首先把栈顶向上扩展sizeof(A)的空间,我们将在这块空间中构造要传递的参数。把栈顶放入ecx,这里的ecx和构造函数中的ecx起同样的作用,然后把拷贝的目标对象的地址作为参数push到栈内,调用拷贝构造函数。
当我们构造完参数之后,调用addv之前,栈的情况
两个完整的对象已经构造完成,作为传给addv的参数。
在addv函数内部
可见我们使用的是我们之前压入栈中的,完整的变量。这里的赋值操作和前边的步骤类似,只是使用的地址不同。
传引用方式
并没有构造对象,只是把当前栈内对象的地址当做参数传给了函数。
addr函数内部
反正调用拷贝构造函数的时候不论是拷贝的目标,还是我们所要构造的对象都是通过指针被我们操作的,我们只需要把上一步传来的地址再传给拷贝构造函数就可以了。
总结
对于内置类型,传引用并没有什么性能上的提升,反而在函数内使用参数时需要额外的取值操作,反而降低了速度。
对于自定义类型,传引用可以有效地避免中间对象的构造和析构,极大的提升了效率。