[C++系列] Printf系列函数的玄机

<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->

Printf 函数是一组函数的总称,包含:

       #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);

注:这组函数使用可变函数列表作为参数。

还包括间接使用 printf 的函数,如 wxWidget 中的 wxLogXXX 等。

 

Printf 函数的 format 参数 使用 % 作为特殊字符用以定义所要打印的串的特殊格式; printf 函数可以定义以下打印格式:

1) 标识字符 (flag characters): #, 0, -, ‘ ‘, +

       0 : 表示在有位数要求时,使用 0 作为填充字符,而不是空白字符 ( 默认 )

    - : 表示在有对齐要求时,使用左对齐,而非右对齐 ( 默认 )

    ‘ ‘ : 强制在正数后空串前留一位空字符。

    + : 显示正负数的数学符号 (+/-)

 

       2) 位数和精度 (field width and precision)

       Printf(“%12.4f”, num);

       Printf(“%12”, num);

 

3) 长度修饰语 (length modifier) : h, hh, l, ll, z, t ( 仅用于整型数的转换 )

其中, t 用来将整型数打印成内存地址。

 

4) 转换分类符 (conversion specifier) : d, i; o, u, x, X; e, E, f, F, g, G; c, s; p;

其中, %p 用于以 16 进制打印 void* 的指针地址;

 

 

问题:如何使用 printf 打印 %?

% printf 函数的 format 参数特殊字符,如果想在 format 中识别 % ,需使用 %% 才行;或者使用 %s 格式打印带有 % 的字符串,因为使用 %s 将打印相应参数中的所有字符。

 

下面是一个综合例子:

#include <cstdio>

#include <stdarg.h>

 

const int MAX_LEN = 1024;

 

int vspf(char *fmt , ...)

{

        char msg[MAX_LEN] = {0};

 

        va_list argptr;

        va_start(argptr, fmt ); //get the address of va, fmt must be the last parameter before va list.

        int cnt = vsnprintf(msg, MAX_LEN, fmt, argptr);

        va_end(argptr);

 

        printf("%s", msg);

        return(cnt);

}

 

int main()

{

        // printf("test % string: %s , %s /n", "test", "abc");  // dangrous

        printf("test %% string: %s , %s /n", "test", "a%%b%%c");

        printf("test string: %s , %s /n", "t%est", "a%b%c");

 

        int width = -10;

        int num = 111;

        double high = 12.34;

         printf("test: %*d %1$d/n", width, num);  // one may write ‘*’ or ‘*m$’ (for some decimal integer m) to print the next argument, or in the m-th argument.

 

        printf("====================/n");

        printf("test: %2$ *1$d/n", width, num);

        printf("test: %012d %+12d/n", width, num);

        printf("test: %12d %12.4f/n", width, high );

 

        printf("test: %-d %-d/n", width, num);

        printf("test: %+d %+d/n", width, num);

 

        vspf("test: %s, %s /n", "abc","xyz");

        vspf("test: %s, %s , %d/n", "a%b%c","xyz", num);

  }

实际输出 (Linux)

test % string: test , a%%b%%c

test string: t%est , a%b%c

test: 111        -10

====================

test:  111

test: -00000000010         +111

test:          -10      12.3400

test: -10 111

test: -10 +111

test: abc, xyz

test: a%b%c, xyz , 111

 

例子中用到了很多 printf 的例子,并使用了 vsnprintf ,由于该函数和 va_list 直接相关,所以下说说 va_list

函数参数的传递原理:函数参数是以数据结构 : 栈的形式存取 (C 语言在函数调用时,先将最后一个参数压入栈 ) 。在 stdcall 下,堆栈中 , 各个参数的分布情况是倒序的 . 即最后一个参数在列表中地址最高部分 , 第一个参数在列表地址的最低部分 . 参数在堆栈中的分布情况如下 :

最后一个参数

倒数第二个参数

...

第一个参数

函数返回地址

函数代码段

 

获取省略号指定的参数:在函数体中声明一个 va_list ,然后用 va_start 函数来获取参数列表中的参数,使用完毕后调用 va_end() 结束。举个例子 (VS)

void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...)

{

va_list args;

va_start(args, pszFormat); // pszFormat 参数下一个参数的地址 , 即第一个可变参数地址 .

_vsnprintf(pszDest, DestLen, pszFormat, args);

va_end(args);

}

原理: va_start 使 argp 指向第一个可选参数。 va_arg 返回参数列表中的当前参数并使 argp 指向参数列表中的下一个参数。 va_end argp 指针清为 NULL 。函数体内可以多次遍历这些参数,但是都必须以 va_start 开始,并以 va_end 结尾。

va_start, va_arg va_end 仅仅是一些宏 用于获取参数堆栈地。

摘自: wchar.h

#ifdef _M_CEE_PURE

typedef System::ArgIterator va_list;

#else

typedef char *  va_list;

#endif

:char 型指针的特点是 ++ -- 操作对其作用的结果是增 1 和减 1 (因为 sizeof(char) 1

 

摘自 stdarg.h

#define va_start _crt_va_start

#define va_arg _crt_va_arg

#define va_end _crt_va_end

 

摘自 *vadefs.h - defines helper macros for stdarg.h

/* A guess at the proper definitions for other platforms */( 不同平台定义不同,指针是依赖平台的 )

#ifdef  __cplusplus

#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )

#else

#define _ADDRESSOF(v)   ( &(v) )

#endif

 

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) //4 的倍数

 

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

#define _crt_va_end(ap)      ( ap = (va_list)0 )

 

注: _INTSIZEOF(n) 宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟 sizeof(int) 对齐。一般的 sizeof(int)= 4 ,也就是参数在内存中的地址都为 4 的倍数。比如,如果 sizeof(n) 1 4 之间,那么 _INTSIZEOF(n) 4 ;如果 sizeof(n) 5 8 之间,那么 _INTSIZEOF(n)=8 。具体细节可以参见:数据在机器内的存储与运算章节。

va_arg(ap,t) 得到第一个参数的值 ( 即调用前 ap 的值 ), 并且将 ap 指针上移一个 _INTSIZEOF(t), 即指向下一个可变参数的地址。这个宏分两步: 1) (ap += _INTSIZEOF(t)) 移动指针地址 ; 2) 使用移动后的 ap 值计算原先的参数值 ( 减掉移动值 _INTSIZEOF(t))

 

如何直接只用可变参数列表:

虽然可以通过在堆栈中遍历参数列表来读出所有的可变参数 , 但是想直接使用可变参数列表,还需要知道可变参数的个数 , 以便结束遍历。解决办法 : a. 参数指定 : 在第一个起始参数中指定参数个数 , 那么就可以在循环还中读取所有的可变参数 ; b. 位哨 : 定义一个结束标记 , 在调用函数的时候 , 在最后一个参数中传递这个标记 , 这样在遍历可变参数的时候 , 可以根据这个标记结束可变参数的遍历。举例:

void arg_cnt(int cnt, ...)

{

  int value=0;

  int i=0;

  int arg_cnt=cnt;

  va_list arg_ptr;

  va_start(arg_ptr, cnt);

  for(i = 0; i < cnt; i++)

  {

  value = va_arg(arg_ptr,int);

  printf("value%d=%d/n", i+1, value);

  }

}

注:可变参数列表比较危险,不建议使用 ( C++ 编码规范》 )

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值