本节分析一下printf的机理,通过编制一个自己的myprintf打印函数,进一步加深对打印输出函数的理解,用好这个函数。
20.4.1 具有可变参数的函数
printf函数的原型声明如下:
int printf (const char *format ,... )
按照这个格式,声明如下的test函数。
int test (const char *format ,... )
在主函数中声明整型变量a和b,并把它们的地址&a和&b打印出来,参照printf函数的调用方式,写出如下对test函数的调用方法。
test ("%d ,%d\n" ,a ,b );
由此可以写出如下主函数。
#include <stdio.h> int test (const char *format ,... ); // 声明test 函数 int main () { int a=100 , b=-100 ; printf (" 变量a 和b 的地址:%p ,%p\n" ,&a ,&b ); // 输出变量a 和b 的地址 test ("\n" ,a ,b ); // 调用test 函数 return 0 ; }
在test函数中再次输出传递参数a和b的地址,这是函数test内的临时变量,所以它们的地址与主函数里的地址并不相同。声明指针p并用format初始化。因为format是字符常量指针,所以使用int强制转换。
为了简单。test函数内并不处理字符串,所以可以随便赋值,这里用一个换行符。根据对函数test的要求,编写如下实现程序。在程序里移动指针p,看看会带来什么结果。
int test (const char *format , int a , int b ) { int *p ; printf ("test 内变量a 和b 的地址:%p ,%p\n" ,&a ,&b ); p= (int* )&format ; // 指向format 地址 printf ("format :%p\n" ,p ); // 输出format 地址 p++ ; //p 现在指向format + 1 的地址 printf ("%p ,%d\n" ,p ,*p ); // 输出当前p 的指向地址和地址里的内容 p++ ; // p 现在指向format + 2 的地址 printf ("%p ,%d\n" ,p ,*p ); // 输出当前p 的指向地址和地址里的内容 return 0 ; }
程序运行结果如下:
变量a 和b 的地址:0012FF7C ,0012FF78 test 内变量a 和b 的地址:0012FF24 ,0012FF28 format :0012FF20 0012FF24 ,100 0012FF28 ,-100
传输给函数test的参数在函数里将作为临时变量被重新分配地址。format是test函数的第1个参数,被分配的地址是0012FF20,参数a为0012FF24,b为0012FF28。如果再有一个参数,将依次分配地址。这就是test内的参数地址分配规律。
因为分配给参数的地址是连续的,所以根据formart的地址就可以利用指针找到后面的参数了。在test函数里,正是利用指针依次打印出a和b的值。
为了演示变量a和b在test内分配的地址与format的关系,将它设计成只有两个参数的函数。下面将它设计为可变参数并能将一个整数按10进制和16进制打印出来。为了分析方便,添加测试用的打印信息。
处理10进制和16进制的字符串使用标准的“%d”和“%x”,它们将作为字符串常量传给test函数,在test函数内,将根据是“%d”还是“%x”借用printf函数输出。
【例20.16】设计可变参数程序的例子。
#include <stdio.h> int test (const char *format ,... ); // 声明可变参数test 函数 int main () { int a=100 ; test ("%d%x%d 结束!\n" ,a ,a ,-200 ); return 0 ; } int test (const char *format ,... ) { int *p ; char c ; int value ; p= (int* )&format ; p++ ; // 先做p++ ,使p 指向字符串常量后面的第1 个参数 while ((c = *format++ ) != '\0' ) // 循环到常量字符串结束标志 { if (c != '%' ) // 如果不是格式字符则直接输出 { putchar (c ); continue ; } else // 处理格式字符 { c = *format++ ; // 取% 后面的字符 if (c=='d' ) { value=*p++ ; // 将参数值赋给value ,加1 指向下一个参数 printf ("10 进制:%d\n" ,value ); // 借用测试 } if (c=='x' ) { value=*p++ ; // 将参数值赋给value ,加1 指向下一个参数 printf ("16 进制:%x\n" ,value ); // 借用测试 } } } return 0 ; }
测试时有意使用"%d%x%d结束!\n"字符串,以便演示判断语句的正确性。程序中的注释已经很清楚,不再赘述,下面给出程序的运行结果。
10 进制:100 16 进制:64 10 进制:-200 结束!
20.4.2 设计简单的打印函数
test函数已经初具雏形,但它的输出是借用了printf函数。为了设计自己的myprint函数,现在不再借用printf函数,而是设计自己的函数完成打印。
【例20.17】设计实现printf简单功能的myprintf可变参数函数的例子。
设计自己的打印函数myprintf,实现最简单的“%d”和“%x”功能。函数原型如下:
int myprintf (const char *format ,... );
要把数值转换成倒序的字符串,再把字符串反序即得到正确的字符串。设计一个根据进制转换相应的字符串函数,最后一个参数为要转换的进制。其原型如下:
void itoa (int , char * , int );
在itoa函数里,先把数字按进制转换为数字字符串,这是一个与给定数字逆序的字符串,直接在程序里面设计一个宏SWAP,通过交换实现字符串反转,得到与给定数字相同的字符串供输出。
在调用itoa之前,还需要判断数字的正负,如果是负整数,需要变成正整数,待转换后再在它的前面输出负的符号位。
因为puts函数自动在尾部实现换行,这不符合输出要求(会多一个换行)。设计一个去掉换行的函数myputs。其原型如下:
void myputs (char *buf )
为了验证程序,除了正负整数,也需要打印0以及与格式字符一起的其他字符。曾经提到过,对于一个字符串s,“printf(s);”与“printf("%s",s);”是不等效的,通过这个演示,将能进一步证明这一点。
// 完整的程序 #include <stdio.h> int myprintf (const char *format ,... ); // 声明打印函数的函数原型 void myputs (char * ); // 声明输出字符串函数的函数原型 void itoa (int , char * , int ); // 声明数制转换函数的函数原型 int main ( ) { int a=100 ; char s="OK !" ; myprintf ("10 进制:%d\n16 进制:%x\n10 进制:%d 零%d\n" ,a ,a ,-100 ,0 ); myprintf (s ); myprintf (" 原来如此!\n" ); myprintf ("here !%s\n" ,s ); return 0 ; } //puts 有换行符,必须去掉,设计myputs 替代它 void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } // 数制转换函数内部使用宏定义SWAP void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; printf ("\n 逆序:%s\n" ,buf ); // 验证信息 // 定义交换宏实现反转 #define SWAP (a ,b ) do{a= (a )+ (b ); \ b= (a )- (b ); \ a= (a )- (b ); \ }while (0 ) // 反转 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } printf ("\n 正序:%s\n" ,buf ); // 验证信息 return ; } // 可变参数输出函数 int myprintf (const char *format ,... ) { int *p ; char c ; char buf[32] ; int value ; p= (int* )&format ; p++ ; while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); // 输出字符串中的非格式字符 continue ; } else { c = *format++ ; // 取% 后面的字符 if (c=='d' ) // 处理10 进制 { value=*p++ ; if (value<0 ) // 处理负整数 { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else // 处理正整数 { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) // 将10 进制正整数按16 进制处理 { value=*p++ ; itoa (value ,buf ,16 ); myputs (buf ); } } } return 0 ; }
程序输出结果如下:
10 进制: 逆序:001 正序:100 100 16 进制: 逆序:46 正序:64 64 10 进制: 逆序:001 正序:100 -100 零 逆序:0 正序:0 0 OK !原来如此! here !
程序对0的处理正确。语句
myprintf (" 原来如此!\n" );
是由“putchar(c);”语句输出。语句
myprintf (s );
中的字符串“OK”,也是由“putchar(c);”语句输出。因为没有设计“%s”的功能,所以语句
myprintf ("here !%s\n" ,s );
只是通过“putchar(c);”语句输出“here!”,而不输出s的内容。如果设计了“%s”的功能,则将s的内容作为字符串输出,如果字符串里有“%”号,它也不会处理,只会原样输出。对于printf函数而言,如果字符串不是自己预先设计的,而是程序运行的中间产物,都应尽可能地使用格式“%s”输出,以免发生错误。
【例20.18】为myprintf函数增加处理字符和字符串的功能。
增加“%c”和“%s”的功能也很容易,为了简洁,将调试信息去掉。下面是它的源程序。为了对照主程序的输出结果,将主程序放在最后,其他函数按先后顺序排列,所以就不需要先声明它们的函数原型了。
#include <stdio.h> void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; // 定义交换宏 #define SWAP (a ,b ) do{a= (a )+ (b ); \ b= (a )- (b ); \ a= (a )- (b ); \ }while (0 ) // 反转 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } return ; } int myprintf (const char *format ,... ) { int *p ; char c ; char buf[32] ; int value ; p= (int* )&format ; p++ ; while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; // 取% 后面的字符 if (c=='c' ) { value=*p++ ; putchar (value ); } if (c=='s' ) { value=*p++ ; myputs ((char* )value ); } if (c=='d' ) { value=*p++ ; if (value<0 ) { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) { value=*p++ ; itoa (value ,buf ,16 ); myputs (buf ); } } } return 0 ; } int main () { char c1='H' ; char c2="How are you ?" ; myprintf ("%d ,%d ,%d ,%x ,%x\n" ,100 ,0 ,-100 ,100 ,0 ); //1 验证%d 和%x myprintf ("%c ,%s\n" ,c1 ,c2 ); //2 验证%c 和%s myprintf ("%c ,%s\n" ,'H' ,"Fine !" ); //3 带格式使用字符常量 myprintf ("How are you ?\n" ); //4 直接用字符串常量 myprintf (c2 ); //5 直接用字符串名字 myprintf ("%s\n" ,c2 ); //6 标准格式 myprintf ("\n" ,c2 ); //7 使用有误,只输出换行,不处理c2 myprintf ("How are%s" ,"you ?\n" ); //8 格式正确 return 0 ; }
主程序使用6条验证语句,注意它们执行路径的区别。第4条和第5条是在判别格式字符的时候直接一个字一个字地输出。第7条有误,但编译系统无法识别错误。第8条的参数是字符常量,经由“%s”的路径输出。显然,字符串作为整体输出时的速度会快些,字符串愈长,差别愈显著。比较下面的运行结果,仔细体会不同语句的区别。
100 ,0 ,-100 ,64 ,0 H ,How are you ? H ,Fine ! How are you ? How are you ?How are you ? How areyou ?
20.4.3 利用宏改进打印函数
标准库实现printf函数用到了va_开头的三个有参数宏va_start、va_arg和va_end。这些宏定义在头文件stdarg.h中。利用这些宏可以大大简化设计,为了看看它们的作用,设计一个不处理10进制,仅输出参考信息的myprintf函数。va_list用来声明一个供宏使用的指针类型的变量。
【例20.19】研究如何使用宏来简化设计的例子。
#include <stdio.h> #include <stdarg.h> int myprintf (const char *format ,... ) { int *p ,i=101 ; va_list va_p ; //1 char c ; char buf[32]={'\0'} ; int value=0 ; p= (int* )&format ; printf ("format 的地址=%x\n" ,(int )p ); // 打印对照 p++ ; // 先做p++ ,使两者相等,后面程序也变化 printf ("p+1 后的变量%d 的地址=%x\n" ,i ,(int )p ); // 打印对照 va_start (va_p ,format ); //2 printf ("va_p=%x\n" ,(int )va_p ); // 打印对照 while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; if (c=='d' ) { printf (" 变量%d 的va_p=%x\n" , i ,(int )va_p ); // 打印对照 value=va_arg (va_p ,int ); printf (" 执行va_arg (va_p ,int )后的va_p=%x\n" , (int )va_p ); // 打印对照 i++ ; printf (" 变量%d 的va_p=%x\n" , i ,(int )va_p ); // 打印对照 printf ("%d" ,value ); } } } printf (" 结束后的va_p=%x\n" , (int )va_p ); // 打印对照 va_end (va_p ); printf (" 执行va_end (va_p )后的va_p=%x\n" , (int )va_p ); // 打印对照 return 0 ; } int main () { myprintf ("%d\n%d\n%d\n" ,101 ,102 ,103 ); return 0 ; }
程序输出结果如下:
format 的地址=12ff24 p+1 后的变量101 的地址=12ff28 va_p=12ff28 变量101 的va_p=12ff28 执行va_arg (va_p ,int )后的va_p=12ff2c 变量102 的va_p=12ff2c 101 变量102 的va_p=12ff2c 执行va_arg (va_p ,int )后的va_p=12ff30 变量103 的va_p=12ff30 102 变量103 的va_p=12ff30 执行va_arg (va_p ,int )后的va_p=12ff34 变量104 的va_p=12ff34 103 结束后的va_p=12ff34 执行va_end (va_p )后的va_p=0
对照分析输出结果,执行语句
va_start (va_p ,format );
的作用首先是把format地址赋给va_p,然后执行加1,这时va_p就变成第1个变量101的地址。原来的程序要执行p++才能取得变量101的地址,这就可以不需要执行+1操作了。
执行value=va_arg(va_p,int)语句,将整数值赋给value的同时,也对va_p执行加1操作,使va_p指向下一个变量102的地址12ff2c,这就可以直接取得变量102的value值。原来利用指针p时,需要执行p+1操作。改用宏,宏内执行了这一操作,所以简化了指令。
程序循环结束后的va_p=12ff34(程序指示是变量104,其实是越界的地址),所以要求调用一个用于释放空间的宏va_end,执行va_end(va_p)后的va_p=0。
下面的例题是使用宏完成简单打印函数的完整程序,程序中还改用异或定义交换宏,异或运行快(加法要有进位操作),提高程序性能。
【例20.20】使用宏优化简单打印函数的例子。
#include <stdio.h> #include <stdarg.h> void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; // 使用异或定义交换宏,异或运行快(加法要有进位操作) #define SWAP (a ,b ) do{a= (a )^ (b ); \ b= (a )^ (b ); \ a= (a )^ (b ); \ }while (0 ) // 反转 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } return ; } int myprintf (const char *format ,... ) { va_list ap ; char c ; char buf[32] ; int value ; va_start (ap ,format ); while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; // 取% 后面的字符 if (c=='c' ) { putchar (va_arg (ap ,char )); } if (c=='s' ) { myputs (va_arg (ap ,char * )); } if (c=='d' ) { value=va_arg (ap ,int ); if (value<0 ) { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) { value=va_arg (ap ,int ); itoa (value ,buf ,16 ); myputs (buf ); } } } va_end (ap ); return 0 ; } int main () { char c1='H' ; char c2="How are you ?" ; myprintf ("%d ,%d ,%d ,%x ,%x\n" ,100 ,0 ,-100 ,100 ,0 ); //1 验证%d 和%x myprintf ("%c ,%s\n" ,c1 ,c2 ); //2 验证%c 和%s myprintf ("%c ,%s\n" ,'H' ,"Fine !" ); //3 带格式使用用字符常量 myprintf ("How are you ?\n" ); //4 直接用字符串常量 myprintf (c2 ); //5 直接用字符串名字 myprintf ("%s\n" ,c2 ); //6 标准格式 myprintf ("\n" ,c2 ); //7 使用有误,只输出换行,不处理c2 myprintf ("How are%s" ,"you ?\n" ); //8 格式正确 return 0 ; }
这是改写例20.18的程序,主程序一样,所以运行结果也相同。
注意程序中有一条语句
putchar (va_arg (ap , char ));
是可以正确执行的,这是因为直接作为putchar的参数。其实,va_arg宏的第2个参数不能被指定为char、short或float类型。因为char和short类型的参数会被转换为int类型,而float类型会被转换成double类型。如果指定错误,将会引起麻烦。语句
c = va_arg (ap , char );
肯定是不对的,因为无法传递一个char类型参数,如果传递了,它会被自动转换为int类型。应该将它写为如下语句:
c = va_arg (ap , int );
如果cp是一个字符指针,而程序中又需要一个字符指针类型的参数,则下面的写法是正确的。
cp = va_arg (ap , char * );
当作为参数时,指针并不会转换,只有char、short或float类型的数值才会被转换。
【例20.21】分析下面程序的输出结果。
#include <stdio.h> #include <string.h> int main () { int i=0 ,len=0 ; char str="Look !" ; len=strlen (str ); for (i=0 ; i<len ;i++ ) printf ("%s\n" ,str+i ); }
【解答】“printf("%s\n",str+i);”语句不是把str作为首地址,而是str+i做地址。由自行设计myprintf函数中可以知道,str+i等效于&str[i]。它与下面程序的输出结果一样。
#include <stdio.h> #include <string.h> int main () { int i=0 ,len=0 ; char str="Look !" ; len=strlen (str ); for (i=0 ; i<len ;i++ ) printf ("%s\n" ,&str[i] ); }
程序每循环一次,输出字符就从左边减少一个字符。输出结果如下:
Look ! ook ! ok ! k ! !