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语言自我修养——从芯片、编译器到操作系统》

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

(0)

相关推荐

  • c语言基于stdarg.h的可变参数函数的用法

    C语言编程中有时会遇到一些参数个数可变的函数,本文详细讲解了可变参数函数的实现原理,分享给大家 在开始学习C语言的函数的时候,我们就知道函数的参数个数应该是在函数声明的时候就指定的,这一点我们没有任何疑问.但是不知道大家有没有注意到我们的printf()函数,他的函数参数理论上并不是确定的,而是随着匹配字符串中的格式控制符的个数控制的.其实当时也没有注意到这一点,到是最近,偶然间看到了 <嗨翻C语言> 这本书,这里就详细讲解了这种可变参数函数的实现原理,今天考试间隙就顺带学习了一下,其实就是一

  • C语言中编写可变参数函数

    通过stdarg.h头文件为函数提供了定义可变参数列表的能力.声明一个可变参数的函数类似: void f1(int n,...); 其中n表示参数列表个数,而用省略号来表示未知参数列表.stdarg.h中提供了一个va_list类型,用于存放参数.一个大概的使用过程类似: void f1(int n,...) { va_list ap; va_start(ap,n); //初始化参数列表 double first=va_arg(ap,double); //取第一个参数 int second=va

  • C语言可变参数函数详解示例

    先看代码 复制代码 代码如下: printf("hello,world!");其参数个数为1个.printf("a=%d,b=%s,c=%c",a,b,c);其参数个数为4个. 如何编写可变参数函数呢?我们首先来看看printf函数原型是如何定义的.在linux下,输入man 3 printf,可以看到prinf函数原型如下: 复制代码 代码如下: SYNOPSIS#include <stdio.h>int printf(const char *form

  • C语言中变参函数传参的实现示例

    目录 背景引入 问题分析 指针大小 参数位置排布 解决问题 额外的测试 总结 参考资料 背景引入 近期在看一本书,叫做<嵌入式C语言自我修养>,写的内容对我帮助很大,是一本好书.在第6章,GNU C编译器扩展语法精讲一节,这本书给出了一些变参函数的例子: //1.变参函数初体验 #include<stdio.h> void print_num(int count,...) { int *args; args = &count + 1; for(int i = 0;i <

  • ajax中data传参的两种方式分析

    本文实例讲述了ajax中data传参的两种方式.分享给大家供大家参考,具体如下: 1. POST方式: /** * 订单取消 * @return {Boolean} 处理是否成功 */ function orderCancel(orderId, commant){ var flag = false; $.ajax({ type: "POST", url: "../order/orderCancel.action", //orderModifyStatus data:

  • 实例讲解Vue.js中router传参

    Vue-router参数传递 为什么要在router中传递参数 设想一个场景,当前在主页中,你需要点击某一项查看该项的详细信息.那么此时就需要在主页传递该项的id到详情页,详情页通过id获取到详细信息. vue-router 参数传递的方式 Parma传参 贴代码: /router/index.vue export default new Router({ routes: [ { path: '/', name: 'Home', component: Home }, { path: '/work

  • Vue-CLI项目中路由传参的方式详解

    一.标签传参方式:<router-link></router-link> 第一种 router.js { path: '/course/detail/:pk', name: 'course-detail', component: CourseDetail } 传递层 <!-- card的内容 { id: 1, bgColor: 'red', title: 'Python基础' } --> <router-link :to="`/course/detail

  • vue中路由传参以及跨组件传参详解

    路由跳转 this.$router.push('/course'); this.$router.push({name: course}); this.$router.go(-1); this.$router.go(1); <router-link to="/course">课程页</router-link> <router-link :to="{name: 'course'}">课程页</router-link> 路由

  • Python中引用传参四种方式介绍

    目录 引用传参一: ​引用传参二: ​​引用传参三: ​​引用传参四: 总结 引用传参一: ​​>>> a = 100 #这里的a是不可变类型 >>> def test(a): ... a+=a #这个式子有两层含义:1.这里可能是重新定义一个新的变量a,2.也有可能是修改a的值,但由于全局 #变量a不能修改,所以此处是重新定义了一个a: ... print("函数内:%d"%a) ... >>> test(a) 函数内:200 &

  • C语言中可变参数的使用方法示例

    前言 在C语言程序编写中我们使用最多的函数一定包括printf以及很多类似的变形体.这个函数包含在C库函数中,定义为 int printf( const char* format, ...); 除了一个格式化字符串之外还可以输入多个可变参量,如: printf("%d",i); printf("%s",s); printf("the number is %d ,string is:%s", i, s); 格式化字符串的判断本章暂且不论,下面分析一

  • GO语言中常见的排序算法使用示例

    目录 快排 冒泡 选择排序 插入排序 希尔排序 二分法查找 快排 package main import ( "fmt" "math/rand" "time" ) func main() { li:=[]int{1,3,5,2,4,6,9,7} left:=0 right:=len(li)-1 fmt.Println(quick_sort(li,left,right)) } func quick_sort(li []int, left,right

  • go语言中如何使用select的实现示例

    目录 1.基本语法 2.select语句的实际应用 在golang语言中,select语句 就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case动作. 有了 select语句,可以实现 main主线程 与 goroutine线程 之间的互动. 1.基本语法 select { case <-ch1 : // 检测有没有数据可读 // 一旦成功读取到数据,则进行该case处理语句 case ch2 <- 1 : // 检测有没有数据可写 // 一旦成功向ch2写入数据,

  • Mybatis中#{}和${}传参的区别及#和$的区别小结

    最近在用mybatis,之前用过ibatis,总体来说差不多,不过还是遇到了不少问题,再次记录下, 比如说用#{},和 ${}传参的区别, 使用#传入参数是,sql语句解析是会加上"",比如 select * from table where name = #{name} ,传入的name为小李,那么最后打印出来的就是 select * from table where name = '小李',就是会当成字符串来解析,这样相比于$的好处是比较明显对的吧,#{}传参能防止sql注入,如果

随机推荐