printf 源代码 实现

转载自品略图书馆 http://www.pinlue.com/article/2020/03/2222/1210046055343.html

 

打开Source Insight来阅读EduOS的源代码,我们在stdio.c里找到了printf的实现代码.首先看看对printf的定义:

int printf (const char *cntrl_string, ...)

第一个参数cntrl_string是控制字符串,也就是平常我们写入%d,%f的地方.紧接着后面是一个变长参数.

看看函数头部的定义:

int pos = 0, cnt_printed_chars = 0, i;

unsigned char* chptr;

va_list ap;

马上晕!除了ap我们可以马上判断出来是用来读取变长参数的,i用于循环变量.其他变量都不知道是怎么回事.不要着急,我们边看代码边分析.代码的第一行必然是

va_start (ap, cntrl_string);

用来初始化变长参数.

接下来是一个while循环

while (cntrl_string[pos]) {

...

}

结束条件是cntrl_string[pos]为NULL,显然这个循环是用来遍历整个控制字符串的.自然pos就是当前遍历到的位置了.进入循环首先闯入视线的是

if (cntrl_string[pos] == "%") {

pos++;

...

}

开门见山,上来就当前字符是否办断是否%.一猜就知道如果成立pos++马上取出下一个字符在d,f,l等等之间进行判断.往下一看,果真不出所料:

