首页 > 学院 > 开发设计 > 正文

Linux下C库学习 - stdarg.h

2019-11-10 20:16:54
字体:
来源:转载
供稿:网友

想要函数使用可变参数,那就必须要包含stdarg.h这个头文件,简单就不说了,我们重新来看看可变参数的定义和使用吧。

1.声明可变参数

可变参数的声明有两点

使用‘…’来代表可变参数可变参数之前必须有一个命名的参数

简单说就是如果你想声明一个可变参数的函数,那么有两种形式

func(...) //错误,前面必须有一个命名的参数func(xxx,...) //正确,xxx可以用任意的参数代替,比如char *name,int i都可以

2.定义可变参数

可变参数的定义和声明相同,两者保持一致即可

3.可变参数的使用

要使用可变参数,主要会用到下列几个函数

#include <stdarg.h>void va_start(va_list ap, last);type va_arg(va_list ap, type);void va_end(va_list ap);void va_copy(va_list dest, va_list src);

这里四个函数是参考man手册上的,最后的va_copy没有用过,不太清楚什么情况下需要使用,麻烦各位在留言赐教 剩下的三个我们来一个一个看,这里举个简单的例子方便讲解

#include <stdio.h>#include <stdarg.h>void fun(char *name, ...){ va_list pa; int a, b; char *c; double d; va_start(pa, name); a = va_arg(pa, int); b = va_arg(pa, int); c = va_arg(pa, char *); d = va_arg(pa, double); va_end(pa); PRintf("a = %d/n", a); printf("b = %c/n", b); printf("c = %s/n", c); printf("d = %f/n", d);}int main(int argc, char *argv[]){ fun("func", 1, 'a', "abc", 1.0); return 0;}

可以看到,要使用可变参数,首先需要定义一个va_list类型的变量,这个变量就相当于是指向可变参数列表的指针,通过va_start函数将这个指针赋值,后面就可以通过va_arg来获取每一个参数。

va_arg的函数原型里面有个type,这个type类型怎么理解呢? 大家可以这样理解,因为可变参数是没有声明参数类型的,那么编译器怎么去检测到底类型是否匹配呢?最简单的办法就是向上提升,比如

float类型的实际参数将提升到doublechar、short和相应的signed、unsigned类型的实际参数提升到int如果int不能存储原值,则提升到unsigned int

由于默认肯定会向上提升,所以一定要尽量避免以下类型的参数 type绝对不能为以下类型:

char、signed char、unsigned charshort、unsigned shortsigned short、short int、signed short int、unsigned short intfloat

因此在示例中,第一个int类型的数字我用int类型接收,第二个字符我依然使用int类型来接收,第三个字符串就必须使用char *类型的接收,第四个浮点型使用double类型来接收,如果你不小心写错了类型,系统的提示如下(这里我把示例中的double改成了float)

test.c: In function ‘fun’:test.c:17:20: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’ [enabled by default] d = va_arg(pa, float); ^test.c:17:20: note: (so you should pass ‘double’ not ‘float’ to ‘va_arg’)test.c:17:20: note: if this code is reached, the program will abort

最后的va_end就相当于是结束标记,一个va_start必须和一个va_end对应起来使用才可以。

使用的话我想大家应该都会,那具体原理是什么样的呢? 要搞清楚原理,首先需要知道参数到底是怎么传递进来的,事实上,在进程中,堆栈地址时由高向低分配的,在调用参数的时候,首先入栈的函数参数,接下来是函数的返回地址,再下来是函数的执行代码,而参数的入栈顺序是先入最后一个参数,最后入第一个参数

这里写图片描述

如上图所示,参数在堆栈的排列是从高地址向低地址的,实际上具体是宏定义如下

typedef char * va_list; // x86平台下va_list的定义#define va_start(ap, v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //第一个可选参数地址#define va_arg(ap, t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址#define va_end(ap) ( ap = (va_list)0 ) // 将指针置为无效

这里有个宏_INTSIZEOF需要特别介绍下,这个宏是为了求出变量所占内存空间的大小,具体实现如下

#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

网上有位大神对这个函数又深刻的理解,大家可以去看看他的文章_INTSIZEOF(n)这个函数简单说就是把n转化成int的整数倍,来实现格式对齐,明白了这里,我们接着往下看

va_start 这个应该很好理解,就是说固定参数的地址加上他本身的内存大小,结合上面的图也就是第一个可变参数的地址,这样ap就指向了第一个可变参数,后面我们通过ap就可以得到其他的参数

va_arg 这个宏写的有些复杂,我们需要把它拆成两部分看 1. ap += _INTSIZEOF(t); // 此时指针ap已经指向下一个参数了 /* ap减去当前参数的大小得到当前参数的地址,再把地址强制类型转换后返回它的值 */ 2. return (t )( ap - _INTSIZEOF(t)) 通过第一步我们让指针ap指向了后一个参数,通过第二步返回了当前的参数

va_end 这个宏很简单,就是清空了指针,记着需要和va_start配套使用

看了C语言的具体实现,不得不感叹,大神们真的是把指针使用的淋漓尽致,不过任何事情都有两方面,这样做虽然高效快捷,但同时也留下了不小的安全隐患

参考文档 深入浅出va函数 关于va_arg中的type


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表