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的Django REST框架中的序列化及请求和返回
Python的Django REST框架中的序列化及请求和返回 序列化Serialization 1. 设置一个新的环境 在我们开始之前, 我们首先使用virtualenv要创建一个新的虚拟环境,以使 ...
- day36 03-Hibernate检索方式:排序、参数绑定、投影查询
排序之后是分页查询. 检索单个对象 还可以进行参数的绑定. HQL的参数绑定,按参数名称绑定或者是按参数位置绑定. 还可以用投影的操作,投影的操作是只查询这里面的某几个属性.只查询某一个属性,查询多个 ...
- NOIP模拟赛 17.10.10
初次见面(firstmeet)[题目背景]雾之湖边,静得可怕.露米娅出神凝望.黑白连衣裙,像极了绽放的墨黑和洁白的莲.身边的雾之湖,倒映着血色天空.酒红的双眸,映照一切.低声浅笑,双臂伸直,她悄无声息 ...
- Leetcode17.Letter Combinations of a Phone Number电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合. 给出数字到字母的映射如下(与电话按键相同).注意 1 不对应任何字母. 示例: 输入:"23" 输出:[&quo ...
- UE4物理模块(三)---碰撞查询(上)
在前一文中介绍了如何在UE4中创建简单碰撞或者直接使用其mesh表示的复杂碰撞: Jerry:UE4物理模块(二)---建立物体碰撞zhuanlan.zhihu.com 那么在拿到碰撞之后,就可以进 ...
- FTP权限问题解析,553 Can't open that file: Permission denied
FTP上传文件,提示553 Can't open that file: Permission denied 原因: 目录的所属组,所属用户属于root, 导致FTP无法上传, 修改组和所属用户为www ...
- arcgis访问百度地图
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- <第一周>降维
PCA 矩阵的主成分就是其协方差矩阵对应的特征向量,按照对应的特征值大小进行排序,最大的特征值为第一主成分,以此类推 主要过程 对所有样本进行中心化 计算样本的协方差矩阵 XX.T 对协方差矩阵做特征 ...
- JSP Web第七章整理复习 Servlet基础知识
P206-208 Servlet项目的创建,web.xml的配置及标签含义,相关程序 创建:new 一个Servlet类,继承自javax.servlet.http.HttpServlet; 写doG ...
- 阿里云ecs环境配置
在阿里云 CentOS 服务器(ECS)上搭建 nginx + mysql + php-fpm 环境 https://ninghao.net/blog/1368 阿里云ecs从购买到环境搭建和建站!! ...