switch (cntrl_string[pos]) {

case "c":

...

case "s":

...

case "i":

...

case "d":

...

case "u":

...

用上switch-case了. 快速浏览一下下面的代码.

首先看看case "c"的部分

case "c":

putchar (va_arg (ap, unsigned char));

cnt_printed_chars++;

break;

%c表示仅仅输出一个字符.因此先通过va_arg进行参数的类型转换,之后用putchar[1]输出到屏幕上去.之后是

cnt_printed_chars++,通过这句我们就可以判断出cnt_printed_chars使用来表示,已经被printf输出的字符个数的.

再来看看 case "s":

case "s":

chptr = va_arg (ap, unsigned char*);

i = 0;

while (chptr [i]) {

cnt_printed_chars++;

putchar (chptr [i++]);

}

break;

和case "c",同出一辙.cnt_printed_chars++放在了循环内,也证明了刚才提到的他的作用.另外我们也看到了cnptr是用来在处理字符串时的位置指针.到此为止,我们清楚的所有变量的用途,前途变得更加光明了.

接下来:

// PartI

case "i":

case "d":

cnt_printed_chars += printInt (va_arg (ap, int));

break;

case "u":

cnt_printed_chars += printUnsignedInt (va_arg (ap, unsigned int));

break;

case "x":

cnt_printed_chars += printHexa (va_arg (ap, unsigned int), "x");

break;

case "X":

cnt_printed_chars += printHexa (va_arg (ap, unsigned int), "X");

break;

case "o":

cnt_printed_chars += printOctal (va_arg (ap, unsigned int));

break;

// Part II

case "p":

putchar ("0");

putchar ("x");

cnt_printed_chars += 2; /* of "0x" */

cnt_printed_chars += printHexa (va_arg (ap, unsigned int), "x");

break;

case "#":

pos++;

switch (cntrl_string[pos]) {

case "x":

putchar ("0");

putchar ("x");

cnt_printed_chars += 2; /* of "0x" */

cnt_printed_chars += printHexa (va_arg (ap, unsigned int), "x");

break;

case "X":

putchar ("0");

putchar ("X");

cnt_printed_chars += 2; /* of "0X" */

cnt_printed_chars += printHexa (va_arg (ap, unsigned int), "X");

break;

case "o":

putchar ("0");

cnt_printed_chars++;

cnt_printed_chars += printOctal (va_arg (ap, unsigned int));

break;

注意观察一下,PartII的代码其实就是比PartI的代码多一个样式.在16进制数或八进制前加入0x或是o,等等.因此这里就只分析一下PartI咯.

其实仔细看看PartI的个条case,也就是把参数分发到了更具体的函数用于显示,然后以返回值的形式返回输出个数.对于这些函数就不具体分析了.我们先来看看一些善后处理:

先看case的default处理.

default:

putchar ((unsigned char) cntrl_string[pos]);

cnt_printed_chars++;

就是直接输出cntrl_string里%号后面的未知字符.应该是一种容错设计处理.

再看看if (cntrl_string[pos] == "%")的else部分

else {

putchar ((unsigned char) cntrl_string[pos]);

cnt_printed_chars++;

pos++;

}

如果不是%开头的,那么直接输出这个字符.

最后函数返回前

va_end (ap);

return cnt_printed_chars;

va_end处理变长参数的善后工作.并返回输出的字符个数.

在最后我们有必要谈谈putChar函数以及基本输出的基础函数printChar,先来看看putChar

int putchar (int c) {

switch ((unsigned char) c) {

case "\n" :

newLine ();

break;

case "\r" :

carriageReturn ();

break;

case "\f" :

clearScreen ();

break;

case "\t" :

printChar (32); printChar (32); /* 32 = space */

printChar (32); printChar (32);

printChar (32); printChar (32);

printChar (32); printChar (32);

break;

case "\b":

backspace ();

break;

case "\a":

beep ();

break;

default :

printChar ((unsigned char) c);

}

return c;

}

通览一下,也是switch-case为主体的.主要是用来应对一些特殊字符,如\n,\r,....这里需要提一下,关于\t的理解.有些人认为\t就是8个space,有些人则认为,屏幕分为10大列(每个大列8个小列总共80列).一个\t就跳到下一个大列输出.也就是说不管你现在实在屏幕的第1,2,3,4,5,6,7位置输出字符,只要一个\t都在第8个位置开始输出. VS.NET中就是用的这种理解.因此如果按照这个理解的话,\t的实现可以这样

int currentX = ((currentX % 10) + 1) * 8;

然后在currentX位置输出.

接下来看printChar也就是输出部分最低层的操作咯

void printChar (const byte ch) {

*(word *)(VIDEO + y * 160 + x * 2) = ch | (fill_color << 8);

x++;

if (x >= WIDTH)

newLine ();

setVideoCursor (y, x);

}

这里VIDEO表示显存地址也就是0xB8000.通过 y * 160 + x 屏幕(x,y)坐标在显存中的位置.这里需要知道,一个字符显示需要两个字节,一个是ASCII码,第二个是字符属性代码也就是颜色代码.因此才必须 y * 80 * 2 + x = y * 160 + x.那么ch | (fill_color << 8)也自然就是写入字符及属性代码用的了.每写一个字符光标位置加1,如果大于屏幕宽度WIDTH就换行.最后通过setVideoCursor设置新的光标位置.完成了整个printChar过程.

转载自http://hi.baidu.com/yangyangye2008/blog/item/e2f251b5e5e025688bd4b2d7.html

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

深入printf

阅读:359次   时间:2004-10-07 00:00:00   字体:[ ]

/***

*printf.c - print formatted

*

* Copyright (c) 1985-1997, Microsoft Corporation. All rights reserved.

*

*Purpose:

* defines printf() - print formatted data

*

*******************************************************************************/

#include

#include

#include

#include

#include

#include

#include

/***

*int printf(format, ...) - print formatted data

*

*Purpose:

* Prints formatted data on stdout using the format string to

* format data and getting as many arguments as called for

* Uses temporary buffering to improve efficiency.

* _output does the real work here

*

*Entry:

* char *format - format string to control data format/number of arguments

* followed by list of arguments, number and type controlled by

* format string

*

*Exit:

* returns number of characters printed

*

*Exceptions:

*

*******************************************************************************/

int __cdecl printf (

const char *format,

...

)

/*

* stdout ""PRINT"", ""F""ormatted

*/

{

va_list arglist;

int buffing;

int retval;

va_start(arglist, format);

_ASSERTE(format != NULL);//断言宏。如果输出格式字符串指针为空,则在DEBUG版下断言,报告错误。

_lock_str2(1, stdout);

buffing = _stbuf(stdout);//stdout:指定输出到屏幕

retval = _output(stdout,format,arglist);

_ftbuf(buffing, stdout);

_unlock_str2(1, stdout);

return(retval);

}

以上为printf()的源代码

1、从含有可选参数函数中获得可选参数,以及操作这些参数

typedef char *va_list;

void va_start( va_list arg_ptr, prev_param );

type va_arg( va_list arg_ptr, type );

void va_end( va_list arg_ptr );

假定函数含有一个必选参数和多个可选参数,必选参数声明为普通数据类型,且能通过参数名来获得该变量的值。可选参数通过宏va_start、va_arg和va_end(定义在stdarg.h或varargs.h中)来进行操作,即通过设置指向第一个可选参数指针、返回当前参数、在返回参数后重新设置指针来操作所有的可选参数。

va_start:为获取可变数目参数的函数的参数提供一种便捷手段。设置arg_ptr为指向传给函数参数列表中的第一个可选参数的指针,且该参数必须是va_list类型。prev_param是在参数列表中第一个可选参数前的必选参数。

va_arg:返回由arg_ptr所指向的参数的值,且自增指向下一个参数的地址。type为当前参数的类型,用来计算该参数的长度,确定下一个参数的起始位置。它可以在函数中应用多次,直到得到函数的所有参数为止,但必须在宏va_start后面调用。

va_end:在获取所有的参数后,设置指针arg_ptr为NULL。

下面举例说明:

#include

#include

int average( int first, ... );

void main( void )

{

/* Call with 3 integers (-1 is used as terminator). */

printf( "Average is: %d\n", average( 2, 3, 4, -1 ) );

/* Call with 4 integers. */

printf( "Average is: %d\n", average( 5, 7, 9, 11, -1 ) );

/* Call with just -1 terminator. */

printf( "Average is: %d\n", average( -1 ) );

}

int average( int first, ... )

{

int count = 0, sum = 0, i = first;

va_list marker;

va_start( marker, first ); /* Initialize variable arguments. */

while( i != -1 )

{

sum += i;

count++;

i = va_arg( marker, int);

}

va_end( marker ); /* Reset variable arguments. */

return( sum ? (sum / count) : 0 );

}

返回值为:

Average is: 3

Average is: 8

Average is: 0

综上所述,在printf()函数中,可以只输出一个字符串,也可按照一定的形式输出含有多个可选参数的字符串信息。因此,首先就要通过这些宏来获取所有的可选参数。在上面的源码可以看出printf()中,只使用了宏at_start,将可选参数的首地址赋给了arglist。

2、锁定字符串及输出字符串到屏幕

#define _lock_str2(i,s) _lock_file2(i,s)

void __cdecl _lock_file2(int, void *);

#define _unlock_str2(i,s) _unlock_file2(i,s)

void __cdecl _unlock_file2(int, void *);

int __cdecl _stbuf(FILE *);

void __cdecl _ftbuf(int, FILE *);

int __cdecl _output(FILE *, const char *, va_list);

在output函数中,读取格式字符串中的每一个字符,然后对其进行处理,处理方式根据每一个字符所代表的意义来进行,如:普通字符直接利用函数WRITE_CHAR(ch, &charsout);输出到控制台。

其中的主要部分是对转换说明符(d,c,s,f)的处理,现在将对其中的部分代码进行详细说明,这里只说明最基本的转换说明符,对这些须基本的转换说明符进行修饰的修饰符,程序中单独进行处理。下面是函数output()(output.c)部分源代码:

case ST_TYPE:

//表示当前处理的字符的类型为转换说明符。

...

switch (ch) {

//下面对参数的获取都是利用宏va_arg( va_list arg_ptr, type );来进行的。

case ""c"": {

//从参数表中获取单个字符,输出到缓冲字符串中,此时,type=int

buffer[0] = (char) get_int_arg(&argptr); /* get char to print */

text = buffer;

textlen = 1; /* print just a single character */

}

break;

case ""s"": {

//从参数表中获取字符串,输出到缓冲字符串中,此时,type=char*

int i;

char *p; /* temps */

text = get_ptr_arg(&argptr);

...

}

break;

case ""w"": {

//对宽字符进行处理

...

} /* case ""w"" */

break;

...

case ""e"":

case ""f"":

case ""g"": {

//对浮点数进行操作

...

#if !LONGDOUBLE_IS_DOUBLE

/* do the conversion */

if (flags & FL_LONGDOUBLE) {

_cldcvt((LONGDOUBLE*)argptr, text, ch, precision, capexp);

va_arg(argptr, LONGDOUBLE);

//对长双精度型进行处理,此时,type=long double

}

else

#endif /* !LONGDOUBLE_IS_DOUBLE */

{

//对双精度型进行处理,此时,type=double

_cfltcvt((DOUBLE*)argptr, text, ch, precision, capexp);

va_arg(argptr, DOUBLE);

}

...

break;

//对整型变量处理

case ""d"":

case ""i"":

...

goto COMMON_INT;

case ""u"":

radix = 10;

goto COMMON_INT;

case ""p"":

...

goto COMMON_INT;

case ""o"":

...

注:对于浮点型double和long double,有相应的转换说明符(%f表示双精度型,%lf表示长双精度型),而float却没有。其中的原因是,在K&RC下,float值用于表达式或用作参数前,会自动转换成double类型。而ANSI C一般不会自动把float转换成double。有些程序已假定其中的float参数会被转换成double,为了保护大量这样的程序,所有printf()函数的float参数还是被自动转换成double型。因此,在K&RC或ANSI C下,都无需用特定的转换说明符来显示float型。

综上所述,转换说明符必须与待打印字符的类型。通常,用户有种选择。例如,如要打印一个int类型的值。则只可以使用%d,%x或%o。所有这些说明符都表示要打印一个int类型的值;它们只不过提供了一个数值的几种不同表示。类似一,可以用%f、%g和%e来表示double类型的值。但如果转换说明与类型不匹配,将会出现意想不到的结果。为什么呢?问题就在于C向函数传递信息的方式。

这个失败的根本细节与具体实现相关。它决定了系统中的参数以何方式传递。函数调用如下:

float n1;

double n2;

long n3;

long n4;

...

printf("%ld,%ld,%ld,%ld",n1,n2,n3,n4);

这个调用告诉计算机,要把变量n1,n2,n3和n4的值交给计算机,它把这些变量放进称作栈(stack)的内存区域中,来完成这一任务。计算机把这些值放进栈中,其根据是变量的类型而不是转换说明符,比如n1,把8个字节放入栈中(float被转换成double),类似地,为n2放了8字节,其后给n3和n4各放了4个字节。接着,控制的对象转移到printf();此函数从栈中读数,不过在这一过程中,它是在转换说明符的指导下,读取数值的。说明符%ld指定printf()应读4个字节(va_arg( va_list arg_ptr, type )中type=long),因此printf()读入栈中的4个字节,作为它的第一个值。但是这只是n1的前半部分,这个值被看成一个long整数。下一个说明符%ld读入4个字节,这正是n1的后半部分,这个值被看成第二个long整数。类似地,第三、第四次又读入n2的前后两部分。因此,尽管我们对n3和n4使用了正确的说明符,printf()仍然会产生错误。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值