C函数和宏中的可变参数
一:调用惯例
函数的调用方和被调用方对函数如何调用应该有统一的理解,否则函数就无法正确调用。比如foo(int n, int m),调用方如果认为压栈顺序是m,n,而foo认为压栈顺序是n, m,那么这个函数就不会调用成功。
因此,函数的调用方和被调用方对于函数如何调用需要有个明确的约定,双方都遵守同样的约定,函数才能调用成功,这种约定称为调用惯例,一个调用惯例一般会规定如下几个方面的内容:
1:函数参数的传递顺序和方式
函数参数的传递有多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己在从栈中将参数取出。如果有多个参数,调用惯例要规定函数调用方参数压栈的顺序:从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。
2:栈的维护方式
在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,使得栈在函数调用前后保持一致。这个弹出的工作可以由函数调用方完成,也可以由函数本身完成。
3:名字修饰策略
不同的调用惯例对函数名有不同的修饰策略
在C中,存在多个调用惯例,默认的调用惯例是cdecl,任何一个没有显示执行调用惯例的函数默认都是cdecl惯例。另外,_cdecl是非标准关键字,在不同的编译器中可以有不同的写法,比如在GCC中,使用:__attribute__((cdecl))。cdecl的调用惯例:参数从右至左的顺序入栈,参数出栈由调用方完成。
除了cdecl调用惯例之外,还存在许多别的调用惯例,比如stdcall,fastcall等,不再赘述。
二:函数中的可变参数
printf函数的原型如下:
int printf(const char *format, ...);
printf函数就是可变参数的典范。除了第一个参数类型为const char *之外,可以追加任意数量,任意类型的参数。
可变参数的实现,得益于C语言默认的cdecl调用惯例,它从右向左进行参数的入栈,比如函数:int sum(unsigned num, ...);num表示后面会传递num个整数,当调用sum时:int n = sum(3, 16, 38, 53);参数在栈上的布局如下图:
函数内部,可以使用num得到数字3,而且其他参数在栈上的排列就是在num的高地址方向,从而可以通过num的地址计算出其他参数的地址,所以,sum函数的实现如下:
int sum(unsigned num, ...)
{
int *p = &num + 1;
int ret = 0;
while(num--)
ret += *p++; return ret;
}
所以,cdecl调用惯例保证了参数的正确处理,但是在调用sum函数的时候,必须要知道有多少个不定参数,每个不定参数的类型是什么。因此printf在format中指定了参数类型和参数个数。
可以使用stdarg.h中定义的宏来访问各个不定参数:
#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);
首先需要定义va_list类型的变量:va_list ap; 该变量会依次指向各个可变参数。 va_list实际上是一个指针,指向各种不定参数,因类型不同,所以va_list以void *或char *为最佳选择。
ap必须首先使用va_start初始化,ap只有经过va_start初始化之后,才可以被后续的va_arg和va_end使用。
va_start(ap, last);其中last是函数最后一个具名参数,比如printf中的format。因va_start中会使用last的地址,因此该变量不能是寄存器变量,也不能是函数或者数组类型。
经过va_start初始化之后,ap指向第一个可变参数。
va_arg宏返回当前可变参数的值,并使ap指向下一个可变参数:type va_arg(va_list ap, type); type是ap指向的当前可变参数的类型。该宏还会修改ap,使其指向下一个可变参数。
如果后续没有可变参数了,或者type与实际的可变参数类型不符,则va_arg会发生不可预知的错误。
同一个函数中,每一个va_start必须跟着一个相应的va_end。经过va_end之后,ap将是未定义的。一般是将指针ap置NULL。
这些宏可以如下实现:
#define va_list char *
#define va_start(ap, arg) (ap = (va_list)(&arg) + sizeof(arg))
#define va_arg(ap, t) (*(t*)((ap += sizeof(t)) – sizeof(t)))
#define va_end(ap) (ap = (va_list)0)
va_copy将src复制到dest。行为上类似于,以同样的步骤和参数,将之前对src的调用,施加于dest上。
va_start、va_arg、va_end和va_copy都是线程安全的。其中va_start、va_arg和va_end是C89中就定义的,在C99中,又新增了va_copy。
下面的例子,是实现一个printf函数的简易版:
#include <stdio.h>
#include <stdarg.h> void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s; va_start(ap, fmt);
while (*fmt)
switch (*fmt++) {
case 's': /* string */
s = va_arg(ap, char *);
printf("string %s\n", s);
break;
case 'd': /* int */
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case 'c': /* char */
/* need a cast here since va_arg only
takes fully promoted types */
c = (char) va_arg(ap, int);
printf("char %c\n", c);
break;
}
va_end(ap);
}
三:宏中的可变参数
C99中,类似于函数,宏也可以接受可变参数。比如下面的例子:
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)
其中的”...”就是可变参数。在宏的调用中,它会展开为最后一个命名参数之后,’)’之前的所有token,包括逗号,并替换掉宏体中的”__VA_ARGS__”。
比如下面的语句:
debug("the int is %d, string is %s\n", 3, "hello, world");
经过宏替换之后,展开为下面的语句:
fprintf (stderr, "the int is %d, string is %s\n", 3, "hello, world");
在GCC中,除了支持上面的写法之外,还支持使用更具描述性的名字表示可变参数,而不使用”__VA_ARGS__”。使用”argname...”的写法表示可变参数,其中argname是参数名,可以任意取。比如上面的宏定义,可以写成下面的形式,效果是一样的:
#define debug(format, arg...) fprintf(stderr, format, arg)
但是,上面两种写法的debug宏还有一个共同的问题,就是必须提供一个可变参数,否则无法匹配宏,而且会出现语法错误,比如下面的语句:
debug("hehe\n");
展开后,就会扩展为下面的语句:
fprintf(stderr, "hehe\n", );
这显然会报语法错误。
解决这个问题有两种方法:
1:将所有参数都当成可变参数,将宏定义成下面的形式:
#define debug(...) fprintf (stderr, __VA_ARGS__)
或是:
#define debug(arg...) fprintf(stderr, arghehe)
2:使用”##”。当将”##”置于逗号和可变参数之间时,它有特殊的意义。比如下面的写法:
#define debug(format, ...) fprintf (stderr, format, ##__VA_ARGS__)
或是:
#define debug(format, arg...) fprintf(stderr, format, ##arg)
这种情况下,当省略可变参数时,”##”会使得预编译器删除它前面的逗号。当确实提供了可变参数时,”##”不起作用,像正常的可变参数一样进行替换。
推荐使用第二种方法。
参考:
《程序员的自我修养》
https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html
https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html
C函数和宏中的可变参数的更多相关文章
- c语言中的# ## 可变参数宏 ...和_ _VA_ARGS_ _
1.#假如希望在字符串中包含宏参数,ANSI C允许这样作,在类函数宏的替换部分,#符号用作一个预处理运算符,它可以把语言符号转化程字符串.例如,如果x是一个宏参量,那么#x可以把参数名转化成相应的字 ...
- 【转】C,C++中使用可变参数
可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是 int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处 ...
- 嵌入式C语言自我修养 12:有一种宏,叫可变参数宏
12.1 什么是可变参数宏 在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list.va_start.va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后, ...
- [C++]C,C++中使用可变参数
可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处, ...
- C语言中的可变参数-printf的实现原理
C语言中的可变参数-printf的实现原理 在C/C++中,对函数参数的扫描是从后向前的.C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出 ...
- C语言 宏定义之可变参数
可变参数宏定义 C99编译器标准允许你可以定义可变参数宏(variadic macros),这样你就可以使用拥有可以变化的参数表的宏.可变参数宏就像下面这个样子: #define dbgprint(. ...
- C# 中的可变参数方法(VarArgs)
首先需要明确一点:这里提到的可变参数方法,指的是具有 CallingConventions.VarArgs 调用约定的方法,而不是包含 params 参数的方法.可以通过MethodBase.Call ...
- Java中的可变参数以及foreach语句
Java中的可变参数的定义格式如下: 返回值类型 方法名称(类型 ... 参数名称){} foreach语句的格式如下: for ( 数据类型 变量名称 :数据名称){ ... } public ...
- 关于Retrofit网络请求URL中含有可变参数的处理
开题:在此默认各位看官对Retrofit.以及Okhttp已经有过一定的了解及应用,所以今天我们不谈基础入门的东西,今天我们谈在Retrofit请求接口管理类中URL参数含有动态参数的处理方式.一般我 ...
随机推荐
- python基础(输出、变量、常量、数据类型、流程控制)
输出 print print("Hello World!") # python2 和 python3 的区别 # python2 # coding:utf-8 print 123 ...
- java-多线程安全-锁
一 同步函数 1.1 一般的方法 同步的另一种体现形式:同步函数. 同步函数使用的锁是哪个?经过分析:大概猜的是this,因为函数必须被对象调用. 验证:写一个同步代码块,写一个同步函数,如果同步代码 ...
- Intellij IDEA 创建资源文件夹 source folder
[转载]原文链接:https://blog.csdn.net/u014723529/article/details/78050725 之前用 eclipse 新建有一个 source folder , ...
- 高可用服务 AHAS 在消息队列 MQ 削峰填谷场景下的应用
在消息队列中,当消费者去消费消息的时候,无论是通过 pull 的方式还是 push 的方式,都可能会出现大批量的消息突刺.如果此时要处理所有消息,很可能会导致系统负载过高,影响稳定性.但其实可能后面几 ...
- 客户端用javascript填充Dropdownlist,服务器端获取不到Dropdownlist的值
今天遇到一个奇怪的问题,某一页面需要使用三级级联下拉列表框.为提高用户体验,采用jQuery的cascadingDropDown插件调用后台Web Services来实现ajax填充. 填充没有任何问 ...
- PyCharm使用之配置SSH Interpreter
在文章PyCharm使用之利用Docker镜像搭建Python开发环境中,该文章介绍了在PyCharm中如何利用Docker镜像搭建Python开发环境.在本文中,将会介绍如何使用PyCharm来 ...
- React 按需加载 - 代码分隔
代码分隔 我们现在大多数React项目都是以Webpack 或者 Browserify等将一堆的jsx文件组织一起,并且由一个类似index.js的入口文件串联起来的单页面web页面. 例如: // ...
- 前端与编译原理——用JS写一个JS解释器
说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念.作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于"抽象语法树(AST)".但这仅仅是个开头而已.编 ...
- VisualTreeHelper使用——使用BFS实现高效率的视觉对象搜索
BFS,即广度优先搜索,是一种典型的图论算法.BFS算法与DFS(深度优先搜索)算法相对应,都是寻找图论中寻路的常用算法,两者的实现各有优点. 其中DFS算法常用递归实现,也就是常见的一条路找到黑再找 ...
- ansible Ansible Galaxy ansible-playbook 安装 使用 命令 笔记 生成密钥 管控机 被管控机 wget epel源
笔记 ansible 安装 与salt对比 相同 都是为了同时在多台机器上执行相同的命令 都是python开发 不同 agent(saltstack需要安装.ansible不需要) 配置(salt配置 ...