背景引入

近期在看一本书,叫做《嵌入式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

但是结果却出人意料:

打印出的值和传进去的值完全不相等,甚至毫无规律可言。

问题分析

上述代码中,是通过取首个参数的地址,并往后移动这个指针来获得后面参数的,那么问题很可能出在两个地方:

  1. 指针移动的方式不正确
  2. 参数的地址排布可能不是连续的

我们一个一个来看,先暂且假定这些参数地址是连续的,且相隔一样的距离。那么我们就可以聚焦于指针的移动方式了。指针移动是“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语言中变参函数传参探究的更多相关文章

  1. 在Java中动态传参调用Python脚本

    最近,又接触到一个奇葩的接口,基于老板不断催促赶时间的情况下,在重写java接口和复用已有的python脚本的两条路中选择了后者,但是其实后者并没有好很多,因为我是一个对python的认识仅限于其名称 ...

  2. Python中的传参是传值还是传址?

    传值:在C++中,传值就是把一个参数的值给这个函数,其中的更改不会影响原来的值. 传址:即传引用,直接把这个参数的内存地址传递进去,直接去这个内存地址上进行修改. 但是这些在Python中都没有,Py ...

  3. apiCloud中openFrameGroup传参

    apiCloud中openFrameGroup传参 1.无效的 api.openFrameGroup({ // 打开 frame 组 name: 'group', scrollEnabled: fal ...

  4. Vue-CLI项目中路由传参

    Vue-CLI项目中路由传参 一.标签传参方式:<router-link></router-link> 第一种 router.js { path: '/course/detai ...

  5. Vue-cli中axios传参的方式以及后端取的方式

    0917自我总结 Vue-cli中axios传参的方式以及后端取的方式 一.传参 params是添加到url的请求字符串中的,用于get请求. data是添加到请求体(body)中的, 用于post请 ...

  6. java方法中,传参是传值还是传址问题(对比C语言、C#和C++)

    问题引出: 编写一个简单的交换值的小程序,如果我们只是简单地定义一个交换函数接收两个数,在函数内部定义一个中间变量完成交换.那么当我们把a,b两个实参传给这个函数时,往往得不到预期的结果.这是为什么呢 ...

  7. 兼容性js中setTimeout 传参“保值”方案

    这里所谓“保值”,是指在setTimeout中指定的时间后,执行指定的方法所用到的“参数”值,跟执行setTimeout时该“参数”值一样.是不是有点懵?看如下例子: ================ ...

  8. vue.js 1中父组件跳到子组件中并传参让子组件显示不同的内容

    父组件中的点击跳转: <ul class="insurance-entry clearfloat"> <li v-link="{name:'produc ...

  9. Mybatis 中在传参时,${} 和#{} 的区别

    介绍 MyBatis中使用parameterType向SQL语句传参,parameterType后的类型可以是基本类型int,String,HashMap和java自定义类型. 在SQL中引用这些参数 ...

随机推荐

  1. 为什么要使用MongoDB?

    1.Mongo与Mysql简单对比 关系型数据库-MySQL 1.在不同的引擎上有不同的存储方式. 2.查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 3.开源数据库的份额在不断增加 ...

  2. 利用C语言判别输入数的位数并正逆序输出

    利用C语言判别用户输入数的位数并正逆序输出 #include <stdio.h> void main() {        int i, scanfNum, printfNum, temp ...

  3. 【译】GO语言:管理多个错误

    原文:https://medium.com/a-journey-with-go/go-multiple-errors-management-a67477628cf1 ​ 关于开发者使用Go遇到的最大挑 ...

  4. LAMP——实现phpMyadmin、wordpress及Discuz应用部署

    一.环境准备 操作系统:Centos8.3.2011 软件:Apache2.4.37.Mysql8.0.21.PHP7.2.24 二.安装过程 1.安装phpmyadmin 1.1.安装软件包并启动服 ...

  5. C#/VB.NET 设置PDF跨页表格重复显示表头行

    在创建表格时,如果表格内容出现跨页显示的时候,默认情况下该表格的表头不会在下一页显示,在阅读体验上不是很好.下面分享一个方法如何在表格跨页时显示表格的表头内容,在C#中只需要简单使用方法grid.Re ...

  6. edraw max for mac 安装

    1.下载网址:https://xclient.info/s/edraw-max.html#versions 2.安装断网安装,直接打开 .dmg文件安装 3.按照阅读文件中的第二步,将opt.cell ...

  7. XCTF(MISC) give_you_flag

    题目描述:菜狗找到了文件中的彩蛋很开心,给菜猫发了个表情包 1.下载附件,点击查看 发现在数完钱后,有出现一个二维码的东西. 2.使用stegsolv工具,进行逐帧查看. 说个题外话,stegsolv ...

  8. libcurl库(C++)快速使用

    ---恢复内容开始--- libcurl是一个跨平台的网络协议库,支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议.libcurl同样支 ...

  9. JM操作数据库

    [前言] 为什么要去直连数据库,去操作数据库? 因为在我们做自动化的时候,或者在大批量准备数据的时候,自动化的时候有时候会生成很多条页面上,接口上无法删除的数据,那么就很有很多的测试数据遗留在系统上, ...

  10. JPcap入门

    1,参照入门:安装第一个代码:https://blog.csdn.net/qq_37638061/article/details/80710143 2,数据解析,不可用但有启发意义:https://b ...