C/C++ 函数参数和返回值传递机制
2016-02-18 20:27
831 查看
说明 C/C++ 函数调用中,参数和返回值传递的机制,包括低级汇编指令和高级 C++ 对象拷贝构造
关键字:参数传递,返回值传递,按值传递 (passed by value),按引用传递 (passed by reference),拷贝构造
相关参考
调用约定 (Calling Convention) wiki: Calling
convention
字节对齐 (Bytes Alignment) wiki: Data
structure alignment
函数调用时返回值传递 (Return Value Transfer) wiki: Call
stack
名字修饰 (Name Decoration) 与 C++ 的函数重载 (Overload) wiki: Name
mangling
目录
函数调用栈示意图
参数按值传递
IDA 调试 POD 参数传递
C++ 对象参数传递
VC 调试 C++ 对象参数传递
返回值按值传递
返回值传递步骤
返回值传递测试结果
返回值传递效率
调用顺序 func_1->func_2,调用时栈操作顺序从 高地址 到 低地址
示意图如下:
栈示意图后面的 1、2 表示哪个函数会访问这些存储
基本过程如此,但编译器之间略有差别,如 VC 调试方式编译,用 sub esp, XXh 预留栈空间等
C++ 中参数和返回值传递都是初始化语义
prolog 和 epilog
prolog: 进入 func_2 时的准备工作,保存 func_1 的环境,如 EBP 和其它寄存器值,预留栈空间等
epilog: 离开 func_2 时的恢复工作,恢复 func_1 的环境
prolog 和 epilog 由编译器产生,但可使用 VC 的 __declspec(naked) 裸函数,手工编写 prolog 和 epilog,参考 MSDNConsiderations
for Writing Prolog/Epilog Code
以按值传递一个 POD 结构 Student 为例,说明汇编指令
编译器 Linux GCC 4
编译命令 g++ -O0 -g3 -Wall
测试程序:
源码打印?
struct Student
{
int id; // 学号
char* name; // 姓名
int age; // 年龄
int sex; // 性别
};
// 被调函数
int print_student(Student stu);
// 调用函数
int main()
{
Student stu_1 = {12345, "LiMing", 21, 0};
print_student(stu);
}
call print_student 指令及栈操作
![](http://static.oschina.net/uploads/img/201205/23110207_GTES.gif)
print_student prolog 及栈操作
![](http://static.oschina.net/uploads/img/201205/23110207_INzb.png)
IDA 中两个函数参数相关汇编符号:
var_XX: caller 访问 callee 的参数使用的符号,表示为:相对于 caller ESP+XXh 的偏移量 [ESP+XXh+var_XX]
arg_XX: callee 访问自己参数使用的符号,表示为:相对于 callee EBP 的偏移量 [EBP+arg_XX]
在一次函数调用中,var_XX 和 arg_XX 是同一存储的不同名称,在 caller 中用 var_XX 访问,在 callee 中用 arg_XX 访问
传递 C++ 类对象时的拷贝构造,以及按引用传递、按地址(指针)传递参数时的汇编指令
编译器 VC 2010
编译命令 cl /Od /MDd /Zi /EHsc (Debug)
测试程序:
源码打印?
// 复数类模板
template<class Type>
class Complex
{
// 省略无关代码
// copy ctor
Complex(const Complex& right) : m_real(right.m_real), m_image(right.m_image) {}
}
// 被调函数
void some_func(Complex<double> complex_v1, Complex<double>& complex_v2, Complex<double>* complex_v3,
double v1, double& v2, double* v3);
// 调用函数
void caller()
{
Complex<double> complex_v1(1.2, 2.3);
double v1 = 1.3;
some_func(complex_v1, complex_v1, &complex_v1, 1.1, v1, &v1);
}
下面是 caller 中调用 some_func() 的汇编指令:
按引用传递 和 按地址(指针)传递
两者的汇编指令相似:
按值传递 double 字面量
按值传递 Complex 对象
调用 Complex copy ctor 拷贝构造对象:
调用 some_func()
堆栈数据
测试程序:
源码打印?
class TestClass
{
public:
// ctor
TestClass(int d = 0) : m_data(d)
{
cerr << "TestClass ctor: " << this << endl;
}
// copy ctor
TestClass(const TestClass& right) : m_data(right.m_data)
{
cerr << "TestClass copy ctor: " << this << " <= " << &right << endl;
}
// dtor
~TestClass()
{
cerr << "TestClass dtor: " << this << endl;
}
// assign
TestClass& operator=(const TestClass& right)
{
cerr << "TestClass assign: " << this << "<=" << &right << endl;
m_data = right.m_data;
return *this;
}
template<class CharT>
std::basic_ostream<CharT>& output(std::basic_ostream<CharT>& os) const
{
os << m_data;
return os;
}
private:
int m_data;
};
// 被调函数
TestClass get_test_obj()
{
TestClass ret_obj(200);
return ret_obj;
}
// 调用函数
int main()
{
TestClass obj = get_test_obj();
return 0;
}
callee 用返回对象 ret_obj 初始化 class TestClass 的 返回值临时对象
临时对象的销毁时机
参考 "The C++ Programming Language"
临时对象在维持它的那条语句之后被销毁,除非临时对象被约束到其它名字,此时由这个名字控制临时对象的生存期,约束不产生初始化或赋值语义,没有拷贝
callee 返回时,由 ret_obj 的存储方式,决定是否销毁
如果 ret_obj 是局部对象或 callee 参数,则在返回时销毁
caller 中根据对 callee 返回值的使用,会有不同的情况,常见如下:
用返回值赋值
源码打印?
TestClass obj;
obj = get_test_obj(); // 返回值临时对象调用 obj.operator=() 进行拷贝,这句结束后销毁返回值临时对象
用返回值初始化
源码打印?
TestClass obj = get_test_obj(); // 隐式初始化对象
TestClass obj(get_test_obj()); // 显式初始化对象
TestClass& obj_ref = get_test_obj(); // 初始化引用
上面 3 者效果相同,均将返回值临时对象约束到 obj 或 obj_ref,期间只有一个对象本体,就是返回值临时对象,没有拷贝,之后由约束名字控制其生存期
即时使用返回值而不保存
源码打印?
get_test_obj().output(cout) << endl; // 返回值临时对象没有约束到其它名字
cout << "bla bla bla" << endl; // 上句结束后,这句之前,销毁返回值临时对象
对上面程序的测试结果
编译器 VC 2010
编译命令 cl /Od /MDd /Zi /EHsc (Debug)
运行结果:
编译器 MinGW GCC 4
编译命令 g++ -O0 -g3 -Wall
运行结果:
GCC 不创建额外的返回值临时对象(即使 -O0 关闭优化),而直接将 callee 的局部对象作为返回值临时对象,被调函数返回后,将其栈交给调用函数控制
初始化返回值到非 const 引用 TestClass& obj = get_test_obj() 时,编译报错,而 VC 不报错,应初始化到 const 引用 const TestClass& obj_ref = get_test_obj()
因为拷贝开销,一般 不建议返回对象,除非:
返回值是内部类型,如 整数、浮点数、枚举、指针、数组名等
返回值是小 size 类型的对象,如 Point、Rect 等 POD,或 smart pointer 等小型封装类
返回引用类型实际是 caller 直接访问 callee 中返回值对象的别名,没有拷贝开销
返回局部变量时,可使用 返回时构造 技巧,如:
源码打印?
Complex<double> func_test()
{
return Complex<double>(2.3, 1.2);
}
return 语句中的 Complex(2.3, 1.2) 即是返回值临时对象,不调用 copy ctor 创建第 2 个临时对象,返回时没有销毁局部对象的开销
关键字:参数传递,返回值传递,按值传递 (passed by value),按引用传递 (passed by reference),拷贝构造
相关参考
调用约定 (Calling Convention) wiki: Calling
convention
字节对齐 (Bytes Alignment) wiki: Data
structure alignment
函数调用时返回值传递 (Return Value Transfer) wiki: Call
stack
名字修饰 (Name Decoration) 与 C++ 的函数重载 (Overload) wiki: Name
mangling
目录
函数调用栈示意图
参数按值传递
IDA 调试 POD 参数传递
C++ 对象参数传递
VC 调试 C++ 对象参数传递
返回值按值传递
返回值传递步骤
返回值传递测试结果
返回值传递效率
函数调用栈示意图^
调用顺序 func_1->func_2,调用时栈操作顺序从 高地址 到 低地址示意图如下:
***低地址*** [ ... ] 2 [func_2 局部变量 2 ] 2 [func_2 局部变量 1 ] 2 [func_1 寄存器值: ECX, EBX, EDI, ESI 等 ] 2 保存 func_1 的运行环境 [func_2 预留栈空间 ] 2 用 sub esp, XXh 预留栈空间 [func_1 EBP ] 2 保存 func_1 EBP,然后 EBP <= ESP [func_1 中下一条 EIP ] 2 保存 func_2 调用完成后,func_1 的下一条指令地址 [func_2 返回值 ] 2 1 func_1 使用 func_2 的返回值,size 小的返回值通过寄存器传递 [func_2 第 1 个参数 ] 2 1 [func_2 第 2 个参数 ] 2 1 [... ] 2 1 IDA 中用 func_2 arg_XX 表示 [func_2 第 N 个参数 ] 2 1 C/C++ 参数入栈方式:从后到前 ***高地址***
栈示意图后面的 1、2 表示哪个函数会访问这些存储
基本过程如此,但编译器之间略有差别,如 VC 调试方式编译,用 sub esp, XXh 预留栈空间等
C++ 中参数和返回值传递都是初始化语义
prolog 和 epilog
prolog: 进入 func_2 时的准备工作,保存 func_1 的环境,如 EBP 和其它寄存器值,预留栈空间等
epilog: 离开 func_2 时的恢复工作,恢复 func_1 的环境
prolog 和 epilog 由编译器产生,但可使用 VC 的 __declspec(naked) 裸函数,手工编写 prolog 和 epilog,参考 MSDNConsiderations
for Writing Prolog/Epilog Code
参数按值传递^
以按值传递一个 POD 结构 Student 为例,说明汇编指令编译器 Linux GCC 4
编译命令 g++ -O0 -g3 -Wall
测试程序:
源码打印?
struct Student
{
int id; // 学号
char* name; // 姓名
int age; // 年龄
int sex; // 性别
};
// 被调函数
int print_student(Student stu);
// 调用函数
int main()
{
Student stu_1 = {12345, "LiMing", 21, 0};
print_student(stu);
}
IDA 调试 POD 参数传递^
call print_student 指令及栈操作![](http://static.oschina.net/uploads/img/201205/23110207_GTES.gif)
print_student prolog 及栈操作
![](http://static.oschina.net/uploads/img/201205/23110207_INzb.png)
IDA 中两个函数参数相关汇编符号:
var_XX: caller 访问 callee 的参数使用的符号,表示为:相对于 caller ESP+XXh 的偏移量 [ESP+XXh+var_XX]
arg_XX: callee 访问自己参数使用的符号,表示为:相对于 callee EBP 的偏移量 [EBP+arg_XX]
在一次函数调用中,var_XX 和 arg_XX 是同一存储的不同名称,在 caller 中用 var_XX 访问,在 callee 中用 arg_XX 访问
C++ 对象参数传递^
传递 C++ 类对象时的拷贝构造,以及按引用传递、按地址(指针)传递参数时的汇编指令编译器 VC 2010
编译命令 cl /Od /MDd /Zi /EHsc (Debug)
测试程序:
源码打印?
// 复数类模板
template<class Type>
class Complex
{
// 省略无关代码
// copy ctor
Complex(const Complex& right) : m_real(right.m_real), m_image(right.m_image) {}
}
// 被调函数
void some_func(Complex<double> complex_v1, Complex<double>& complex_v2, Complex<double>* complex_v3,
double v1, double& v2, double* v3);
// 调用函数
void caller()
{
Complex<double> complex_v1(1.2, 2.3);
double v1 = 1.3;
some_func(complex_v1, complex_v1, &complex_v1, 1.1, v1, &v1);
}
VC 调试 C++ 对象参数传递^
下面是 caller 中调用 some_func() 的汇编指令:按引用传递 和 按地址(指针)传递
两者的汇编指令相似:
lea eax, [v1] eax <= v1 的地址 push eax esp = esp - sizeof(eax), [esp] <= eax
按值传递 double 字面量
sub esp, 8 预留 8 byte (64bit) 栈空间 fld qword ptr [__real@3ff199999999999a] 浮点寄存器 ST0 <= 浮点数 1.1 (64bit: 3ff199999999999a) fstp qword ptr [esp] [esp] <= ST0,不移动 esp,因为已预留 8 byte
按值传递 Complex 对象
调用 Complex copy ctor 拷贝构造对象:
sub esp, 10h 预留 16 byte 栈空间,sizeof(Complex <double style="padding: 0px; margin: 0px;"> ) = 16 byte mov ecx, esp 准备 Complex copy ctor 的 this 指针 lea edx, [complex_v1] 两条指令准备 Complex copy ctor 的参数 right push edx call Complex <double style="padding: 0px; margin: 0px;"> ::Complex <double style="padding: 0px; margin: 0px;"> 调用 Complex copy ctor,由 callee 平衡堆栈 (thiscall) </double> </double> </double>
调用 some_func()
call some_func [esp] <= eip add esp, 28h 由 caller 平衡堆栈 (cdecl),some_func() 的参数共 40 (28h) byte
堆栈数据
0x00000000 0x00000004 0x00000008 0x0000000C . ... . 0x0012FB7C 98 fd 12 00 caller edi 0x0012FB80 78 fc 12 00 caller esi 0x0012FB84 00 60 fd 7f caller ebx . ... sub esp, 0C0h 预留 192 byte 栈空间 (some_func) . 0x0012FC48 98 fd 12 00 caller ebp 0x0012FC4C b1 25 41 00 调用 Complex copy ctor 时是 Complex copy ctor 的参数 right 调用 some_func() 时是 caller eip 以下是 some_func() 的参数 0x0012FC50 33 33 33 33 Complex copy ctor 构造的对象,this 指针 = 起始地址 0x0012FC50 0x0012FC54 33 33 f3 3f 浮点数 1.2 (64bit: 3ff3333333333333) 0x0012FC58 66 66 66 66 浮点数 2.3 (64bit: 4002666666666666) 0x0012FC5C 66 66 02 40 0x0012FC60 84 fd 12 00 Complex& 0x0012FC64 84 fd 12 00 Complex* 0x0012FC68 9a 99 99 99 double 64bit 0x0012FC6C 99 99 f1 3f 0x0012FC70 74 fd 12 00 double& 0x0012FC74 74 fd 12 00 double*
返回值按值传递^
测试程序:源码打印?
class TestClass
{
public:
// ctor
TestClass(int d = 0) : m_data(d)
{
cerr << "TestClass ctor: " << this << endl;
}
// copy ctor
TestClass(const TestClass& right) : m_data(right.m_data)
{
cerr << "TestClass copy ctor: " << this << " <= " << &right << endl;
}
// dtor
~TestClass()
{
cerr << "TestClass dtor: " << this << endl;
}
// assign
TestClass& operator=(const TestClass& right)
{
cerr << "TestClass assign: " << this << "<=" << &right << endl;
m_data = right.m_data;
return *this;
}
template<class CharT>
std::basic_ostream<CharT>& output(std::basic_ostream<CharT>& os) const
{
os << m_data;
return os;
}
private:
int m_data;
};
// 被调函数
TestClass get_test_obj()
{
TestClass ret_obj(200);
return ret_obj;
}
// 调用函数
int main()
{
TestClass obj = get_test_obj();
return 0;
}
返回值传递步骤^
callee 用返回对象 ret_obj 初始化 class TestClass 的 返回值临时对象临时对象的销毁时机
参考 "The C++ Programming Language"
临时对象在维持它的那条语句之后被销毁,除非临时对象被约束到其它名字,此时由这个名字控制临时对象的生存期,约束不产生初始化或赋值语义,没有拷贝
callee 返回时,由 ret_obj 的存储方式,决定是否销毁
如果 ret_obj 是局部对象或 callee 参数,则在返回时销毁
caller 中根据对 callee 返回值的使用,会有不同的情况,常见如下:
用返回值赋值
源码打印?
TestClass obj;
obj = get_test_obj(); // 返回值临时对象调用 obj.operator=() 进行拷贝,这句结束后销毁返回值临时对象
用返回值初始化
源码打印?
TestClass obj = get_test_obj(); // 隐式初始化对象
TestClass obj(get_test_obj()); // 显式初始化对象
TestClass& obj_ref = get_test_obj(); // 初始化引用
上面 3 者效果相同,均将返回值临时对象约束到 obj 或 obj_ref,期间只有一个对象本体,就是返回值临时对象,没有拷贝,之后由约束名字控制其生存期
即时使用返回值而不保存
源码打印?
get_test_obj().output(cout) << endl; // 返回值临时对象没有约束到其它名字
cout << "bla bla bla" << endl; // 上句结束后,这句之前,销毁返回值临时对象
返回值传递测试结果^
对上面程序的测试结果编译器 VC 2010
编译命令 cl /Od /MDd /Zi /EHsc (Debug)
运行结果:
TestClass ctor: 0012FF44 TestClass copy ctor: 0012FF64 <= 0012FF44 TestClass dtor: 0012FF44 TestClass dtor: 0012FF64
编译器 MinGW GCC 4
编译命令 g++ -O0 -g3 -Wall
运行结果:
TestClass ctor: 0x22ff3c TestClass dtor: 0x22ff3cGCC 返回值传递优化和 VC 的区别
GCC 不创建额外的返回值临时对象(即使 -O0 关闭优化),而直接将 callee 的局部对象作为返回值临时对象,被调函数返回后,将其栈交给调用函数控制
初始化返回值到非 const 引用 TestClass& obj = get_test_obj() 时,编译报错,而 VC 不报错,应初始化到 const 引用 const TestClass& obj_ref = get_test_obj()
返回值传递效率^
因为拷贝开销,一般 不建议返回对象,除非:返回值是内部类型,如 整数、浮点数、枚举、指针、数组名等
返回值是小 size 类型的对象,如 Point、Rect 等 POD,或 smart pointer 等小型封装类
返回引用类型实际是 caller 直接访问 callee 中返回值对象的别名,没有拷贝开销
返回局部变量时,可使用 返回时构造 技巧,如:
源码打印?
Complex<double> func_test()
{
return Complex<double>(2.3, 1.2);
}
return 语句中的 Complex(2.3, 1.2) 即是返回值临时对象,不调用 copy ctor 创建第 2 个临时对象,返回时没有销毁局部对象的开销
相关文章推荐
- 《Effective C++》与《More Effective C++》笔记总结
- 理解C语言——从小菜到大神的晋级之路(9)——多维数组
- C 和 C++ 的矩阵库
- Trie树的c++实现
- c++ STL
- c++ STL
- c/c++冒泡排序
- C++中函数strcpy和strcpy_s(VS pro 2015)
- C语言一些常用的“位”操作函数
- AMR解码编程(C++直接用opencore-amr-0.1.3库)
- c语言入门之项目1.7——利用if语句解决数学题
- 同时被3和5整除的数
- AMR解码编程(C++直接用opencore-amr-0.1.3库)
- C/C++面试题(1):交换两个相同大小的整型数组
- c语言:【顺序表】静态顺序表的初始化、打印、尾插、尾删
- c语言入门之项目1.6——if语句嵌套
- C++11 用户自定义字面值
- c语言实现线程池
- 5.2类与对象---对象和C语言中的指针变量的比较
- C++ 设计模式之——简单工厂模式(SimpleFactoryPattern)