C语言中变参函数传参探究
背景引入
近期在看一本书,叫做《嵌入式C语言自我修养》,写的内容对我帮助很大,是一本好书。在第6章,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个字节,这是因为对于"int"型指针的移动操作,是以4(sizeof(int))为基本单位的。同理,对于"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"型指针自增操作不适用于笔者,笔者将其改为“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语言自我修养——从芯片、编译器到操作系统》
欢迎大家转载本人的博客(需注明出处),本人另外还有一个个人博客网站:lularible的个人博客,欢迎前去浏览。
C语言中变参函数传参探究的更多相关文章
- 在Java中动态传参调用Python脚本
最近,又接触到一个奇葩的接口,基于老板不断催促赶时间的情况下,在重写java接口和复用已有的python脚本的两条路中选择了后者,但是其实后者并没有好很多,因为我是一个对python的认识仅限于其名称 ...
- Python中的传参是传值还是传址?
传值:在C++中,传值就是把一个参数的值给这个函数,其中的更改不会影响原来的值. 传址:即传引用,直接把这个参数的内存地址传递进去,直接去这个内存地址上进行修改. 但是这些在Python中都没有,Py ...
- apiCloud中openFrameGroup传参
apiCloud中openFrameGroup传参 1.无效的 api.openFrameGroup({ // 打开 frame 组 name: 'group', scrollEnabled: fal ...
- Vue-CLI项目中路由传参
Vue-CLI项目中路由传参 一.标签传参方式:<router-link></router-link> 第一种 router.js { path: '/course/detai ...
- Vue-cli中axios传参的方式以及后端取的方式
0917自我总结 Vue-cli中axios传参的方式以及后端取的方式 一.传参 params是添加到url的请求字符串中的,用于get请求. data是添加到请求体(body)中的, 用于post请 ...
- java方法中,传参是传值还是传址问题(对比C语言、C#和C++)
问题引出: 编写一个简单的交换值的小程序,如果我们只是简单地定义一个交换函数接收两个数,在函数内部定义一个中间变量完成交换.那么当我们把a,b两个实参传给这个函数时,往往得不到预期的结果.这是为什么呢 ...
- 兼容性js中setTimeout 传参“保值”方案
这里所谓“保值”,是指在setTimeout中指定的时间后,执行指定的方法所用到的“参数”值,跟执行setTimeout时该“参数”值一样.是不是有点懵?看如下例子: ================ ...
- vue.js 1中父组件跳到子组件中并传参让子组件显示不同的内容
父组件中的点击跳转: <ul class="insurance-entry clearfloat"> <li v-link="{name:'produc ...
- Mybatis 中在传参时,${} 和#{} 的区别
介绍 MyBatis中使用parameterType向SQL语句传参,parameterType后的类型可以是基本类型int,String,HashMap和java自定义类型. 在SQL中引用这些参数 ...
随机推荐
- 为什么要使用MongoDB?
1.Mongo与Mysql简单对比 关系型数据库-MySQL 1.在不同的引擎上有不同的存储方式. 2.查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 3.开源数据库的份额在不断增加 ...
- 利用C语言判别输入数的位数并正逆序输出
利用C语言判别用户输入数的位数并正逆序输出 #include <stdio.h> void main() { int i, scanfNum, printfNum, temp ...
- 【译】GO语言:管理多个错误
原文:https://medium.com/a-journey-with-go/go-multiple-errors-management-a67477628cf1 关于开发者使用Go遇到的最大挑 ...
- LAMP——实现phpMyadmin、wordpress及Discuz应用部署
一.环境准备 操作系统:Centos8.3.2011 软件:Apache2.4.37.Mysql8.0.21.PHP7.2.24 二.安装过程 1.安装phpmyadmin 1.1.安装软件包并启动服 ...
- C#/VB.NET 设置PDF跨页表格重复显示表头行
在创建表格时,如果表格内容出现跨页显示的时候,默认情况下该表格的表头不会在下一页显示,在阅读体验上不是很好.下面分享一个方法如何在表格跨页时显示表格的表头内容,在C#中只需要简单使用方法grid.Re ...
- edraw max for mac 安装
1.下载网址:https://xclient.info/s/edraw-max.html#versions 2.安装断网安装,直接打开 .dmg文件安装 3.按照阅读文件中的第二步,将opt.cell ...
- XCTF(MISC) give_you_flag
题目描述:菜狗找到了文件中的彩蛋很开心,给菜猫发了个表情包 1.下载附件,点击查看 发现在数完钱后,有出现一个二维码的东西. 2.使用stegsolv工具,进行逐帧查看. 说个题外话,stegsolv ...
- libcurl库(C++)快速使用
---恢复内容开始--- libcurl是一个跨平台的网络协议库,支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议.libcurl同样支 ...
- JM操作数据库
[前言] 为什么要去直连数据库,去操作数据库? 因为在我们做自动化的时候,或者在大批量准备数据的时候,自动化的时候有时候会生成很多条页面上,接口上无法删除的数据,那么就很有很多的测试数据遗留在系统上, ...
- JPcap入门
1,参照入门:安装第一个代码:https://blog.csdn.net/qq_37638061/article/details/80710143 2,数据解析,不可用但有启发意义:https://b ...