名著阅读 > C语言解惑 > 20.4 模拟设计printf函数 >

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
,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
!
!