可变长参数列表误区与陷阱——va_arg不可接受的类型
实现一个有可变长参数列表函数的时候,会使用到stdarg.h(这里不讨论varargs.h)中提供的宏。
例如,我们要实现一个简易的my_printf:
1. 它只返回void, 不记录输出的字符数目
2. 它只接受"%d"按整数输出、"%c"按字符输出、"%%"输出'%'本身
如下:
2
3 void my_printf(const char* fmt, ... )
4 {
5 va_list ap;
6 va_start(ap,fmt); /* 用最后一个具有参数的类型的参数去初始化ap */
7 for (;*fmt;++fmt)
8 {
9 /* 如果不是控制字符 */
if (*fmt!='%')
{
putchar(*fmt); /* 直接输出 */
continue;
}
/* 如果是控制字符,查看下一字符 */
++fmt;
if ('\0'==*fmt) /* 如果是结束符 */
{
); /* 这是一个错误 */
break;
}
switch (*fmt)
{
case '%': /* 连续2个'%'输出1个'%' */
putchar('%');
break;
case 'd': /* 按照int输出 */
{
/* 下一个参数是int,取出 */
int i = va_arg(ap,int);
printf("%d",i);
}
break;
case 'c': /* 按照字符输出 */
{
/** 但是,下一个参数是char吗*/
/* 可以这样取出吗? */
char c = va_arg(ap,char);
printf("%c",c);
}
break;
}
}
va_end(ap); /* 释放ap——
必须! 见相关链接*/
}
这与《C++程序设计语言》中的一道练习题很类似。
——需要支持"%c"控制符
在《C++程序设计语言-题解》中,给出了一个答案(中文p65页)。
但是, 如同上面的代码一样,它们都是错误的!
简单的说,我们用va_arg(ap,type)取出一个参数的时候,
type绝对不能为以下类型:
——char、signed
char、unsigned
char
——short、unsigned
short
——signed short、short
int、signed
short int、unsigned
short
int
——float
一个简单的理由是:
——调用者绝对不会向my_printf传递以上类型的实际参数。
在C语言中,调用一个不带原型声明的函数时:
调用者会对每个参数执行“默认实际参数提升(default argument promotions)”。
同时,对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int
然后,调用者将提升后的参数传递给被调用者。
所以,my_printf是绝对无法接收到上述类型的实际参数的。
上面的代码的38与39行,应该改为:
printf("%c",c);
同理, 如果需要使用short和float, 也应该这样:
float f = (float)va_arg(ap,double);
这也是printf族函数没有用于short和float的控制符的原因。
附录:
在《C语言程序设计》对可变长参数列表的相关章节中,并没有提到这个陷阱。
但是有提到默认实际参数提升的规则:
在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。
——《C语言程序设计》第2版 2.7 类型转换 p36
在其他一些书籍中,也有提到这个规则:
事情很清楚,如果一个参数没有声明,编译器就没有信息去对它执行标准的类型检查和转换。
在这种情况下,一个char或short将作为int传递,float将作为double传递。
这些做未必是程序员所期望的。
脚注:这些都是由C语言继承来的标准提升。
对于由省略号表示的参数,其实际参数在传递之前总执行这些提升(如果它们属于需要提升的类型),将提升后的值传递给有关的函数。——译者注
——《C++程序设计语言》第3版-特别版 7.6 p138
…… float类型的参数会自动转换为double类型,short或char类型的参数会自动转换为int类型 ……
——《C陷阱与缺陷》 4.4 形参、实参与返回值 p73
这里有一个陷阱需要避免:
va_arg宏的第2个参数不能被指定为char、short或者float类型。
因为char和short类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……
例如,这样写肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
c = va_arg(ap,int);
——《C陷阱与缺陷》p164
2009/05/07 修改:
printf函数族有用于short的控制符“h”。
见:http://www.cplusplus.com/reference/clibrary/cstdio/printf/
相关链接:
——《可变长参数列表误区与陷阱——va_end是必须的吗?》
http://www.cppblog.com/ownwaterloo/archive/2009/04/21/is_va_end_necessary.html

