C语言变参函数传参机制深度解析:指针移动与栈帧偏移

程一诺
程一诺
发布时间:2026-10-13| 5人浏览过
原创

本文通过实际代码测试,深入探讨C语言变参函数的传参机制。针对书中示例在64位系统下打印结果异常的问题,分析了指针自增步长与参数在栈中非连续排布两个关键因素。通过汇编代码验证,揭示了首参与次参之间存在28字节偏移的现象,并指出32位与64位系统下编译器处理差异,强调动手实践对理解底层机制的重要性。

背景引入

近期阅读《嵌入式C语言自我修养》一书,内容详实且极具参考价值。在第六章“GNU C编译器扩展语法精讲”中,书中列举了若干变参函数的使用示例,旨在展示如何手动处理可变参数。

//1.变参函数初体验
#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count + 1;
	for(int i = 0;i < count;i++)
	{
		printf("*args:%d\n",*args);
		args++;
	}
}

int main(void)
{
	print_num(5,1,2,3,4,5);
	return 0;
}

上述示例代码逻辑直观:定义一个名为print_num的变参函数,在函数体内首先获取第一个参数的地址并赋值给指针变量,随后通过移动该指针来依次获取后续参数并进行打印。在main函数中向print_num传递了6个参数(含计数值),按照书中描述的逻辑,预期输出应如下所示:

*args:1 *args:2 *args:3 *args:4 *args:5

然而,实际运行结果却出乎意料:

打印输出的数值与传入的参数值完全不符,且呈现出无规律的状态。

问题分析

前述代码通过获取首个参数地址并移动指针来获取后续参数,问题可能源于以下两个方面:

  • 指针移动的步长计算方式不正确
  • 参数在内存中的地址排布并非连续

我们逐一排查。首先假设参数地址是连续的且间距一致,将焦点集中在指针移动方式上。指针的移动由“args++”这一语句控制。笔者对书中代码进行了修改,增加了打印每个参数地址的逻辑:

#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count;
	for(int i = 0;i <= count;i++)
	{
		printf("addr:%p\n",args);
		printf("*args:%d\n",*args);
		args++;
	}
}

int main(void)
{
	print_num(5,1,2,3,4,5);
	return 0;
}

主要增加了对于每个参数的地址的打印,运行结果如下:

通过观察发现,“args++”每次使指针向后移动4个字节。这是因为在C语言中,int类型指针的自增操作是以sizeof(int)(即4字节)为基本单位进行的。同理,char类型指针的移动单位是1字节(sizeof(char))。

指针大小

如果int类型指针的大小确实是4字节,那么上述指针移动操作在逻辑上似乎没有错误。然而,int类型指针的大小真的恒等于4吗?

笔者编写了测试代码来验证不同指针类型的大小:

#include<stdio.h>

int main()
{
	char*	charPoint;
	int*	intPoint;
	double*	doublePoint;

	struct st{
		int first;
	};

	struct st *structPoint;

	printf("sizeof(char*):%ld\n",sizeof(charPoint));
	printf("sizeof(int*):%ld\n",sizeof(intPoint));
	printf("sizeof(double*):%ld\n",sizeof(doublePoint);
	printf("sizeof(struct*):%ld\n",sizeof(structPoint));
	return 0;
}

运行结果:

结果显示,不仅int类型指针的大小为8字节,char、double以及结构体指针的大小也均为8字节。这是由于笔者所使用的计算机安装的是64位操作系统。因此,书中代码中针对int类型指针的自增操作在64位环境下不再适用。笔者尝试将代码修改为“args += 2”,在Dev C++ IDE中得到了正确结果,但在Ubuntu GCC环境下依然未能得到正确输出。

参数位置排布

解决了指针移动步长的问题后,依然无法获得正确答案。笔者推测参数在内存中的地址可能并非连续排列。如何查看函数参数的地址信息?方法众多,笔者选择了一种较为快捷的方式——查看汇编代码。

在Ubuntu终端中输入以下命令:

gcc -S [源文件]

即可生成一个带有“.s”后缀的汇编代码文件。

我们将main函数与print_num函数中涉及参数传递的部分进行对比分析:

在main函数中,各个参数被放入不同的寄存器;在print_num函数中,再从寄存器中将参数取出并放入print_num的函数栈帧中。仔细观察各个参数最终被放入栈帧的位置,发现第一个参数地址与第二个参数地址之间相差28个字节,而后续参数地址之间的间距均为8个字节。这解释了为何之前的代码运行结果不正确。

解决问题

因此,只需在第一个参数地址的基础上加上28字节的偏移量(针对char*类型指针)即可修正问题。

运行结果符合预期:

但是,为何第一个参数与第二个参数之间间隔28字节,笔者目前尚不清楚,推测需要查阅GCC编译器相关文档以获取更深入的解释。

额外的测试

以往对于固定参数个数的普通函数,传参规则通常是:前几个参数放入寄存器,若参数个数超出寄存器承载范围,则剩余参数压入函数栈帧。笔者好奇变参函数是否遵循相同规则,于是向print_num传递了18个参数进行测试:

汇编代码如下:

这表明变参函数的传参规则与普通函数并无二致。

总结

在阅读书籍时,笔者习惯边看边敲代码。此次按照书中示例编写的代码运行结果异常,从而引发了上述探究过程。若缺乏动手实践,未来遇到类似问题时可能会感到困惑。因此,动手实践对于理解技术细节至关重要。

此外,书中的内容并非绝对正确,其有效性往往依赖于特定的前提条件。例如,若使用32位系统,且编译器在处理变参函数时将参数连续压栈,那么书中的代码将是完全正确的。我们无需畏惧这些潜在陷阱,关键在于找到前提条件,探究问题本质,并最终解决问题。

参考资料

《嵌入式C语言自我修养——从芯片、编译器到操作系统》

到此这篇关于C语言中变参函数传参的实现示例的文章就介绍到这了,更多相关C语言变参函数传参内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。