深入理解C语言的函数参数

2023-12-14 20:41:58

1、一个简单的函数

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	printf("%d", Add(2, 3, 4, 5, 6));
	return 0;
}

????????这一段足够简单的代码,闭眼都能知道运行结果会在屏幕上打印 5 。那编译器是怎么处理后面的 4、5、6 ?

????????我们再看看这个函数。

void MyTest(int a, int b, int c, int d)
{
    printf("%d", a);
}

? ? ? ? ?似乎参数 b、c、d 的设定是多余的。不论这三个参数传入什么值,都不影响结果。那上述的 Add 函数是不是也能看作后续 4、5、6 对应的参数没有用到,所以没有表现出任何现象?

? ? ? ? 带着这个问题,再看一个函数:

void MyTest_2(int num)
{
	int* ptr = #
	for (int i = 0; i <= num; i++)
	{
		printf("%d ", *ptr);
		ptr++;
	}
}

? ? ? ? 是不是有点懵?对形参取地址是个什么操作?还要对形参的指针进行移动再打印出来又是什么鬼?预感上,大概率会报非法访问。

? ? ? ? 那不妨,我们在 main 函数里调用一下?

MyTest_2(0);
MyTest_2(1);
MyTest_2(10);

? ? ? ? 然而结果是:

? ? ? ? 虽然返回了一堆不明所以的值,但返回代码 0 说明程序压根就没有报错。而参数 0 、 1 、 10 都被完整打印出来了,简直是毁三观。

? ? ? ? 知道你很急,但是你先别急,再在 main 函数中试试这个足以让人懵逼的例子:

MyTest_2(5, 100, 20, 35, 40, 114514);

? ? ? ? 结果更毁三观了:?

? ? ? ? 什么玩意?明明 MyTest 创建时只设定了一个参数,为什么传入六个参数能全部打印出来?是不是说明,一开始的 Add 调用,后面的 4、5、6 也一并进行了传参,只是没有在函数内部进行使用?

? ? ? ? 要弄懂这个问题,首先得了解编译器对函数调用时的参数是怎么处理,传参过程又是怎么样的。

2、函数传参过程

2.1、栈帧建立之前

????????调用函数时,系统会在内存中创建对应函数的栈帧。关于栈帧建立及销毁这部分内容可以看这一篇开头部分:函数栈帧简述

? ? ? ? 而在进行栈帧建立之前,程序还会执行一系列的操作。以这段代码为例,直接在汇编中看看 ret 赋值时的 Add 调用,汇编指令到底做了什么:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int ret = Add(0xAB, 0xCD, 0xEF, 0xAA, 0xDD);

	return 0;
}

? ? ? ? 当前栈帧是 main 函数,根据以上汇编指令,在调用 Add 函数之前,程序将 0xAB、0xCD、0xEF、0xAA、?0xDD 这五个参数逆序放入 main 函数栈顶( ESP 是栈顶寄存器)。

? ? ? ? ?这一步实际上就是函数传参。通过这一步得出结论,不论函数在创建时定义了多少个形参,甚至不定义形参,只要在调用时,函数名后的括号内写入参数,就一定会进行传参。

2.2、参数调用

? ? ? ? 上述两句汇编代码首先是将 ebp+8 位置的值存入 eax 寄存器,再让 eax 寄存器中的值 +=? ebp+12 位置的值。而 ebp+8 的地址与 ebp+12 的地址分别储存了 0xAB 和 0xCD 。

? ? ? ? 至此就是一次完整的传参及参数调用。?

? ? ? ? 也就是说,只需要知道第一个参数的地址,那么剩下的参数即使不在创建函数时定义,也可以通过第一个参数的地址进行访问。就此,最开始的 MyTest_2 函数产生的现象也就解释完毕。

3、可变参数列表

3.1、定义阐述

? ? ? ? 严格来说 C 语言的函数参数数量并不是固定的,那么在应用上根据传入的各个参数类型及第一个参数的地址,对函数传入任意参数个数,只需要通过某种方式在函数内部进行调用,那么函数的灵活性和扩展性就大大提高了。

? ? ? ? 很好, printf 也是这么想的。在使用 printf 时,第一个参数中有几个占位符,后续就带几个参数,各位对这规则应该已经形成肌肉记忆了。而对于之前的 MyTest_2 函数,唯一定义的参数便是后续传入有效参数的个数。而为了语义上更加直观,像这类可对后续参数进行操作的函数在创建时,一般会加上三个点。当然,也是为了语义,将变量名改为 argc (argument count):

