本节分析一下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,0012FF78test内变量a和b的地址:0012FF24,0012FF28format:0012FF200012FF24,1000012FF28,-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进制:10016进制:6410进制:-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;}//数制转换函数内部使用宏定义SWAPvoid 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正序:10010016进制:逆序:46正序:646410进制:逆序:001正序:100-100零逆序:0正序:00OK!原来如此!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,0H,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的地址=12ff24p+1后的变量101的地址=12ff28va_p=12ff28变量101的va_p=12ff28执行va_arg(va_p,int )后的va_p=12ff2c变量102的va_p=12ff2c101变量102的va_p=12ff2c执行va_arg(va_p,int )后的va_p=12ff30变量103的va_p=12ff30102变量103的va_p=12ff30执行va_arg(va_p,int )后的va_p=12ff34变量104的va_p=12ff34103结束后的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!!