【QA】C和C++有哪些常用的调用约定
#cpp
1. __stdcall
- 参数传递顺序:参数从右到左依次压入栈中,这一点和
__cdecl
一样。例如对于函数int func(int a, int b, int c)
,调用时参数入栈顺序为c
、b
、a
。 - 栈清理方式:由被调用函数负责清理栈。在函数返回之前,被调用函数会将之前压入栈的参数从栈中移除。这意味着被调用函数需要知道参数的数量和大小,所以它不适合可变参数函数。
- 名称修饰:在 Visual C++ 中,函数名会被修饰,通常在函数名前加下划线,后面跟着
@
符号以及参数的总字节数。例如int func(int a, int b)
(假设int
为 4 字节),修饰后的名称可能是_func@8
。 - 应用场景:常用于 Windows API 函数,这样可以减少代码重复,因为每个函数自身清理栈,无需调用者处理。
示例代码:
#include <iostream>
// 使用 __stdcall 调用约定的函数
int __stdcall add(int a, int b) {
return a + b;
}
int main() {
int result = add(1, 2);
std::cout << "Result: " << result << std::endl;
return 0;
}
2. __fastcall
- 参数传递顺序:部分参数通过寄存器传递,其余参数从右到左压入栈中。一般来说,前几个较小的参数(如
int
类型)会使用寄存器(如 ECX、EDX)传递,以提高参数传递的效率。 - 栈清理方式:由被调用函数负责清理栈中剩余的参数。
- 名称修饰:不同编译器的名称修饰规则不同。在 Visual C++ 中,函数名前会加
@
符号,后面跟着参数的总字节数。例如int func(int a, int b)
修饰后的名称可能是@func@8
。 - 应用场景:适用于对性能要求较高的函数,因为通过寄存器传递参数可以减少栈操作,提高函数调用的速度。
示例代码:
#include <iostream>
// 使用 __fastcall 调用约定的函数
int __fastcall subtract(int a, int b) {
return a - b;
}
int main() {
int result = subtract(5, 3);
std::cout << "Result: " << result << std::endl;
return 0;
}
3. __thiscall
- 参数传递顺序:
__thiscall
主要用于 C++ 类的成员函数。this
指针通过寄存器(通常是 ECX)传递,其他参数从右到左压入栈中。 - 栈清理方式:如果是普通成员函数,由被调用函数负责清理栈;如果是可变参数的成员函数,由调用者负责清理栈。
- 名称修饰:和
__cdecl
类似,不同编译器有不同的名称修饰规则。 - 应用场景:C++ 类的成员函数默认使用
__thiscall
调用约定。
示例代码:
#include <iostream>
class MyClass {
public:
// 成员函数默认使用 __thiscall 调用约定
int add(int a, int b) {
return a + b;
}
};
int main() {
MyClass obj;
int result = obj.add(1, 2);
std::cout << "Result: " << result << std::endl;
return 0;
}
4. __vectorcall
(仅适用于 Visual C++)
- 参数传递顺序:使用寄存器和栈混合传递参数,部分参数通过寄存器传递,其余参数从右到左压入栈中。它会优先使用寄存器来传递向量类型的参数,以提高性能。
- 栈清理方式:由被调用函数负责清理栈。
- 名称修饰:函数名会被修饰,遵循特定的规则。
- 应用场景:适用于处理向量类型数据且对性能有较高要求的函数。
调用者负责清理栈时之所以可以使用可变参数,主要是因为调用者清楚具体传递了多少个参数,从而能够正确地清理栈空间,下面从可变参数函数的工作原理、栈清理的要求以及调用者和被调用者的信息差异等方面详细解释。
为什么调用者清理就可以使用可变参?
可变参数函数的工作原理
可变参数函数允许在调用时传递数量和类型不确定的参数。在 C 和 C++ 中,常见的可变参数函数有 printf
系列函数,其使用 ...
表示可变参数部分。例如:
#include <stdio.h>
// 可变参数函数示例
int sum(int count, ...) {
va_list args;
va_start(args, count);
int result = 0;
for (int i = 0; i < count; ++i) {
result += va_arg(args, int);
}
va_end(args);
return result;
}
在上述代码中,sum
函数接收一个固定参数 count
表示可变参数的数量,然后使用 va_list
、va_start
、va_arg
和 va_end
等宏来处理可变参数。
栈清理的要求
在函数调用过程中,参数会被压入栈中。当函数执行完毕后,需要将这些参数从栈中移除,以恢复栈的原始状态,保证后续的函数调用能够正常进行。为了正确清理栈,必须知道压入栈的参数数量和每个参数的大小。
调用者和被调用者的信息差异
- 被调用者不清楚参数数量:对于可变参数函数,被调用者(函数本身)在编译时无法确定调用时具体传递了多少个参数。因为可变参数的数量和类型是在调用时动态确定的,所以被调用者无法准确知道需要清理多少栈空间。例如,在
sum
函数中,它只知道有一个固定参数count
,但不知道具体的可变参数数量,因此无法自行清理栈。 - 调用者知道参数数量:调用者在调用可变参数函数时,明确知道自己传递了多少个参数。例如,在调用
sum
函数时:
int main() {
int result = sum(3, 1, 2, 3);
return 0;
}
main
函数作为调用者,清楚地知道传递了 3 个可变参数,加上固定参数 count
,总共压入了 4 个参数到栈中。因此,调用者可以根据这些信息正确地清理栈。
总结
由于调用者在调用可变参数函数时知道具体传递的参数数量和大小,所以由调用者负责清理栈可以确保栈空间被正确恢复。而被调用者由于无法在编译时确定可变参数的具体情况,不适合负责栈清理工作。这就是为什么调用者清理栈的调用约定(如 __cdecl
)可以支持可变参数函数的原因。