printf 函数的实现原理
/*
* =====================================================================================
*
* Filename: printf.c
*
* Description: printf 函数的实现
*
* Version: 1.0
* Created: 2010年12月12日 14时48分18秒
* Revision: none
* Compiler: gcc
*
* Author: Yang Shao Kun (), cdutyangshaokun@163.com
* Company: College of Information Engineering of CDUT
*
* =====================================================================================
*/
要了解变参函数的实现,首先我们的弄清楚几个问题:
:该函数有几个参数。
:该函数增样去访问这些参数。
:在访问完成后,如何从堆栈中释放这些参数。
对于c语言,它的调用规则遵循_cdedl调用规则。
在_cdedl规则中:.参数从右到左依次入栈
.调用者负责清理堆栈
.参数的数量类型不会导致编译阶段的错误
要弄清楚变参函数的原理,我们需要解决上述的3个问题,其中的第三个问题,根据调
用原则,那我们现在可以不管。
要处理变参函数,需要用到 va_list 类型,和 va_start,va_end,va_arg 宏定义。我
看网上的许多资料说这些参数都是定义在stdarg.h这个头文件中,但是在我的linux机
器上,我的版本是fedorea ,用vim访问的时候,确是在 acenv.h这个头文件中,估
计是内核的版本不一样的原因吧!!!
上面的这几个宏和其中的类型,在内核中是这样来实现的:
#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif /* _VALIST */
/*
* Storage alignment properties
*/
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
/*
* Variable argument list macro definitions
*/
#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))))
#endif /* va_arg */
首先来看 va_list 类型,其实这是一个字符指针。
va_start,是使ap指针指向变参函数中的下一个参数。
我们现在来看_bnd 宏的实现:
首先:
typedef s32 acpi_native_int;
typedef int s32;
看出来,acpi_native_int 其实就是 int 类型,那么,
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
这两个值就应该是相等的,都-等于:==0x00000003,按位取反后的结果就是:0xfffff
ffc,因此,
_bnd(x,bnd)宏在32位机下就是
(((sizeof (X)) + ()) & (0xfffffffc)),那么作用就很明显是取4的整数,就相当与
整数除法后取ceiling--向上取整。
回过头来看 va_start(ap,A),初始化参数指针ap,将函数参数A右边右边第一个参数地
址赋值给ap,A必须是一个参数的指针,所以,此种类型函数至少要有一个普通的参数
,从而提供给va_start ,这样va_start才能找到可变参数在栈上的位置。
va_arg(ap,T),获得ap指向参数的值,同时使ap指向下一个参数,T用来指名当前参数类
型。
va_end 在有些简单的实现中不起任何作用,在有些实现中可能会把ap改成无效值,这
里,是把ap指针指向了 NULL。
c标准要求在同一个函数中va_start 和va_end 要配对的出现。
那么到现在,处理多参数函数的步骤就是
:首先是要保证该函数至少有一个参数,同时用...参数申明函数是变参函数。
:在函数内部以va_start(ap,A)宏初始化参数指针。
:用va_arg(ap,T)从左到右逐个取参数值。
printf()格式转换的一般形式如下:
%[flags][width][.prec][type]
prec有一下几种情况:
正整数的最小位数
在浮点数中表示的小数位数
%g格式表示有效为的最大值
%s格式表示字符串的最大长度
若为*符号表示下个参数值为最大长度
width:为输出的最小长度,如果这个输出参数并非数值,而是*符号,则表示以下一个参数当做输出长度。
现在来看看我们的printf函数的实现,在内核中printf函数被封装成下面的代码:
static char sprint_buf[];
int printf(const char *fmt, ...)
{
va_list args;
int n;
va_start(args, fmt);//初始化参数指针
n = vsprintf(sprint_buf, fmt, args);/*函数放回已经处理的字符串长度*/
va_end(args);//与va_start 配对出现,处理ap指针
if (console_ops.write)
console_ops.write(sprint_buf, n);/*调用控制台的结构中的write函数,将sprintf_buf中的内容输出n个字节到设备*/
return n;
}
vs_printf函数的实现代码是:
int vsprintf(char *buf, const char *fmt, va_list args)
{
int len;
unsigned long long num;
int i, base;
char * str;
const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/
int flags; /* flags to number() */
int field_width; /* width of output field */
int precision; /* min. # of digits for integers; max
number of chars for from string */
int qualifier; /* 'h', 'l', or 'L' for integer fields */
/* 'z' support added 23/7/1999 S.H. */
/* 'z' changed to 'Z' --davidm 1/25/99 */
for (str=buf ; *fmt ; ++fmt)
{
if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/
{
*str++ = *fmt;
continue;
}
/* process flags */
flags = ;
repeat:
++fmt; /* this also skips first '%'--跳过格式控制符'%' */
switch (*fmt)
{
case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/
case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/
case ' ': flags |= SPACE; goto repeat;/*p with space*/
case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/
case '': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/
}
//#define ZEROPAD 1 /* pad with zero */
//#define SIGN 2 /* unsigned/signed long */
//#define PLUS 4 /* show plus */
//#define SPACE 8 /* space if plus */
//#define LEFT 16 /* left justified */
//#define SPECIAL 32 /* 0x */
//#define LARGE 64 /* use 'ABCDEF' instead of 'abcdef' */
/* get field width ----deal 域宽 取当前参数字段宽度域值,放入field_width 变量中。如果宽度域中是数值则直接取其为宽度值。 如果宽度域中是字符'*',表示下一个参数指定宽度。因此调用va_arg 取宽度值。若此时宽度值小于0,则该负数表示其带有标志域'-'标志(左靠齐),因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值。 */
field_width = -;
if ('' <= *fmt && *fmt <= '')
field_width = skip_atoi(&fmt);
else if (*fmt == '*')
{
++fmt;/*skip '*' */
/* it's the next argument */
field_width = va_arg(args, int);
if (field_width < ) {
field_width = -field_width;
flags |= LEFT;
}
}
/* get the precision-----即是处理.pre 有效位 */
precision = -;
if (*fmt == '.')
{
++fmt;
if ('' <= *fmt && *fmt <= '')
precision = skip_atoi(&fmt);
else if (*fmt == '*') /*如果精度域中是字符'*',表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/
{
++fmt;
/* it's the next argument */
precision = va_arg(args, int);
}
if (precision < )
precision = ;
}
/* get the conversion qualifier 分析长度修饰符,并将其存入qualifer 变量*/
qualifier = -;
if (*fmt == 'l' && *(fmt + ) == 'l')
{
qualifier = 'q';
fmt += ;
}
else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z')
{
qualifier = *fmt;
++fmt;
}
/* default base */
base = ;
/*处理type部分*/
switch (*fmt)
{
case 'c':
if (!(flags & LEFT))/*没有左对齐标志,那么填充field_width-1个空格*/
while (--field_width > )
*str++ = ' ';
*str++ = (unsigned char) va_arg(args, int);
while (--field_width > )/*不是左对齐*/
*str++ = ' ';/*在参数后输出field_width-1个空格*/
continue;
/*如果转换参数是s,则,表示对应的参数是字符串,首先取参数字符串的长度,如果超过了精度域值,则取精度域值为最大长度*/
case 's':
s = va_arg(args, char *);
if (!s)
s = "";
len = strnlen(s, precision);/*字符串的长度,最大为precision*/
if (!(flags & LEFT))
while (len < field_width--)/*如果不是左对齐,则左侧补空格=field_width-len个空格*/
*str++ = ' ';
for (i = ; i < len; ++i)
*str++ = *s++;
while (len < field_width--)/*如果是左对齐,则右侧补空格数=field_width-len*/
*str++ = ' ';
continue;
/*如果格式转换符是'p',表示对应参数的一个指针类型。此时若该参数没有设置宽度域,则默认宽度为8,并且需要添零。然后调用number()*/
case 'p':
if (field_width == -)
{
field_width = *sizeof(void *);
flags |= ZEROPAD;
}
str = number(str,(unsigned long) va_arg(args, void *), ,
field_width, precision, flags);
continue;
// 若格式转换指示符是'n',则表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中。
// 首先利用va_arg()得该参数指针,然后将已经转换好的字符数存入该指针所指的位置
case 'n':
if (qualifier == 'l')
{
long * ip = va_arg(args, long *);
*ip = (str - buf);
}
else if (qualifier == 'Z')
{
size_t * ip = va_arg(args, size_t *);
*ip = (str - buf);
}
else
{
int * ip = va_arg(args, int *);
*ip = (str - buf);
}
continue;
//若格式转换符不是'%',则表示格式字符串有错,直接将一个'%'写入输出串中。
// 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中,并返回到继续处理
//格式字符串。
case '%':
*str++ = '%';
continue;
/* integer number formats - set up the flags and "break" */
case 'o':
base = ;
break;
case 'X':
flags |= LARGE;
case 'x':
base = ;
break;
// 如果格式转换字符是'd','i'或'u',则表示对应参数是整数,'d', 'i'代表符号整数,因此需要加上
// 带符号标志。'u'代表无符号整数
case 'd':
case 'i':
flags |= SIGN;
case 'u':
break;
default:
*str++ = '%';
if (*fmt)
*str++ = *fmt;
else
--fmt;
continue;
}
/*处理字符的修饰符,同时如果flags有符号位的话,将参数转变成有符号的数*/
if (qualifier == 'l')
{
num = va_arg(args, unsigned long);
if (flags & SIGN)
num = (signed long) num;
}
else if (qualifier == 'q')
{
num = va_arg(args, unsigned long long);
if (flags & SIGN)
num = (signed long long) num;
}
else if (qualifier == 'Z')
{
num = va_arg(args, size_t);
}
else if (qualifier == 'h')
{
num = (unsigned short) va_arg(args, int);
if (flags & SIGN)
num = (signed short) num;
}
else
{
num = va_arg(args, unsigned int);
if (flags & SIGN)
num = (signed int) num;
}
str = number(str, num, base, field_width, precision, flags);
}
*str = '/0';/*最后在转换好的字符串上加上NULL*/
return str-buf;/*返回转换好的字符串的长度值*/
}
参看该资料:
C中的可变参数研究
一. 何谓可变参数
int printf( const char* format, ...);
这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示). 而我们又可以用各种方式来调用printf,如:
printf("%d",value);
printf("%s",str);
printf("the number is %d ,string is:%s", value, str);
二.实现原理
C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:
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)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/
#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v) )
/*va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
/*这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
#define va_end(ap) ( ap = (va_list)0 )
/*x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. */
以下再用图来表示:
在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
|最后一个可变参数 | ->高内存地址处
|——————————————————————————|
...................
|——————————————————————————|
|第N个可变参数 | ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N个可变参数的地址。
|——————————————— |
………………………….
|——————————————————————————|
|第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变参数的地址
|——————————————— |
|———————————————————————— ——|
| |
|最后一个固定参数 | -> start的起始地址
|—————————————— —| .................
|—————————————————————————— |
| |
|——————————————— |-> 低内存地址处
三.printf研究
下面是一个简单的printf函数的实现,参考了中的156页的例子,读者可以结合书上的代码与本文参照。
#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,//参数必须都是int 类型
{
char* pArg=NULL; //等价于原来的va_list
char c;
pArg = (char*) &fmt; //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
pArg += sizeof(fmt); //等价于原来的va_start
do
{
c =*fmt;
if (c != '%')
{
putchar(c); //照原样输出字符
}
else
{
//按格式字符输出数据
switch(*++fmt)
{
case 'd':
printf("%d",*((int*)pArg));
break;
case 'x':
printf("%#x",*((int*)pArg));
break;
default:
break;
}
pArg += sizeof(int); //等价于原来的va_arg
}
++fmt;
}while (*fmt != '\0');
pArg = NULL; //等价于va_end
return;
}
int main(int argc, char* argv[])
{
int i = 1234;
int j = 5678;
myprintf("the first test:i=%d",i,j);
myprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);
system("pause");
return 0;
}
在intel+win2k+vc6的机器执行结果如下:
the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;
四.应用
求最大值:
#include //不定数目参数需要的宏
int max(int n,int num,...)
{
va_list x;//说明变量x
va_start(x,num);//x被初始化为指向num后的第一个参数
int m=num;
for(int i=1;i {
//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
int y=va_arg(x,int);
if(y>m)m=y;
}
va_end(x);//清除变量x
return m;
}
int main()
{
printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
return 0;
}
printf 函数的实现原理的更多相关文章
- C语言printf()函数具体解释和安全隐患
一.问题描写叙述 二.进一步说明 请细致注意看,有例如以下奇怪的现象 int a=5; floatx=a; //这里转换是没有问题的.%f打印x是 5.000000 printf("%d\n ...
- 不定参数函数原理以及实现一个属于自己的printf函数
一.不定参数函数原理 二.实现一个属于自己的printf函数 参考博文:王爽汇编语言综合研究-函数如何接收不定数量的参数
- C语言中可变参数的原理——printf()函数
函数原型: int printf(const char *format[,argument]...) 返 回 值: 成功则返回实际输出的字符数,失败返回-1. 函数说明: 使用过C语言的人所再熟悉不过 ...
- 可变参数列表与printf()函数的实现
问题 当我们刚开始学习C语言的时候,就接触到printf()函数,可是当时"道行"不深或许不够细心留意,又或者我们理所当然地认为库函数规定这样就是这样,没有发现这个函数与普通的函数 ...
- 【C语言】浅谈可变参数与printf函数
一.何谓可变参数 int printf( const char* format, ...); 这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用& ...
- 实现简单的printf函数
首先,要介绍一下printf实现的原理 printf函数原型如下: int printf(const char* format,...); 返回值是int,返回输出的字符个数. 例如: int mai ...
- s3c2440——实现裸机的简易printf函数
在单片机开发中,我们借助于vsprintf函数,可以自己实现一个printf函数,但是,那是IDE帮我们做了一些事情. 刚开始在ARM9裸机上自己写printf的实现的时候,包含对应头文件也会提示vs ...
- C利用可变参数列表统计一组数的平均值,利用函数形式参数栈原理实现指针运算
//描述:利用可变参数列表统计一组数的平均值 #include <stdarg.h> #include <stdio.h> float average(int num, ... ...
- 你真的很了解printf函数吗?
对C语言中经常使用的printf这个库函数,你是否真的吃透了呢? 系统化的学习C语言程序设计,是不是看过一两本C语言方面的经典著作就足够了呢?答案是显而易见的:不够.通过这种典型的入门级的学习方式,是 ...
随机推荐
- python 惰性求值 https://blog.csdn.net/Appleyk/article/details/77334221
为什么调用的不是同一个函数呢 是因为调用函数后,函数的生命周期就结束了,再调用就是另一个函数了
- day11 filter函数
场景模拟:我想判断某个列表里面的某个元素怎么怎么样 基础方法,如果需要判断多次则重复代码 ret = [] move_peole = ["alex","sb_wupeiq ...
- 获取外网出口ip
curl ifconfig.me 或 curl cip.cc
- Threed.sleep是不会释放锁,而wait是释放锁的(对象锁)
实战分析 一直都说,Threed.sleep是不会释放锁,而wait是释放锁的(对象锁),现理论上来分析一下啊. v package thread.concurrent; public class D ...
- Mysql 数据库 基础代码
-- 创建数据库 CREATE DATABASE book; -- 创建作者表 CREATE TABLE authors( Id int not NULL, -- 作者编号 Fname VARCHAR ...
- luogu1514 [NOIp2010]引水入城 (bfs+记忆化搜索)
我们先bfs一下看看是否能到最底下的所有点 如果不能的话,直接把不能到的那几个数一数就行了 如果能的话: 可以发现(并不可以)某格能到达的最底下的格子一定是一个连续的区间 (因为如果不连续的话,我们先 ...
- SDL源码阅读笔记(2) video dirver的初始化及选择
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie 前一篇文章 讲了SDL的除video以外的大部分模块.本文主要关注SDL的video模块部分. SD ...
- Ubuntu/Unity中更改窗口修饰键Alt为Super
在Ubuntu中的Unity桌面环境里,可以使用Alt配合鼠标左键拖动窗口,这一方便的设定有许多不方便的地方.和很多的软件有热键上的冲突,比如Visual Stdio Code的多光标控制功能. 注意 ...
- pacman安装软件包出现损坏
状况 File .pkg.tar.xz is corrupted (invalid or corrupted package (PGP signature)).Do you want to delet ...
- [luoguU42591][小T的面试题]
luoguU42591 题意: n个不超过n的正整数中,其中有一个数出现了两次,其余的数都只出现了一次, 求这个出现两次的数. 思路: 这个题的亮点在于内存限制1MB.明显不能再用数组储存了,肯定是用 ...