作者:戎亚新

摘要:一直以来都觉得printf似乎是c语言库中功能最强大的函数之一,不仅因为它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。

关键字:printf, 可变参数

1. 使用情形

int a =10;
double b = 20.0;
char *str = "Hello world";
printf("begin print\n");
printf("a=%d, b=%.3f, str=%s\n", a, b, str);
...

  从printf的使用情况来看,我们不难发现一个规律,就是无论其可变的参数有多少个,printf的第一个参数总是一个字符串。而正是这第一个参数,使得它可以确认后面还有有多少个参数尾随。而尾随的每个参数占用的栈空间大小又是通过第一个格式字符串确定的。然而printf到底是怎样取第一个参数后面的参数值的呢,请看如下代码

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. 分析

  从上面的代码来看,printf似乎并不复杂,它通过一个宏va_start把所有的可变参数放到了由args指向的一块内存中,然后再调用vsprintf. 真正的参数个数以及格式的确定是在vsprintf搞定的了。由于vsprintf的代码比较复杂,也不是我们这里要讨论的重点,所以下面就不再列出了。我们这里要讨论的重点是va_start(ap, A)宏的实现,它对定位从参数A后面的参数有重大的制导意义。现在把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含义解释一下如下:

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

  在printf的va_start(args, fmt)中,fmt的类型为char *, 因此对于一个32为系统 sizeof(char *) = 4, 如果int大小也是32,则va_start(args, fmt);相当于 char *args = (char *)(&fmt) + 4; 此时args的值正好为fmt后第一个参数的地址。对于如下的可变参数函数

    void fun(double d,...)
{
va_list args;
int n;
va_start(args, d);
}

则 va_start(args, d);相当于

    char *args = (char *)&d + sizeof(double);

  此时args正好指向d后面的第一个参数。

  可变参数函数的实现与函数调用的栈结构有关,正常情况下c/c++的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。对于函数

    void fun(int a, int b, int c)
{
int d;
...
}

其栈结构为

    0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c

  对于任何编译器,每个栈单元的大小都是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字节)

  对于函数void fun1(char a, int b, double c, short d)

  如果知道了参数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);
}

  如果后续参数的类型是变化而且是未知的,则必须通过一个参数中设定模式来匹配后续参数的个数和类型,就像printf一样,当然我们可以定义自己的模式,如可以用i表示int参数,d表示double参数,为了简单,我们用一个字符表示一个参数,并由该字符的名称决定参数的类型而字符的出现的顺序也表示后续参数的顺序。 我们可以这样定义字符和参数类型的映射表,

i---int
s---signed short
l---long
c---char

"ild"模式用于表示后续有三个参数,按顺序分别为int, long, double类型的三个参数那么这样我们可以定义自己版本的printf 如下

void printf(char *fmt, ...)
{
char s[80] = "";
int paramCount = strlen(fmt);
write(stdout, "paramCount = " , strlen(paramCount = ));
itoa(paramCount,s,10);
write(stdout, s, strlen(s));
char *p = (char *)(&fmt) + sizeof(char *);
int *pi = (int *)p;
for (int i=0; i<paramCount; i++)
{
char line[80] = "";
strcpy(line, "param");
itoa(i+1, s, 10);
strcat(line, s);
strcat(line, "=");
switch(fmt[i])
{
case 'i':
case 's':
itoa((*pi),s,10);
strcat(line, s);
pi++;
break;
case 'c':
{
int len = strlen(line);
line[len] = (char)(*pi);
line[len+1] = '\0';
}
break;
case 'l':
ltoa((*(long *)pi),s,10);
strcat(line, s);
pi++;
break;
default:
break;
}
}
}

也可以这样定义我们的Max函数,它返回多个输入整型参数的最大值

int Max(int n, ...)
{
int *p = &n + 1;
int ret = *p;
for (int i=0; i<n; i++)
{
if (ret < *(p + i))
ret = *(p + i);
}
return ret;
}

可以这样调用, 后续参数的个数由第一个参数指定

int m = Max(3, 45, 12, 56);
int m = Max(1, 3);
int m = Max(2, 23, 45);
int first = 34, second = 45, third=5;
int m = Max(5, first, second, third, 100, 4);

结论

  对于可变参数函数的调用有一点需要注意,实际的可变参数的个数必须比前面模式指定的个数要多,或者不小于, 也即后续参数多一点不要紧,但不能少, 如果少了则会访问到函数参数以外的堆栈区域,这可能会把程序搞崩掉。前面模式的类型和后面实际参数的类型不匹配也有可能造成把程序搞崩溃,只要模式指定的数据长度大于后续参数长度,则这种情况就会发生。如:

printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);

  参数1,2,3,4的默认类型为整型,而模式指定的需要为double型,其数据长度比int大,这种情况就有可能访问函数参数堆栈以外的区域,从而造成危险。但是printf("%d, %d, %d", 1.0, 20., 3.0);这种情况虽然结果可能不正确,但是确不会造成灾难性后果。因为实际指定的参数长度比要求的参数长度长,堆栈不会越界。

从printf谈可变参数函数的实现相关推荐

  1. printf以及可变参数函数讲解(转载)

    printf以及可变参数函数的讲解(转载自 谁不小心的) 添加链接描述 printf以及可变参数函数的讲解 转载自 谁不小心的 链接:https://blog.csdn.net/trochiluses ...

  2. 解析可变参数函数的实现原理(printf,scanf)

    From: http://hi.baidu.com/huifeng00/blog/item/085e8bd198f46ed3a8ec9a0b.html 学习C的语言的时候,肯定接触到标准输出和标准输入 ...

  3. printf 函数使用 可变参数函数实现原理

    一. Printf 和scanf 函数格式 Printf 和 scanf 家族函数都属于可变参数函数(variadic function).这种函数需要固定数量的强制参数,后面是数量可变的可选参数. ...

  4. C语言可变参数函数(printf/scanf)

    C 语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function).这种函数需要固定数量的强制参数(mandatory argument),后面是数量可变的可选参数(opti ...

  5. c语言怎样获得函数内参数的值_C语言可变参数函数的实现原理

    在本人的<C语言可变参数函数的实现方法>一文中,介绍了如何建立自己的可变参数函数. 下面继续介绍可变参数函数的实现原理. 在汇编语言程序设计中,详细介绍了子程序的实现思想: (1)子程序只 ...

  6. 关于C语言可变参数函数的一些研究和总结

    可变参数函数是指函数参数的个数.类型等是不固定的,需要在用户调用过程中,根据实际传入的参数来确定其类型.个数等信息.例如:可变参数函数printf可谓是在C开发过程中使用最多的标准输出库函数之一,因此 ...

  7. C语言可变参数函数的使用及相关函数介绍

    By qianghaohao(Xqiang) 在C语言中当一个函数参数无法列举出来,或者参数个数  不确定,这时我们将函数声明为可变参数的形式,根据需  要传适当个数的参数.举例如下: int fun ...

  8. mysql不定参数函数_可变参数函数(一)

    一个函数可以接受不定数的参数个数,这就是可变参数函数,比较常见的比如printf(),scanf(): printf(const char*format,-); printf("%d&quo ...

  9. ios开发-Object-C可变参数函数

    简介 一个可变参数函数是指一个函数拥有不定的参数,即为一个函数可接收多个参数.有时我们会遇到一些算术问题需要用到,比如是计算传入参数的总和,字符串的连接或是其它操作过程,我们在 OC 里也经常使用,最 ...

最新文章

  1. python读取配置文件
  2. 数据防泄漏(中文版)
  3. 学习Struts 2.0系列文章
  4. if you do applications
  5. U盘上的笔记全弄丢了,痛苦啊痛苦!
  6. 《构建之法》读后感 二
  7. Mysql 8.0 遇到用遇到的几个问题及解决办法
  8. 信息安全技术网络安全等级保护定级指南_报业网络安全等级保护定级参考指南V2.0发布...
  9. 《C#高效编程》读书笔记11-理解短小方法的优势
  10. php文件流播放拖动,自定义实现可以播放暂停、进度拖拽、音量控制及全屏的H5播放器...
  11. sql server 连接
  12. 树莓派 之 USB摄像头安装和使用
  13. uniapp拍照上传照片流程笔记
  14. 100个开源C/C++项目中的bugs(一)数组和字符串处理的错误
  15. 苹果mp3软件_M4R如何转为MP3?音频转换的高效方法
  16. k8s FailedCreatePodSandBox: Failed create pod sandbox
  17. win10自带ubuntu桌面_windows10开启 linux子系统桌面,巨详细,值得一藏
  18. Mixly实现秒表功能
  19. CnOpenData地方留言文本数据简介
  20. 一、虚拟化技术发展概述

热门文章

  1. samba3.2的安装全过程(tar方式)
  2. leetcode 494. 目标和
  3. leetcode 477. 汉明距离总和(位运算)
  4. 深入理解InnoDB(5)-文件系统
  5. leetcode43. 字符串相乘
  6. firebase auth_如何使用auth和实时数据库构建Firebase Angular应用
  7. javascript实用库_编写实用JavaScript的实用指南
  8. 深度学习算法和机器学习算法_啊哈! 4种流行的机器学习算法的片刻
  9. 简述yolo1-yolo3_使用YOLO框架进行对象检测的综合指南-第一部分
  10. 大数据平台构建_如何像产品一样构建数据平台