今日发掘一个愚蠢、有趣又头疼的bug,因函数传参引起。我设定了一个函数专门给对象赋值,犯了一个极大的错误,我将变量直接传给函数,这个函数执行结束后,对象内部变量直接全部初始化了。后来请师父来看,他发现了问题,这直接刷新我都函数传参的认知。所以赶紧补上这篇函数传参的知识,以C++源代码和汇编为例,简单探索C/C++传参。
目录
一、问题
1.1 问题描述
1.2 解决方法
二、函数传参原理及方式
2.1 原理
2.2 方式
三、总结
直接上代码
#include
#includeint global;
class Inner
{
public:int a;int c;
public:Inner() { a = 0; c = 0; }~Inner() { global = 10; }
};class Outter
{
public:Inner * m_inner;
public://Outter(Outter& outter) = delete;Outter() { m_inner = new Inner(); }~Outter() { if (m_inner) { delete m_inner; m_inner = NULL;} }
};
template
void test(T out)
{}
int main()
{Outter out;test(out);return 0;
}
声明一个Outter变量,将变量以普通变量的身份传入test函数,等到函数执行后,out里原有的值全部没有了。这源自于函数执行后析构了变量。具体如下:
main 中,首先声明了一个 Outter 类型的变量 out,然后将它作为参数传递给了函数 test。函数 test 具有模板类型,因此可以接受任何类型的参数。当函数调用结束时,参数将被析构。因此,当 main 函数结束时,变量 out 将被析构,指针变量也将被初始化。
为了看到这个析构过程,我使用IDA反汇编工具将exe转成汇编去看:
; Attributes: bp-based frame fpd=0F0h; void test(class Outter)
??$test@VOutter@@@@YAXVOutter@@@Z proc nearvar_28= qword ptr -28h
arg_0= qword ptr 10h
arg_8= qword ptr 18h; __unwind { // j___CxxFrameHandler3_0
mov [rsp-8+arg_0], rcx
push rbp
push rdi
sub rsp, 108h
lea rbp, [rsp+20h]
mov rdi, rsp
mov ecx, 42h ; 'B'
mov eax, 0CCCCCCCCh
rep stosd
mov rcx, [rsp+110h+arg_8]
mov [rbp+0F0h+var_28], 0FFFFFFFFFFFFFFFEh
lea rcx, unk_140023028
call j___CheckForDebuggerJustMyCode
nop
lea rcx, [rbp+0F0h+arg_0] ; this
call j_??1Outter@@QEAA@XZ ; Outter::~Outter(void)
lea rsp, [rbp+0E8h]
pop rdi
pop rbp
retn
; } // starts at 1400118B0
??$test@VOutter@@@@YAXVOutter@@@Z endp
这是test函数的汇编解释部分:
1. 初始化栈帧:通过push指令将rbp和rdi寄存器的值压入栈中,并使用sub挀令分配内存。
2. 调用j___CheckForDebuggerJustMyCode函数来检查调试器是否正在运行。
3. 调用Outter类的构造函数和__autoclassinit2函数来初始化Outter类的实例。
4. 调用test函数并传入Outter类实例。
5. 调用Outter类的析构函数销毁实例。
6. 检查栈中的变量并进行安全检查。
7. 恢复栈帧并返回。
Outter的析构确实被调用了: j_??1Outter@@QEAA@XZ ; Outter::~Outter(void)
可以总结以下了:传入给函数带指针对象的类对象,不传入它的引用或者指针,那么函数执行完析构参数时,会将其指针对象同样析构,这带来的影响是灾难的,原本初始化好的对象啪就这么没了。
1. 可以禁止类的copy构造,函数接收到参数时会对执行拷贝构造,我们将其禁止,组织编译成功,coder就会修改,不会出现这样的问题。在类里加上这句禁止拷贝构造:
Outter(Outter& outter) = delete;
2. 很简单,传入参数的引用即可:
template
void test(T& out)
{}
3. 声明对象时直接创建对象的指针,避免对象在栈上。
Outter* out = new Outter();
C++函数接受参数主要有两种情况:传值和传引用。
传值:函数会copy一份变量在函数作用域使用,不改变原参数。
3. 调用Outter类的构造函数和__autoclassinit2函数来初始化Outter类的实例。
4. 调用test函数并传入Outter类实例。
5. 调用Outter类的析构函数销毁实例。
在传入参数前,确实对Outter进行了重构造,也印证了进行了copy。
传引用:传入参数的地址,函数可以对其修改,引用作为函数参数时,引用本质上是对原变量的别名,并且在函数内部对引用进行的操作实际上是对原变量进行的操作。因此,引用参数在内存中与原变量存储在同一个位置。当然前面加const也可以禁止修改。一、中代码传引用时test函数部分汇编如下:
; Attributes: bp-based frame fpd=0D0h; void test(class Outter &)
??$test@VOutter@@@@YAXAEAVOutter@@@Z proc neararg_0= qword ptr 10h
arg_8= qword ptr 18hmov [rsp-8+arg_0], rcx
push rbp
push rdi
sub rsp, 0E8h
lea rbp, [rsp+20h]
mov rdi, rsp
mov ecx, 3Ah ; ':'
mov eax, 0CCCCCCCCh
rep stosd
mov rcx, [rsp+0F0h+arg_8]
lea rcx, unk_140023028
call j___CheckForDebuggerJustMyCode
lea rsp, [rbp+0C8h]
pop rdi
pop rbp
retn
??$test@VOutter@@@@YAXAEAVOutter@@@Z endp
这行代码读取了引用类型的参数(arg_8),并将其存储在 rcx 寄存器中。
mov rcx, [rsp+0F0h+arg_8]
寄存器存储的是CPU中处理数据的临时存储单元,在函数内部对寄存器中的参数进行修改时,函数结束后会将寄存器中的值返回到调用函数中,并在调用函数中通过寄存器进行读取,以此实现对参数的修改。
这个时原理层面的函数传参。
当函数以指针的形式接收参数时,传递的是该变量的地址。函数内部可以通过访问该地址上的值来更改实际变量的值,并且这个修改对于调用该函数的代码是可见的,因为修改的是实际变量。
当向函数传递数组时,数组的首地址实际上被当作指针传递。因此,函数内部可以通过指针访问数组中的元素。但是,数组的大小需要通过另一种方式传递给函数,以便函数可以正确处理数组。
这里不做过多赘述了。
一个传参引发的血案,这边给大家建议,简单的参数例如int、string等可以直接传。复杂的参数,比如类对象,建议传指针或引用。祝大家周末愉快!