printf 系列函数 与 可变参数函数

原创 2013年12月02日 14:36:29
本篇,我们主要讲解printf的系列函数:printf, fprintf, sprintf, snprintf, vprintf, vfprintf, vsprintf, vsnprintf 的使用,然后讲解可变参数函数的使用范围与实例;另外,我们还讲解了可变函数实现的底层原理和陷阱。


1.C语言可变参数函数


熟悉C的人都知道,C语言支持可变参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用(...)表示,比如我们常用的printf()\execl函数等;printf函数的原型如下:

int printf(const char *format, ...);

注意,采用这种形式定义的可变参数函数,至少需要一个普通的形参,比如上面代码中的*format,后面的省略号是函数原型的一部分。

C语言之所以可以支持可变参数函数,一个重要的原因是C调用规范中规定C语言函数调用时,参数是从右向左压入栈的;这样一个函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个;以printf为例:

printf("%d%s\n",i,s);

printf函数在定义的时候,不知道函数调用的时候会传递几个参数。在实现上,printf函数只需关心第一个参数,即字符串“%d%s\n”,当读到%d的时候,printf知道自己需要第二个参数,这时只需要去栈上寻找即可;当读到%s时,再去栈上网上寻找一个参数即可。简单说,printf不关心栈上到底压了多少参数,只关心自己需要多少。

那么对于一个定义为可变参数的函数,函数定义的时候并没有定义形参原型,怎么使用参数呢?

C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_start、va_arg和va_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:

void va_start(va_list ap, last);//取第一个可变参数(如上述printf中的i)的指针给ap,last是函数声明中的最后一个固定参数(比如printf函数原型中的*fromat);
type va_arg(va_list ap, type);//返回当前ap指向的可变参数的值,然后ap指向下一个可变参数;type表示当前可变参数的类型(支持的类型位int和double);
void va_end(va_list ap);//将ap置为NULL

当一个函数被定义位可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。

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

下面是一个具体的示例(摘自wikipedia):

#include <stdarg.h>
 
double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count); //使va_list指向起始的參數
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double); //檢索參數,必須按需要指定類型
    va_end(ap); //釋放va_list
    return tot/count;
}

除此之外,我们还需要注意一个陷阱,即va_arg宏的第2个参数不能被指定为char、short或者float类型。《C和C++经典著作:C陷阱与缺陷在可变参数函数传递时,因为char和short类型的参数会被提升为int类型,而float类型的参数会被提升为double类型 。

例如,以下的代码是错误的

a = va_arg(ap,char);

因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:

a = va_arg(ap,int);

还需要注意的一个问题是,即时我们知道在某种体系结构下C语言函数的参数都压在栈上,我们也应该避免直接去栈上取想要的参数,因为这样会降低程序的灵活性和可移植性,并带来一些安全上潜在的危险。上述的三个宏,包括va_list,在不同的体系结构下会有不同的实现方法,比如va_list,有的系统上直接指向栈;而有的系统却将其实现为一个指针数组。


2.printf函数的实现


//acenv.h
typedef char *va_list;
#define  _AUPBND        (sizeof (acpi_native_int) - 1)
#define  _ADNBND        (sizeof (acpi_native_int) - 1)
                        
#define _bnd(X, bnd)    (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T)   (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)      (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
//start.c
static char sprint_buf[1024];
int printf(char *fmt, ...)
{
        va_list args;
        int n;
        va_start(args, fmt);
        n = vsprintf(sprint_buf, fmt, args);
        va_end(args);
        write(stdout, sprint_buf, n);
        return n;
}
//unistd.h
static inline long write(int fd, const char *buf, off_t count)
{
        return sys_write(fd, buf, count);
}

3.分析可变函数参数的实现


可变函数参数,实现的时候需要逐个调用传入的可变参数。想一想,我们需要完成哪些工作?

1)知道可变参数的起始地址

这个功能,我们是通过va_start(arg_ptr, argN)宏定义来实现的,具体如下:

 #define
va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd
(A,_AUPBND)))) 
 va_start(ap, A)
    {
         char *ap =  ((char *)(&A)) + sizeof(A)并int类型大小地址对齐
    }

这里,需要解释一下,函数调用时,参数入栈顺序是从右向左的,而栈的增长方向是从高地址到低地址。所以,如果一个函数形式如下:

int func(int a,int b,int c)那么,在栈中的情况如下:

c 高地址
b  
a 低地址

这就是说,函数右边的参数占据着高地址。

另外,对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是 
    0x1ffc-->a  (4字节)
    0x2000-->b  (4字节)
    0x2004-->c  (8字节)
    0x200c-->d  (4字节)

2)知道可变参数的个数以及每个参数的类型

如果知道了参数a的地址,则要取后续参数的值则可以通过a的地址计算a后面参数的地址,然后取对应的值,而后面参数的个数可以直接由变量a指定,当然也可以像printf一样根据第一个参数中的%模式个数来决定后续参数的个数和类型。如果参数的个数由第一个参数a直接决定,则后续参数的类型如果没有变化并且是已知的,则我们可以这样来取后续参数, 假定后续参数的类型都是double; 

void fun1(int num, ...)
{
    double *p = (double *)((&num)+1);
    double Param1 = *p;
    double Param2 = *(p+1);
    ...
    double Paramn  *(p+num);
}