本作品采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可。
转载请注明 :
文章作者 - OwnWaterloo
发表时间 - 2009年04月21日
原文链接 - http://www.cppblog.com/ownwaterloo/archive/2009/04/21/unacceptable_type_in_va_arg.html
可变长参数列表误区与陷阱——va_arg不可接受的类型的更多相关文章
- 在Python中使用可变长参数列表
函数定义 在函数定义中使用*args和**kwargs传递可变长参数. *args用作传递非命名键值可变长参数列表(位置参数); **kwargs用作传递键值可变长参数列表 函数调用 在调用函数时,使 ...
- Java 可变长参数列表
Java中定义了变长参数,允许在调用方法时传入不定长度的参数. 定义及调用 在定义方法时,在最后一个形参后加上三点 …,就表示该形参可以接受多个参数值,多个参数值被当成数组传入.上述定义有几个要点需要 ...
- go实例—函数或方法的可变长参数
支持可变长参数列表的函数可以支持任意个传入参数,比如fmt.Println函数就是一个支持可变长参数列表的函数. 需要注意的是,可变长参数应该是函数定义的最右边的参数,即最后一个参数 package ...
- python基础语法5 函数定义,可变长参数
函数 1.什么是函数 函数就是一种工具. 可以重复调用 2.为什么要用函数 1.防止代码冗(rong)余 2.代码的可读性差 3.怎么用函数 1.定义函数-->制造工具 2.调用函数--> ...
- Java中可变长参数的使用及注意事项
在Java5 中提供了变长参数(varargs),也就是在方法定义中可以使用个数不确定的参数,对于同一方法可以使用不同个数的参数调用,例如print("hello");print( ...
- Java中可变长参数的方法
原文转自:http://www.cnblogs.com/lanxuezaipiao/p/3190673.html 在Java5 中提供了变长参数(varargs),也就是在方法定义中可以使用个数不确定 ...
- [转]深度探索C语言函数可变长参数
转自:http://www.cnblogs.com/chinazhangjie/archive/2012/08/18/2645475.html 一.基础部分 1.1 什么是可变长参数 可变长参数:顾名 ...
- 关于C中可变长参数
前言 可变长参数指函数的参数个数在调用时才能确定的函数参数.基本上各种语言都支持可变长参数,在特定情形下,可变长参数使用起来非常方便.c语言中函数可变长参数使用“...”来表示,同时可变长参数只能位于 ...
- C++ 系列:函数可变长参数
一.基础部分 1.1 什么是可变长参数 可变长参数:顾名思义,就是函数的参数长度(数量)是可变的.比如 C 语言的 printf 系列的(格式化输入输出等)函数,都是参数可变的.下面是 printf ...
随机推荐
- This project needs to migrate WTP metadata
in command-line: path> mvn eclipse:clean path> mvn -Dwtpversion=1.5 eclipse:eclipse path> m ...
- UVa 1593 (水题 STL) Alignment of Code
话说STL的I/O流用的还真不多,就着这道题熟练一下. 用了两个新函数: cout << std::setw(width[j]); 这个是设置输出宽度的,但是默认是在右侧补充空格 所 ...
- 总结css兼容问题
目前主流浏览器的兼容性做的都比较好了,本文主要针对IE6,7的不兼容问题进行解决. 1.有浮动存在时,计算一定要精确,不要让内容的宽高超出我们所设置的宽高,IE6下,内容会撑开设置好的高度. 解决方法 ...
- Zookeeper工作原理
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等.Zookeeper是hadoop的一个子项目,其 ...
- mysql日期函数(转)
MySQL 获得当前日期时间 函数 获得当前日期+时间(date + time)函数:now() mysql> select now(); +---------------------+ | n ...
- BZOJ 1406 密码箱
直接两层枚举就行了. 避免排序可以用set. #include<iostream> #include<cstdio> #include<cstring> #incl ...
- Android程序架构基本内容概述
在Android操作系统中开发的应用程序都有一个结构缜密的架构.我们今天就来对这一Android程序架构做一个详细的分析.帮助大家了解程序开发的特点,以方便将来在应用程序开中明确自己的程序架构. An ...
- [转载] FFmpeg API 变更记录
最近一两年内FFmpeg项目发展的速度很快,本来是一件好事.但是随之而来的问题就是其API(接口函数)一直在发生变动.这么一来基于旧一点版本的FFmpeg的程序的代码在最新的类库上可能就跑不通了. 例 ...
- Datawindow.net+access数据窗口制作方法
1) 数据字典:采用SQLServer桌面程序来创建数据字典.配置正确的pbl文件生成输入列表. 在SQLServers查询器中执行select * from pbcatedt where pbe_n ...
- ipad iphone 开发的应用,加一个启动预览图片
如果你想在你的ipad,iphone应用启动是不显示黑屏幕,而是用一个图片代替的话,你只需要吧一张名为 Default.png 的图片加到项目里就行了. 很简单吧,新手们记得去这样美化下你的应用吗,很 ...