讀古今文學網 > 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
!
!