三个与可变参数实现有关的宏定义如下:

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )          //第一个可选参数地址
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址
#define va_end(ap)   ( ap = (va_list)0 )                           // 将指针置为无效


4.一个例子:printf系列函数

看了可变函数的实现,其实使用起来还是相当麻烦的。 而日常工作中,我们经常需要类似于printf的输出,所以,C语言库函数为我们实现了相应的封装。


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


   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):


       snprintf(), vsnprintf():
           _BSD_SOURCE || _XOPEN_SOURCE >= 500 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
           or cc -std=c99


简介:
     printf系列函数根据格式控制产生相应的输出。printf和vprintf出处到stdout;fprintf和fprintf输出到指定的输出流;sprintf和snprintf输出到指定的字符串。n,控制输出个数(包含'\0')。

   函数vprintf(),  vfprintf(), vsprintf(), vsnprintf() 分别等同于 printf(),fprintf(), sprintf(), snprintf()——前者的函数参数是一个va_list,后者的参数是可变个数的。这些函数不调用va_end宏,因为它们调用
  va_arg 宏, 详细的内容可以参考stdarg。


      使用实例:

// 例1:格式化到一个文件流,可用于日志文件

FILE *logfile;
int WriteLog(const char * format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
return nWrittenBytes;
}
…
// 调用时,与使用printf()没有区别。
WriteLog("%04d-%02d-%02d %02d:%02d:%02d  %s/%04d logged out.", 
nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID);

5.潜在的风险


从va的实现可以看出,指针的合理运用,把C语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服C的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。其中存在两个隐患:

1)如何确定参数的类型。 va_arg在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg都把当前指针所指向的内容强制转换到指定类型;

2)结束标志。如果没有结束标志的判断,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例2中SqSum()求的是自然数的平方和,所以我把负数和0作为它的结束标志。例如scanf把接收到的回车符作为结束标志,大家熟知的printf()对字符串的处理用'\0'作为结束标志,无法想象C中的字符串如果没有'\0', 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是malloc/free。

允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker达到其攻击目的。(常见的exploit攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。

 

printf系列详解

printf系列主要有三个函数: printf , fprintf , sprintf 。    各自的功能很好记住,printf 向标准输出 输出内容; fprintf,就是向filestream...
  • yanghangjun
  • yanghangjun
  • 2011年12月29日 11:00
  • 1662

c语言中可变参数的原理---printf()函数

函数原型: int printf(const char *format[,argument]...)        返 回 值: 成功则返回实际输出的字符数,失败返回-1.  函数说明:    ...
  • tangcong29
  • tangcong29
  • 2014年01月18日 10:28
  • 1001

可变参数函数实现

void ErrorMsg(const char *pszParam, ...){ char buf[1024]; va_list va;  va_start(va, pszParam);  vspr...
  • hbyh
  • hbyh
  • 2007年10月08日 16:19
  • 358

可变参数函数printf函数的实现

#include #include using namespace std; void myitoa(int n, char str[], int radix) { int i, j, remai...
  • bladeandmaster88
  • bladeandmaster88
  • 2016年11月20日 15:08
  • 271

【C++基础之二十】可变参数的函数

C++中可变参数的函数是从C中继承而来,可变参数的函数是指函数的参数个数可变,参数类型不定的函数。我们最常见的就是printf()。 1.可变参数函数实现原理 指定参数的函数实现很简单,通过通过指...
  • jackyvincefu
  • jackyvincefu
  • 2013年12月24日 09:55
  • 16612

01 [c语言][重要的知识点]printf函数和scanf函数的数据输出与读取问题

------- android培训、java培训、IOS培训、期待与您交流! ---------- 这是刚学习C语言的时候遇到的最早的一个问题,是以前从来没有接触过的一个全新知识点,...
  • qq_23189441
  • qq_23189441
  • 2015年04月09日 00:06
  • 450

利用matlab中的printf函数和fopen函数debug

http://blog.csdn.net/qing101hua/article/details/50618936 上面网址讲解的是 fprintf和fopen函数的用法。在循环中结合使用fopen和f...
  • uestcxuxu
  • uestcxuxu
  • 2016年08月31日 16:13
  • 772

如何自定义可变参数函数

在我们编写代码中,有时需要我们自定义可变参数函数,像库函数中有pirntf,ioctl都是可变参数函数,如果我们要实现自定义可变参数,一般要实现像int ioctl(int fd, unsigned ...
  • u012681014
  • u012681014
  • 2017年04月15日 00:33
  • 561

C++ - 可变参数函数模板(Variadic Function Template) 详解 及 代码

可变参数函数模板(Variadic Function Template) 详解 及 代码 本文地址: http://blog.csdn.net/caroline_wendy/article/det...
  • u012515223
  • u012515223
  • 2013年12月02日 17:19
  • 6332

单片机中printf函数的重映射

单片机中printf函数的重映射 一、源自于:大侠有话说 1.如果你在学习单片机之前学过C语言,那么一定知道printf这个函数.它最最好用的功能 除了打印你想要的字符到屏幕上外,还能把数字进行...
  • joqian
  • joqian
  • 2012年12月17日 15:20
  • 2379
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:printf 系列函数 与 可变参数函数
举报原因:
原因补充:

(最多只允许输入30个字)