详解-C语言可变参数-va-list和-vsnprintf及printf实现
- 格式:doc
- 大小:41.00 KB
- 文档页数:8
格式化输出函数:printf,f...总览 (SYNOPSIS)#include <stdio.h>int printf(const char *format, ...);int fprintf(FILE *stream, const char *format, ...);int sprintf(char *str, const char *format, ...);int snprintf(char *str, size_t size, const char *format, ...);#include <stdarg.h>int vprintf(const char *format, va_list ap);int vfprintf(FILE *stream, const char *format, va_list ap);int vsprintf(char *str, const char *format, va_list ap);int vsnprintf(char *str, size_t size, const char *format,va_list ap);描述 (DESCRIPTION)printf 系列函数根据下述的 format 参数生成输出内容. printf 和 vprintf 函数把输出内容写到 stdout, 即标准输出流; fprintf 和 vfprintf 函数把输出内容写到给定的 stream 流(字符流设备); sprintf, snprintf, vsprintf 和 vsnprintf 函数把输出内容存放到字符串 str 中.这些函数由格式字符串 format 参数控制输出内容, 它指出怎么样把后面的参数 (或通过 stdarg 的变长参数机制访问的参数) 转换成输出内容.这些函数返回打印的字符数量 (不包括字符串结尾用的`/0'). snprintf 和 vsnprintf 的输出不会超过 size 字节 (包括了结尾的 `/0'), 如果因为这个限制导致输出内容被截断, 则函数返回 -1.格式字符串 (format 参数) 由零到多个指令组成: 普通字符(除 % 外), 它们被原封不动的送到输出流; 以及格式转换说明(conversion specification), 每个格式转换说明都会从后面提取零到多个参数. 格式转换说明由 % 字符引导开始. 参数必须正确的对应到格式转换符 (conversion specifier) 上. 下述字符按顺序列在 % 后面:*零个或多个下列标志:#指出数值应该转换成 "其他形式". 对于 c, d, i, n, p, s, 和 u 格式转换, 这个选项没有影响. 对于 o 格式转换, 数值的精度被提高, 使输出字符串的第一个字符为零 (除非打印一个零值时, 明确定义精度为零). 对于 x 和 X 格式转换, 非零数值前面会添加 `0x' 字符串(或 X 格式转换的 `0X' 字符串). 对于 e, E, f, g, 和 G 格式转换, 其结果始终含有一个十进制小数点, 即使后面没有数字 (一般说来, 只有当格式转换后, 小数点后面有数字时才显示小数点). 对于 g 和 G 格式转换, 将不删去结果末尾的零, 其他情况下这些零应该删掉. 0指出用零填充结果. 所有的格式转换, 除了 n, 转换结果的左边用零填充, 而不是空格. 如果数值转换时给定了精度, (d, i, o, u, i, x, 和 X), 则忽略 0 标志.-(负位宽标志) 指出转换结果必须在位边界上向左边对齐. 除了 n 格式转换, 转换结果的右边用空格填充, 而不是在左边填充空格或零. 如果同时给出了 - 和 0 , 则 - 覆盖 0 .' '(空格) 指出在通过有符号数(signed) 格式转换 ( d, e, E, f, g, G, 或 i ) 产生的正数前面留一个空格.+指出有符号数格式转换产生的结果前面始终有一个正负符号. 如果同时给出了 + 和空格, 则 + 覆盖空格.'指出在数字参数中, 如果 locale 给出相关信息, 输出结果将被分组. 注意, 许多版本的 gcc不能理解这个选项, 因而会产生一个警告.*一个可选的十进制数, 指出最小的位宽. 如果格式转换后产生的字符数少于位宽, 则左边用空格填充 (或者填充右边, 如果给出了向左对齐标志), 直到填满指定的位宽.*一个可选的精度, 格式是一个句号(`.') 后面跟着一个可选的数字. 如果没有给出这个数字, 则精度取为零. 这样就指定了 d, i, o, u, x, 和 X 格式转换显示的最小位数, e, E, 和 f 格式转换小数点后面显示的位数, g 和 G 格式转换显示的最大有效位数(significant digits), 或 s 格式转换打印某个字符串的最多字符数目.*可选的字符 h, 指出后面的 d, i, o, u, x, 或 X 格式转换对应为 short int 或 unsigned short int 的参数, 或者是后面的 n 格式转换对应为指向 short int 参数的指针.*可选的字符 l (ll) 指出后面的 d, i, o, u, x, 或 X 格式转换应用到指向 long int 或 unsigned long int 参数的指针, 或者后面的 n 格式转换对应为指向 long int 参数的指针. Linux 提供和 ANSI 不兼容的双 l 标志, 作为 q 或 L 的同义词. 因此 ll 可以结合浮点格式转换使用. 但是强烈反对这个用法.*字符 L 指出后面的 e, E, f, g, 或 G 格式转换对应 long double 参数, 或者让后面的 d, i, o, u, x, 或 X 格式转换对应 long long 参数. 注意 long long 没有在 ANSI C 中声明, 因此不能够移植到所有的体系平台上.*可选的字符 q 等于 L. 参考 STANDARDS 和 BUGS 节关于 ll, L,和 q 的叙述.*字符 Z 指出后面的整数 (d, i, o, u, x, 或 X) 格式转换对应 size_t 参数.*指出采用格式转换类型的字符.可以用星号 `*' 代替数字指定域宽或精度, 也可以两者同时指定. 这种情况下要求用一个 int 参数指出域宽或精度. 负域宽被认为是正域宽跟在向左对齐标志后面; 负精度被认为是精度丢失.格式转换符(specifier) 及其含义如下:diouxX将 int 形 (或合适的变量) 参数转换输出为有符号十进制数 (d 和 i), 无符号八进制数 (o), 无符号十进制数(u), 或者无符号十六进制数 (x 和 X). x 格式转换用小写字母 abcdef ; X 格式转换用大写字母 ABCDEF .精度值 (如果给出) 指出必须显示的最少数字; 如果转换结果少于这个要求, 则用零填补转换结果的左边.eE将 double 参数舍入后转换为 [-]d.ddde/*(Pmdd 的格式, 这个格式的小数点前面有一位数字, 后面表示精度; 如果没有指出精度, 则意味着精度是 6; 如果精度是 0, 则不显示小数点. E 格式转换使用字母 E (而不是 e) 要求引入指数. 指数至少包含两个数字; 如果值是零, 则指数是 00.f将 double 参数舍入后转换为 [-]ddd.ddd 的十进制表达式, 这个格式小数点后面的数字表示精度. 如果没有指出精度, 则意味着精度是 6; 如果显式给出精度是 0, 则不显示小数点. 如果显示了小数点, 则小数点前面至少有一位数字.g将 double 参数以 f 或 e (或者 G 格式转换的 E 标志) 的形式转换. 其精度指出有符号数字的数目. 如果没有指出精度, 则默认为 6; 如果精度是零, 则按 1 处理. 如果格式转换后其指数小于 -4 或者大于等于其精度, 则使用 e 形式. 转换结果消除了分数部分末尾的零; 小数点前面至少有一位十进制数字.c将 int 参数转换为 unsigned char, 然后输出对应的字符.s认为 ``char *'' 参数是指向字符形数组的指针 (指向字符串). Printf 输出数组内的字符, 直到遇上 (但不包括) 结束字符 NUL ; 如果给出了精度值, printf 不会输出多于这个值的字符, 也不需要提供 NUL 结束符; 如果没有给出精度值, 或精度值大于数组长度, 则数组内一定要包括一个 NUL 字符.p将以十六进制数打印 ``void *'' 指针参数 (就象是 %#x 或 %#lx). n将目前已经输出的字符数目存储在 ``int *'' (或变量) 指针参数指向的地址. 不转换任何参数.%输出一个 '%'. 不转换任何参数. 完整的写法是 `%%'.不指定域宽或偏小的域宽不会导致内容被截断; 如果转换结果的长度超过其域宽, 则域宽会扩大到容下完整的结果.示例 (EXAMPLES)以 `Sunday, July 3, 10:02' 格式显示日期, 其中 weekday 和 month 是字符串指针:#include <stdio.h>fprintf(stdout, "%s, %s %d, %.2d:%.2d/n", weekday, month, day, hour, min);显示五位十进制数:#include <math.h>#include <stdio.h>fprintf(stdout, "pi = %.5f/n", 4 * atan(1.0)); //atan( ) 函数返回数值表达式的反正切弧度值。
vsprintf⽤法vsprintf()函数中的⾃变量是位于数组中的,数组元素的字符串之前都要加上百分号(%)。
这个函数是“⼀步⼀步[step-by-step]”按顺序执⾏。
在第⼀个%后,将插⼊第⼀个数组元素;在第⼆个%后,将插⼊第⼆个数组元素,依次类推。
vsprintf是sprintf的⼀个变形,它只有三个参数。
vsprintf⽤於执⾏有多个参数的⾃订函式,类似printf格式。
vsprintf的前两个参数与sprintf相同:⼀个⽤於保存结果的字元缓冲区和⼀个格式字串。
第三个参数是指向格式化参数阵列的指标。
实际上,该指标指向在堆叠中供函式呼叫的变数。
va_list、va_start和va_end巨集(在STDARG.H中定义)帮助我们处理堆叠指标。
本章最後的SCRNSIZE程式展⽰了使⽤这些巨集的⽅法。
使⽤vsprintf函式,sprintf函式可以这样编写:int sprintf (char * szBuffer, const char * szFormat, ...){ int iReturn ; va_list pArgs ; va_start (pArgs, szFormat) ; iReturn = vsprintf (szBuffer, szFormat, pArgs) ; va_end (pArgs) ; return iReturn ;}va_start巨集将pArg设置为指向⼀个堆叠变数,该变数位址在堆叠参数szFormat的上⾯。
函数名: vsprintf功能: 送格式化输出到串中⽤法: int vsprintf(char *string, char *format, va_list param);程序例:#include <stdio.h>#include <conio.h>#include <stdarg.h>char buffer[80];int vspf(char *fmt, ...){va_list argptr;int cnt;va_start(argptr, fmt);cnt = vsprintf(buffer, fmt, argptr);va_end(argptr);return(cnt);}int main(void){int inumber = 30;float fnumber = 90.0;char string[4] = "abc";vspf("%d %f %s", inumber, fnumber, string);printf("%s\n", buffer);return 0;}va_list ap;int len;va_start(ap, format);vsprintf(_this->printfBuf, format, ap)va_end(ap);请问vsprintf的作⽤⼲什么啊?根据上⾯代码能详细介绍下吗?谢谢!===============================把参数 ap 按照 format 指定的格式,写到 _this->printfBuf 中基本和 sprinf 类似 ......⽐如上⾯给的例⼦:vsprintf(buffer, fmt, argptr);fmt="%d %f %s"就是把后⾯的参数按照 "%d %f %s" 这个格式输出到 buffer 中。
_vsnprintf⽤法_vsnprintf,C语⾔库函数之⼀,属于可变参数。
⽤于向字符串中打印数据、数据格式⽤户⾃定义。
头⽂件:#include <stdarg.h>函数声明:int _vsnprintf(char* str, size_t size, const char* format, va_list ap);参数说明:char *str [out],把⽣成的格式化的字符串存放在这⾥.size_t size [in], str可接受的最⼤字节数,防⽌产⽣数组越界.const char *format [in], 指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序。
va_list ap [in], va_list变量. va:variable-argument:可变参数函数功能:将可变参数格式化输出到⼀个字符数组。
⽤法类似于vsprintf,不过加了size的限制,防⽌了内存溢出(size为str所指的存储空间的⼤⼩)。
返回值:执⾏成功,返回写⼊到字符数组str中的字符个数(不包含终⽌符),最⼤不超过size;执⾏失败,返回负值,并置errno.[1]备注:linux环境下是:vsnprintfVC6环境下是:_vsnprintf#include <stdio.h>#include <stdarg.h>int mon_log(char* format, ...){char str_tmp[50];int i=0;va_list vArgList; //定义⼀个va_list型的变量,这个变量是指向参数的指针.va_start (vArgList, format); //⽤va_start宏初始化变量,这个宏的第⼆个参数是第⼀个可变参数的前⼀个参 //数,是⼀个固定的参数.i=_vsnprintf(str_tmp, 50, format, vArgList); //注意,不要漏掉前⾯的_va_end(vArgList); //⽤va_end宏结束可变参数的获取return i; //返回参数的字符个数中间有逗号间隔}//调⽤上⾯的函数void main() { int i=mon_log("%s,%d,%d,%d","asd",2,3,4); printf("%d\n",i); }输出9。
vsnprintf函数详解1. 定义和用途vsnprintf函数是C语言标准库中的一个函数,其原型如下:int vsnprintf(char *str, size_t size, const char *format, va_list ap);vsnprintf函数用于将可变参数列表(va_list)根据格式化字符串(format)进行格式化,并将结果输出到一个字符数组(str)中,最多输出size-1个字符。
2. 参数解释•str:指向一个字符数组的指针,用于存储格式化后的结果。
•size:字符数组的大小,即可以存储的最大字符数。
注意,这里不包括结尾的空字符’\0’。
•format:格式化字符串,用于控制如何对可变参数进行格式化输出。
•ap:va_list类型的可变参数列表。
3. 工作方式vsnprintf函数根据format字符串中的格式说明符来确定如何处理可变参数列表,并将结果写入str指向的字符数组中。
3.1 格式说明符在format字符串中,以百分号(%)开头的部分被视为格式说明符。
常见的格式说明符有: - %d 或 %i:按照有符号十进制整数形式输出。
- %u:按照无符号十进制整数形式输出。
- %x 或 %X:按照无符号十六进制整数形式输出。
- %f 或 %F:按照浮点数形式输出。
- %s:按照字符串形式输出。
- %c:按照字符形式输出。
除了以上常见的格式说明符外,还有很多其他类型的格式说明符,可以用于更复杂的格式化需求。
3.2 可变参数列表vsnprintf函数使用va_list类型的可变参数列表来传递额外的参数。
可变参数列表是一种特殊的数据类型,用于表示不定数量和类型的参数。
在调用vsnprintf函数之前,必须通过宏va_start初始化可变参数列表,指定可变参数列表中第一个可选参数的名称。
在使用完可变参数列表后,需要通过宏va_end结束可变参数列表。
3.3 格式化结果输出vsnprintf函数将根据format字符串中的格式说明符和相应的可变参数对其进行格式化,并将结果写入str指向的字符数组中。
snprintf 替换函数
如果需要替换snprintf函数,可以使用标准库中的snprintf函数的可替代函数vsnprintf。
这是一个可变参数函数,可以接受一个va_list参数,因此可以模拟snprintf的行为。
以下是一个简单的示例:
```cpp
#include <stdarg.h>
#include <stdio.h>
int my_snprintf(char* str, size_t size, const char* format, ...) {
va_list args;
va_start(args, format);
int result = vsnprintf(str, size, format, args);
va_end(args);
return result;
}
```
在上述示例中,我们定义了一个名为my_snprintf的函数,它接受与snprintf相同的参数。
在函数体中,我们使用vsnprintf函数来处理实际的格式化操作,并返回结果。
您可以将my_snprintf函数与您的代码一起使用,替换原有的snprintf函数。
请注意,这只是一个简单的示例,您可能需要根据自己的需求和平台特性进行相应的修改和调整。
C++11标准库中cstdio头⽂件新增的5个格式化IO函数学习刚开始学⽹络编程,稍微扩展书上的简单C/S程序时,发现以前太忽略标准I/O这⼀块,查官⽹发现C++11新增了⼏个格式化I/O函数。
snprintf 将格式化输出写⼊到有⼤⼩限制的缓存中vfscanf 从流中读取数据到可变参数列表中vscanf 读取格式化数据到可变参数列表中vsnprintf 从可变参数列表中写⼊数据到有⼤⼩限制的缓存中vsscanf 从字符串中读取格式化数据到可变参数列表中主要谈谈snprintf,后⾯4个都是辅助可变参数列表的。
int snprintf ( char * s, size_t n, const char * format, ... );百度⼀下会发现⼀些过时的⽂章写到VC上是_snprintf⽽gcc上是snprintf,VC的_snprintf不会在复制完的字符串后⾯补上⼀个'\0'。
这是很多C新⼿会在Win平台会出现烫烫烫的原因,因为输出字符数组时没有遇到'\0'结尾,所以会⼀直输出,甚⾄是未初始化的内存,默认为0xcccccccc,变成字符串就是“烫烫烫”。
具体参考⽂章但是新标准已经将snprintf标准化了函数签名如下,所以也不⽤担⼼那个问题。
snprintf和sprintf功能基本⼀致,但是更安全,参数多了⼀个size_t n,代表写⼊缓存的数据⼤⼩。
⽐如char buf[10]; 如果n超过10,就会在编译期提醒错误,所以在VS中写C++,如果使⽤fopen等函数会编译不通过,提⽰你使⽤fopen_s(类似的还有其他_s后缀的函数)等安全(safe)函数(会在编译期检测错误)的原因。
题外话,解决⽅案,当然更简单的做法就是每次新建C++项⽬时把默认SDL Check⼀栏的勾勾去掉。
回正题,也就是说,除了跟sprintf⼀样,到格式化字符串结尾会停⽌写⼊缓存外,当写⼊字符数量到达n时也会停⽌写⼊缓存,防⽌越界。
c语言可变参数用法C语言可变参数用法C语言作为一种高度灵活和强大的编程语言,为程序员提供了丰富的编程工具和特性。
其中之一就是可变参数,它允许函数接受不定数量的参数。
在本文中,我们将深入探讨C语言可变参数的用法和实现。
一、什么是可变参数首先,我们需要了解可变参数的概念。
在C语言中,可变参数是指一个函数接受不定数量的参数。
这意味着我们可以使用不同数量的参数来调用函数,而函数内部可以根据需要处理这些参数。
这为我们处理各种情况和需求带来了极大的灵活性。
二、可变参数的声明在C语言中,使用可变参数之前,我们需要在函数声明中使用`...`来表示参数的可变性。
例如,下面是一个使用可变参数的函数声明:cint sum(int count, ...);在这个例子中,`...`表明该函数将接受可变数量的参数,而`count`参数指定了传递给函数的参数数量。
三、可变参数的使用接下来,我们将了解如何在函数内部使用可变参数。
在C语言中,我们可以使用`stdarg.h`头文件中的一些宏来处理可变参数。
让我们逐一了解这些宏。
1. `va_list`类型`va_list`类型用于定义一个变量来保存可变参数列表。
我们可以通过`va_start`宏初始化这个变量。
cvoid func(int count, ...) {va_list args;va_start(args, count);...在上面的例子中,我们使用了`va_list`类型的变量`args`来保存可变参数。
2. `va_start`宏`va_start`宏用于初始化`va_list`类型的变量。
它接受两个参数,第一个参数是保存可变参数的`va_list`类型变量,第二个参数是最后一个固定参数的标识符。
cvoid func(int count, ...) {va_list args;va_start(args, count);...在上面的例子中,我们使用了`va_start(args, count)`来初始化`args`变量,并指定`count`作为最后一个固定参数的标识符。
vsnprintf函数vsnprintf函数是C语言中非常常用的一个函数,它是对标准库函数snprintf的一个扩展。
vsnprintf函数的作用是将可变参数列表按照指定的格式输出到一个字符数组中,同时确保不会发生缓冲区溢出的情况。
在实际编程中,vsnprintf函数可以帮助程序员更加灵活地处理字符串输出,同时提高程序的安全性。
在使用vsnprintf函数时,首先需要了解它的基本语法和用法。
vsnprintf函数通常的形式是:```cint vsnprintf(char *str, size_t size, const char *format, va_list ap);```其中,str是指向输出字符串缓冲区的指针;size是输出字符串的最大长度;format是格式化字符串,用来指定输出的格式;ap是一个指向参数列表的指针。
调用vsnprintf函数后,它会将格式化后的字符串输出到str指向的缓冲区中,最多输出size-1个字符,并在最后自动添加一个空字符'\0',以确保输出的字符串是以null结尾的。
使用vsnprintf函数的一个常见场景是在日志输出中。
我们经常需要将程序执行过程中的一些信息输出到日志文件中,而且需要保证日志信息的格式清晰,并且不会因为数据过长导致缓冲区溢出。
这时候,vsnprintf函数就派上了用场。
通过vsnprintf函数,我们可以将需要输出的信息按照指定的格式写入到一个字符数组中,然后再将这个字符数组写入到日志文件中,从而实现安全可靠的日志输出。
vsnprintf函数还可以用于实现动态生成格式化字符串的功能。
有时候我们需要根据程序运行时的一些变量的值来动态生成输出的格式化字符串,这时候就可以使用vsnprintf函数。
通过将格式化字符串和参数列表分离开来,可以更加灵活地控制输出的格式,同时避免了在程序中硬编码过多的字符串,提高了代码的可读性和可维护性。
printf函数的用法c语言printf函数是C语言中的输出函数,用于将数据打印到控制台或者指定的输出流中。
printf函数的基本用法是:```c#include <stdio.h>int main() {printf("Hello, World!\n");return 0;}```上述代码会将字符串"Hello, World!"打印到控制台,并在最后添加一个换行符。
printf函数的格式化字符串可以包含转义序列和格式化参数。
转义序列以反斜杠(\)开头,用于表示一些特殊的控制字符。
一些常见的转义序列包括:- \n:换行符- \t:制表符- \\":双引号- \:反斜杠格式化参数用于将变量的值插入到格式化字符串中,以指定的格式进行输出。
常见的格式化参数有:- %d:输出整数- %f:输出浮点数- %c:输出字符- %s:输出字符串例如:```c#include <stdio.h>int main() {int age = 20;float height = 1.75;char name[] = "Alice";printf("My name is %s, I'm %d years old, and my height is %.2f.\n", name, age, height);return 0;}```上述代码会将变量的值插入到格式化字符串中,并以指定的格式进行输出。
输出结果为:```My name is Alice, I'm 20 years old, and my height is 1.75.```。
C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是如何实现的呢这里我们一起来挖掘一下C语言变长参数的奥秘。
先考虑这样一个问题:如果我们不使用C标准库(libc)中提供的Facilities,我们自己是否可以实现拥有变长参数的函数呢我们不妨试试。
一步一步进入正题,我们先看看固定参数列表函数,void fixed_args_func(int a, double b, char *c){printf("a = 0x%p\n", &a);printf("b = 0x%p\n", &b);printf("c = 0x%p\n", &c);}对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a我们可以得到a的地址,并通过函数原型声明了解到a是int 类型的; 通过&b我们可以得到b的地址,并通过函数原型声明了解到b是double类型的; 通过&c我们可以得到c的地址,并通过函数原型声明了解到c是char*类型的。
但是对于变长参数的函数,我们就没有这么顺利了。
还好,按照C标准的说明,支持变长参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统C有区别,传统C允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是依然无法从声明中得到其他变长参数的地址,比如:void var_args_func(const char * fmt, ... ){... ...}这里我们只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定"..."中有几个参数、参数都是什么类型的,自然也就无法确定其位置了。
那么如何可以做到呢在大脑中回想一下函数传参的过程,无论"..."中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。
这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置,顺着这个思路,我们继续往下走,通过一个例子来诠释一下:(这里要说明的是:函数参数进栈以及参数空间地址分配都是"实现相关"的,不同平台、不同编译器都可能不同,所以下面的例子仅在IA-32,Windows XP, MinGW gcc v3.4.2下成立)我们先用上面的那个fixed_args_func函数确定一下这个平台下的入栈顺序。
int main(){fixed_args_func(17, , "hello world");return 0;}a = 0x0022FF50b = 0x0022FF54c = 0x0022FF5C从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。
我们基本可以得出这样一个结论:= + x_sizeof(b); /*注意: x_sizeof != sizeof,后话再说 */= + x_sizeof(a);有了以上的"等式",我们似乎可以推导出 void var_args_func(const char * fmt, ... ) 函数中,可变参数的位置了。
起码第一个可变参数的位置应该是: = + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数:void var_args_func(const char * fmt, ... ){char *ap;ap = ((char*)&fmt) + sizeof(fmt);printf("%d\n", *(int*)ap);ap = ap + sizeof(int);printf("%d\n", *(int*)ap);ap = ap + sizeof(int);printf("%s\n", *((char**)ap));}int main(){var_args_func("%d %d %s\n", 4, 5, "hello world");}输出结果:45hello worldvar_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了,如果你把这个程序拿到solaris 9下,运行后,一定得不到正确的结果,为什么呢,后续再说。
先来解释一下这个程序。
我们用ap获取第一个变参的地址,我们知道第一个变参是4,一个int型,所以我们用(int*)ap以告诉编译器,以ap为首地址的那块内存我们要将之视为一个整型来使用,*(int*)ap获得该参数的值;接下来的变参是5,又一个int型,其地址是ap + sizeof(第一个变参),也就是ap + sizeof(int),同样我们使用*(int*)ap获得该参数的值;最后的一个参数是一个字符串,也就是char*,与前两个int型参数不同的是,经过ap + sizeof(int)后,ap指向栈上一个char*类型的内存块(我们暂且称之tmp_ptr, char *tmp_ptr)的首地址,即ap -> &tmp_ptr,而我们要输出的不是printf("%s\n", ap),而是printf("%s\n", tmp_ptr); printf("%s\n", ap)是意图将ap所指的内存块作为字符串输出了,但是ap -> &tmp_ptr,tmp_ptr所占据的4个字节显然不是字符串,而是一个地址。
如何让&tmp_ptr是char **类型的,我们将ap进行强制转换(char**)ap <=> &tmp_ptr,这样我们访问tmp_ptr只需要在(char**)ap前面加上一个*即可,即printf("%s\n", *(char**)ap);前面说过,如果将var_args_func放到solaris上,一定是得不到正确结果的为什么呢由于内存对齐。
编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。
上述例子中,我是根据反编译后的汇编码得到的参数间隔,还好都是4,然后在代码中写死了。
为了满足代码的可移植性,C标准库在中提供了诸多Facilities以供实现变长长度参数时使用。
这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:#include <>void std_vararg_func(const char *fmt, ... ) {va_list ap;va_start(ap, fmt);printf("%d\n", va_arg(ap, int));printf("%f\n", va_arg(ap, double));printf("%s\n", va_arg(ap, char*));va_end(ap);}int main() {std_vararg_func("%d %f %s\n", 4, , "hello world");}输出:4hello world对比一下 std_vararg_func和var_args_func的实现,va_list似乎就是char*, va_start 似乎就是 ((char*)&fmt) + sizeof(fmt),va_arg似乎就是得到下一个参数的首地址。
没错,多数平台下中va_list, va_start和var_arg的实现就是类似这样的。
一般会包含很多宏,看起来比较复杂。
在有的系统中的实现依赖some special functions built into the the compilation system to handle variable argument lists and stack allocations,多数其他系统的实现与下面很相似:(Visual C++ 的实现较为清晰,因为windows上的应用程序只需要在windows平台间做移植即可,没有必要考虑太多的平台情况)。
C语言va_list与_vsnprintf的使用先举一个例子:#define bufsize 80char buffer[bufsize];/* 这个函数用来格式化带参数的字符串*/int vspf(char *fmt, ...){va_list argptr;写可变参数的C函数要在程序中用到以下这些宏:使用可变参数应该有以下步骤:1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:可变参数在编译器中的处理我们知道va_start,va_arg,va_end是在中被定义成宏的,由于:1)硬件平台的不同2)编译器的不同Microsoft Visual Studio\VC98\Include\中,typedef char * va_list;/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。
而在有的机器上va_list是被定义成void*的*/#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) /*_INTSIZEOF (n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。