背景引入

近期在看一本书,叫做《嵌入式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. 流程自动化RPA,Power Automate Desktop系列 - 发布文档中心

    一.背景 内网中有一个基于VuePress搭建的静态文档中心,但是每次修改后都需要重新Build一次才行,之前都是手动执行命令,现在可以基于Power Automate Desktop来创建任务了. ...

  2. sys用户权限不足,本地登录失败 |ORA-01031 insufficient privileges|

    机器总喜欢挑放假的时候出问题,"双节"(中秋.国庆)快到了,对于搞系统运维的工程师来说其实并不轻松,于是今天赶紧装起一台数据库备用服务器以备半夜"机"叫. 安装 ...

  3. 『心善渊』Selenium3.0基础 — 19、使用Selenium操作文件的上传和下载

    目录 1.Selenium实现文件上传 (1)页面中的文件上传说明 (2)文件上传示例 (3)总结 2.Selenium实现文件下载 (1)Firefox浏览器文件下载 1)操作步骤: 2)文件下载示 ...

  4. 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换

    欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...

  5. POJ 1556 计算几何 判断线段相交 最短路

    题意: 在一个左下角坐标为(0,0),右上角坐标为(10,10)的矩形内,起点为(0,5),终点为(10,5),中间会有许多扇垂直于x轴的门,求从起点到终点在能走的情况下的最短距离. 分析: 既然是求 ...

  6. CentOS-Docker安装MongoDB(单点)

    下载镜像 $ docker pull mongo 创建相关目录 $ mkdir -p /usr/mongo/data /usr/mongo/dump 运行镜像 $ docker run --resta ...

  7. 在Java中如何高效判断数组中是否包含某个元素

    如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Overflow中也是一个非常热门的问题.在投票比较高的几个答案中给出了几种 ...

  8. 题解 AtCoder Beginner Contest 168

    小兔的话 欢迎大家在评论区留言哦~ AtCoder Beginner Contest 168 A - ∴ (Therefore) B - ... (Triple Dots) C - : (Colon) ...

  9. tcp三次握手四次挥手----转

    序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生:给字节编上序号后,就给每一个报文段指派一个序号:序列号seq就是这个报文 ...

  10. Caffeine缓存的简单介绍

    1.简介 在本文中,我们将了解Caffeine,一个用于Java的高性能缓存库. 缓存和Map之间的一个根本区别是缓存会清理存储的项目. 一个清理策略会决定在某个给定时间哪些对象应该被删除,这个策略直 ...