void MyTest_2(int argc, ...)
{
	int* ptr = &argc;
	for (int i = 0; i <= argc; i++)
	{
		printf("%d ", *ptr);
		ptr++;
	}
}

? ? ? ? 如以上函数中用其中某个参数确定后续参数的个数,那么这一系列参数就叫可变参数列表。

3.2、初步实现

? ? ? ? 虽然在 MyTest_2 中已经初步实现了带可变参数列表的函数创建,但这个函数好像没什么用。所以这里再举一个例子,求若干浮点数的和:

double Sum(int argc, ...)
{
	double sum = 0.;
	//创建可变参数列表的头部指针,将指针指向列表第一个元素
	double* ptr = (double*)(&argc + 1);
    //遍历可变参数列表,求和
	for (int i = 1; i <= argc; i++)
	{
		sum += *ptr;
        //指针指向下一个参数
		ptr++;
	}
	return sum;
}

? ? ? ? 这代码貌似没问题,但传入的参数列表仅限于 double 类型,如果传入的参数是一个整型变量呢?由于内部只能通过指针访问,根本无法知晓外部传入的变量类型,而且编译器也不会对可变参数列表中的参数类型作检查。

? ? ? ? 所以,如果列表的参数类型不一致,第一个参数除了附带参数的数量信息外,还应附带每个参数的类型。解决办法可以参照 printf 的第一个参数。在此之前,先了解一个点,函数在传参时,汇编指令会对参数进行类型提升和?4 字节对齐。也就是说,char、short 的类型会被提升为 int ,而 float 类型直接提升为 double 。

????????修改后如下:

//format字符串只允许d或f,不区分大小写
double Sum(const char* format, ...)
{
	double sum = 0.;
	int count = strlen(format);

	//创建可变参数列表的头部指针,将指针指向列表第一个元素
	char* ptr = (char*)(&format) + sizeof(char*);

	for (int i = 0; i < count; i++)
	{
		//遇到字符d或者D以整型处理
		if (format[i] == 'd' || format[i] == 'D')
		{
			sum += (double)*((int*)ptr);
			//指针指向下一个参数
			ptr += sizeof(int);
		}
		//遇到字符f或者F以双精度浮点型处理
		else if (format[i] == 'f' || format[i] == 'F')
		{
			sum += *((double*)ptr);
			//指针指向下一个参数
			ptr += sizeof(double);
		}
	}
	return sum;
}

? ? ? ? 至此已经很接近 printf 的参数调用方式了。

3.3、可变参数列表宏

? ? ? ? 调用 stdio.h 头文件便可以使用专用于处理可变参数列表的四个宏:

? ? ? ? va_list:用于创建读取可变参数列表的指针;

typedef char* va_list;

? ? ? ? __crt_va_start:将可变参数列表的指针指向列表第一个参数;

#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)

? ? ? ? __crt_va_arg:获取可变参数列表的指针当前指向的参数,并将指针指向下一个参数;

#define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

? ? ? ? __crt_va_end:用于销毁可变参数列表的指针。

#define __crt_va_end(ap) ((void)(ap = (va_list)0))

? ? ? ? 此外对上述 _INTSIZEOF 和 _ADDRESSOF 也需要作了解:

#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

? ? ? ? 上述的?_INTSIZEOF 比较难以理解。它的运算结果是 4 字节对齐,这个公式有点巧妙,有兴趣可以自行理解。

? ? ? ? 接下来先将上面的代码用这几个宏改造一下:

double Sum(const char* format, ...)
{
	double sum = 0.0;
	va_list ptr;
	__crt_va_start(ptr, format);

	for (int i = 0; i < strlen(format); i++)
	{
		if (format[i] == 'd' || format[i] == 'D')
		{
			sum += __crt_va_arg(ptr, int);
		}
		else if (format[i] == 'f' || format[i] == 'F')
		{
			sum += __crt_va_arg(ptr, double);
		}
	}
	__crt_va_end(ptr);
	return sum;
}

? ? ? ? 不过这几个宏不推荐使用,因为随着编译器的不同,很可能某些编译器并不支持这些宏,可移植性大大降低。这里主要是提供宏的思路,至于宏的实现也已经展示,各位完全可以根据这些宏通过纯 C 代码实现。

文章来源:https://blog.csdn.net/Peeepi/article/details/134995317
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。