首页 » C语言解惑 » C语言解惑全文在线阅读

《C语言解惑》20.4 模拟设计printf函数

关灯直达底部

本节分析一下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!!