第14章 格式化

标准C库函数printf、fprintf和vprintf可以格式化数据并输出,而sprintf和vsprintf可以将数据格式化到字符串中。这些函数调用时的参数包括一个格式串和一组参数列表,列表中的参数将被格式化。格式化的过程,由嵌入到格式串中的转换限定符(conversion specifier,形如%c)控制,第i个%c描述了格式串之后的参数列表中第i个参数如何格式化。格式串中其他字符逐字复制。例如,如果name是字符串Array,而count为8,

sprintf(buf, "The %s interface has %d functions\n", 
    name, count)  

会将字符串"The Array interface has 8 functions\n"填充到buf中,其中\n表示换行符。转换限定符还可以包含宽度、精度和填充字符等说明信息。例如,如果在上述的格式串中使用%06d而不是%d,那么会将字符串"The Array interface has 000008 functions\n"填充到buf中。

这些函数毫无疑问很有用,但却至少有4个缺点。首先,转换限定符的集合是固定的,因而无法提供特定于客户程序的代码。其次,格式化的结果,只能输出或存储到字符串中,无法指定特定于客户程序的输出例程。再次,也是最危险的缺点是,sprintf和vsprintf可能试图在输出缓冲区中存储超出其容量的字符,同时又无法指定输出缓冲区的大小。最后,对于参数列表的可变部分传递的各个参数,没有对应的类型检查机制。Fmt接口改正了前三个缺点。

14.1 接口

Fmt接口导出了11个函数、一个类型、一个变量和一个异常:

fmt.h

〉≡  
  #ifndef FMT_INCLUDED  
  #define FMT_INCLUDED  
  #include <stdarg.h>  
  #include <stdio.h>  
  #include "except.h"  
 
  #define T Fmt_T  
  typedef void (*T)(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[256], int width, int precision);  
 
  extern char *Fmt_flags;  
  extern const Except_T Fmt_Overflow;  
 
 〈exported functions

 155〉  
 
  #undef T  
  #endif  

从技术上讲,Fmt不是一个抽象数据类型,但它确实导出了一个类型Fmt_T,它定义了与每个格式化代码关联的格式转换函数的类型,下文将详细阐述。

14.1.1 格式化函数

两个主要的格式化函数是:

exported functions

 155〉≡  
  extern void Fmt_fmt (int put(int c, void *cl), void *cl,  
      const char *fmt, ...);  
  extern void Fmt_vfmt(int put(int c, void *cl), void *cl,  
      const char *fmt, va_list ap);  

Fmt_fmt按照第三个参数fmt给出的格式串来格式化其第四个和后续参数;并调用put(c, cl)来输出每个格式化完毕的字符c;c当做unsigned char处理,因此传递到put的c值总是正的。Fmt_vfmt按照fmt给出的格式串来格式化ap指向的各个参数,具体过程类似Fmt_fmt,如下所述。

参数cl可以指向客户程序提供的数据,它会直接传递给客户程序提供的put函数而不作解释。put函数返回一个整数,通常是其参数。Fmt函数并不使用该功能,但这种设计使得可以某些机器上将标准I/O函数fputc用作put函数(同时,需要作为cl传递FILE*)。例如,

Fmt_fmt((int (*)(int, void *))fputc, stdout,  
    "The %s interface has %d functions\n", name, count)  

输出

The Array interface has 8 functions  

到标准输出,此时name为Array而count为8。其中的转换是必要的,因为fputc的类型为int (*)(int, FILE*),而put的类型为int (*)(int, void*)。仅当FILE指针的表示与void指针相同时,这种用法才是正确的。

图14-1给出的语法图定义了转换限定符的语法。转换限定符中的字符定义了一条穿过语法图的路径,有效的限定符会从头到尾遍历一条路径。限定符以%开头,后接可选的标志字符,其解释取决于格式码,接下来是可选的字段宽度、周期和精度,最后以单字符的格式码结束,由图14-1中的C表示。有效的标志字符是那些出现在Fmt_flags指向的字符串中的字符,它们通常指定了对齐(justification)、填充(padding)和截断(truncation)信息。如果一个标志字符在一个限定符中出现多于255次,则是已检查的运行时错误。如果字段宽度或精度显示为星号,那么假定下一个参数为整数,且用作宽度或精度。因而,一个限定符可能消耗零或多个参数,这取决于星号的出现与否以及与格式码关联的具体转换函数。如果指定的宽度或精度值等于INT_MIN(最小的负整数),则是已检查的运行时错误。

168

图14-1 转换限定符的语法

标志、宽度和精度的准确的解释,取决于与转换限定符关联的转换函数。所调用的转换函数,是调用Fmt_fmt时已注册的那些函数。

默认的转换限定符及与之相关的转换函数,是标准I/O库中printf和相关函数功能的一个子集。Fmt_flags的初始值指向字符串"-+ 0",其中的字符是有效的标志字符。-使得被转换的字符串按给定的字段宽度向左对齐,否则,字符串将向右对齐。+使得符号转换的结果以-或+开始。空格使得符号转换的结果以空格开始(如果是正的)。0使得数字转换的结果在前部用0补齐,直至达到字段宽度为止,否则使用空格补齐。负数宽度解释为-标志加上对应的正数宽度值。负数精度解释为没有指定精度。

表14-1综述了默认的转换限定符。这些是标准C库中的定义的限定符的一个子集。

表14-1 默认的转换限定符

169

以下函数

exported functions

 155〉+≡ 
  extern void Fmt_print (const char *fmt, ...);  
  extern void Fmt_fprint(FILE *stream,  
      const char *fmt, ...);  
  extern int Fmt_sfmt   (char *buf, int size,  
      const char *fmt, ...);  
  extern int Fmt_vsfmt(char *buf, int size,  
      const char *fmt, va_list ap);  

类似于C库函数printf、fprintf、sprintf和vsprintf。

Fmt_fprint按照fmt给定的格式串来格式化第三个和后续参数,并将格式化输出写到指定的流中。Fmt_print将格式化输出写到标准输出。

Fmt_sfmt按照fmt给定的格式串来格式化第四个和后续参数,将格式化输出以0结尾字符串形式,存储到buf[0..size - 1]中。Fmt_vsfmt的语义类似,但其参数则取自于可变长度参数列表ap。这两个函数都会返回存储到buf中字符的数目,不计算结尾的0字符。如果Fmt_sfmt和Fmt_vsfmt输出的字符数多于size(包含结尾的0字符),则引发Fmt_Overflow异常。如果size不是正值,则造成已检查的运行时错误。

以下两个函数

exported functions

 155〉+≡  
  extern char *Fmt_string (const char *fmt, ...);  
  extern char *Fmt_vstring(const char *fmt, va_list ap);  

类似Fmt_sfmt和Fmt_vsfmt,但它们会分配足够大的字符串来容纳格式化输出结果,并返回这些字符串。客户程序负责释放返回的字符串。Fmt_string和Fmt_vstring可能引发Mem_Failed异常。

如果传递给上述任一格式化函数的参数put、buf或fmt为NULL,则造成已检查的运行时错误。

14.1.2 转换函数

每个格式符C都关联到一个转换函数。这些关联可以通过调用下述函数来改变:

exported functions

 155〉+≡  
  extern T Fmt_register(int code, T cvt);  

Fmt_register将cvt设置为code指定的格式符对应的转换函数,并返回指向先前转换函数的指针。因而,客户程序可以临时替换转换函数,而后又恢复到原来的转换函数。code小于1或大于255,都是已检查的运行时错误。如果格式串使用的转换限定符没有相关联的转换函数,同样是已检查的运行时错误。

许多转换函数,都是%d和%s转换限定符对应的转换函数的变体。Fmt导出了两个实用函数,供对应于数值和字符串的内部转换函数使用。

exported functions

 155〉+≡  
  extern void Fmt_putd(const char *str, int len,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[256], int width, int precision);  
  extern void Fmt_puts(const char *str, int len, 
      int put(int c, void *cl), void *cl,  
      unsigned char flags[256], int width, int precision);  

Fmt_putd假定str[0..len-1]包含了一个有符号数的字符串表示,它将按照flags、width和precision指定的转换,如表14-1中的%d所述,来输出该字符串。类似地,Fmt_puts按照flags、width和precision指定的转换,如%s所述,来输出str[0..len - 1]。如果传递给Fmt_putd或Fmt_puts的str为NULL、len为负值、flags为NULL或put为NULL,则是已检查的运行时错误。

Fmt_putd和Fmt_puts本身不是转换函数,但可以被转换函数调用。在编写特定于客户程序的转换函数时,这两个函数特别有用,如下文说明。

类型Fmt_T定义了转换函数的签名,即其参数的类型和返回类型。转换函数调用时有七个参数。前两个是格式码和指向可变长度参数列表指针的指针,该参数列表用于访问被格式化的数据。第三个和第四个参数是客户程序的输出函数和相关数据。最后三个参数是标志、字段宽度和精度。标志通过一个256个元素的字符数组给出,第i个元素等于标志字符i在转换限定符中出现的次数。width和precision在没有显式给出时等于INT_MIN。

转换函数必须使用如下的表达式

va_arg(*app, type

)  

来取得参数,并根据与该转换函数相关联的格式码进行格式化。type 是该参数的预期类型。该表达式取得参数的值,然后将*app加1使之指向下一个参数。如果转换函数使*app不正确地递增,则造成未检查的运行时错误。

Fmt用于限定符%s的私有转换函数,说明了如何编写转换函数,以及如何使用Fmt_puts。限定符%s类似printf的%s:其转换函数将输出对应的参数字符串中的字符,直至遇到0字符为止,或输出字符的数目已经达到了可选精度的限制。-标志或负数宽度指定了左对齐。转换函数使用va_arg从可变长度参数列表中取得参数并调用Fmt_puts:

conversion functions

 159〉≡  
  static void cvt_s(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      char *str = va_arg(*app, char *);  
 
      assert(str);  
      Fmt_puts(str, strlen(str), put, cl, flags,  
          width, precision);  
  }  

Fmt_puts解释flags、width和precision,并据此输出字符串

functions

 159〉≡  
  void Fmt_puts(const char *str, int len,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
 
      assert(str);  
      assert(len >= 0);  
      assert(flags);  
     〈normalize

 width and

 flags 159〉  
      if (precision >= 0 && precision < len)  
          len = precision;  
      if (!flags['-'])  
          pad(width - len, ' ');  
     〈emit

 str[0..len-1] 159〉  
      if ( flags['-'])  
        pad(width - len, ' ');  
  }  
 
〈emit

 str[0..len-1] 159〉≡  
  {  
      int i;  
      for (i = 0; i < len; i++)  
          put((unsigned char)*str++, cl);  
  }  

到unsigned char的转换确保了传递给put的值总是较小的正整数,正如Fmt的规格所限定。

在忽略宽度或精度时,width和precision等于INT_MIN。该接口提供了特定于客户程序的转换函数所需的灵活性,使之能够对宽度和精度使用显式设置/省略值的所有组合,还可以使用重复的标志。但默认转换函数不需要这种一般性,它们将省略的宽度视为显式将宽度设置为0,负数宽度视为-标志连同对应的正数宽度,负数精度视为省略精度,重复出现的标志被视作只出现一次。如果有显式设置的精度,则忽略0标志,而且如上所示,至多会输出str中的precision个字符。

normalize

 width and

 flags 159〉≡  
 〈normalize

 width 160〉  
 〈normalize

 flags 160〉  
 
〈normalize

 width 160〉≡  
  if (width == INT_MIN)  
      width = 0;  
  if (width < 0) {  
      flags['-'] = 1;  
      width = -width;  
  }  
 
〈normalize

 flags 160〉≡  
  if (precision >= 0)  
      flags['0'] = 0;  

如对pad的调用所示,必须输出width-len个空格来正确地对齐输出:

macros

 160〉≡  
  #define pad(n,c

) do { int nn = (n

); \  
      while (nn-- > 0) \  
        put((c

), cl); } while (0)  

pad是一个宏,因为它需要访问put和cl。

下一节将描述其他默认转换函数的实现。

14.2 实现

Fmt的实现包括接口中定义的各个函数,与默认转换限定符关联的转换函数,以及将转换限定符映射到转换函数的表。

fmt.c

〉≡  
  #include <stdarg.h>  
  #include <stdlib.h>  
  #include <stdio.h>  
  #include <string.h>  
  #include <limits.h>  
  #include <float.h>  
  #include <ctype.h>  
  #include <math.h>  
  #include "assert.h"  
  #include "except.h"  
  #include "fmt.h"  
  #include "mem.h"  
  #define T Fmt_T  
 
 〈types

 162〉  
 〈macros

 160〉  
 〈conversion functions

 159〉  
 〈data

 160〉  
 〈static functions

 161〉  
 〈functions

 159〉  
 
〈data

 160〉≡  
  const Except_T Fmt_Overflow = { "Formatting Overflow" };  

14.2.1 格式化函数

Fmt_vfmt是实现的核心,因为所有其他接口函数都调用它来完成实际的格式化工作。Fmt_fmt是最简单的例子,它初始化一个va_list指针,指向其参数列表的可变部分,并调用Fmt_vfmt:

functions

 159〉+≡  
  void Fmt_fmt(int put(int c, void *), void *cl,  
      const char *fmt, ...) {  
      va_list ap;  
 
      va_start(ap, fmt);  
      Fmt_vfmt(put, cl, fmt, ap);  
      va_end(ap);  
}  

Fmt_print和Fmt_fprint调用Fmt_vfmt时,将outc作为put函数,将对应于标准输出的流或给定的流作为相关的数据:

static functions

 161〉≡  
  static int outc(int c, void *cl) {  
      FILE *f = cl;  
 
      return putc(c, f);  
  }  
 
〈functions

 159〉+≡  
  void Fmt_print(const char *fmt, ...) {  
      va_list ap;  
      va_start(ap, fmt);  
      Fmt_vfmt(outc, stdout, fmt, ap);  
      va_end(ap);  
  }  
 
  void Fmt_fprint(FILE *stream, const char *fmt, ...) {  
      va_list ap;  
 
      va_start(ap, fmt);  
      Fmt_vfmt(outc, stream, fmt, ap);  
      va_end(ap);  
  }  

Fmt_sfmt调用Fmt_vsfmt:

functions

 159〉+≡  
  int Fmt_sfmt(char *buf, int size, const char *fmt, ...) {  
      va_list ap;  
      int len;  
 
      va_start(ap, fmt);  
      len = Fmt_vsfmt(buf, size, fmt, ap);  
      va_end(ap);  
      return len;  
  }  

Fmt_vsfmt调用Fmt_vfmt时,传递了一个put函数和一个指向结构的指针,该结构跟踪了需要格式化输出到buf的字符串和buf能够容纳的字符数:

types

 162〉≡  
  struct buf {  
      char *buf;  
      char *bp;  
      int size;  
  };  

buf和size实际上是复制了Fmt_vsfmt的名称类似的参数,而bp则指向buf中输出下一个被格式化字符的位置。Fmt_vsfmt会初始化该结构的一个局部变量实例,并将指向该实例的一个指针传递给Fmt_vfmt:

functions

 159〉+≡  
  int Fmt_vsfmt(char *buf, int size, const char *fmt,  
      va_list ap) {  
      struct buf cl;  
 
      assert(buf);  
      assert(size > 0);  
      assert(fmt);  
      cl.buf = cl.bp = buf;  
      cl.size = size;  
      Fmt_vfmt(insert, &cl, fmt, ap);  
      insert(0, &cl);  
      return cl.bp - cl.buf - 1;  
  }  

上述对Fmt_vfmt的调用,内部又调用了私有函数insert,参数是每一个需要输出的字符和指向Fmt_vsfmt的局部buf结构实例的指针。insert会检查是否有空间容纳需要输出的字符,并将该字符存储到bp字段指向的位置,并将bp字段加1:

static functions

 161〉+≡  
  static int insert(int c, void *cl) {  
      struct buf *p = cl;  
 
      if (p->bp >= p->buf + p->size)  
          RAISE(Fmt_Overflow);  
      *p->bp++ = c;  
      return c;  
  }  

Fmt_string和Fmt_vstring的工作原理同上,只是使用了不同的put函数。Fmt_string调用了Fmt_vstring:

functions

 159〉+≡  
  char *Fmt_string(const char *fmt, ...) {  
      char *str;  
      va_list ap;  
 
      assert(fmt);  
      va_start(ap, fmt);  
      str = Fmt_vstring(fmt, ap);  
      va_end(ap);  
      return str;  
  }  

Fmt_vstring将buf结构实例初始化为一个可容纳256个字符的字符串,并将执行该实例的指针传递给Fmt_vfmt:

functions

 159〉+≡  
  char *Fmt_vstring(const char *fmt, va_list ap) {  
      struct buf cl;  
 
      assert(fmt);  
      cl.size = 256;  
      cl.buf = cl.bp = ALLOC(cl.size);  
      Fmt_vfmt(append, &cl, fmt, ap);  
      append(0, &cl);  
      return RESIZE(cl.buf, cl.bp - cl.buf);  
  }  

append类似于Fmt_vsfmt的put,只是它会在必要时将buf的容量加倍,使之能够容纳格式化输出的字符。

static functions

 161〉+≡  
  static int append(int c, void *cl) {  
      struct buf *p = cl;  
 
      if (p->bp >= p->buf + p->size) {  
          RESIZE(p->buf, 2*p->size);  
          p->bp = p->buf + p->size;  
          p->size *= 2;  
      }  
      *p->bp++ = c;  
      return c;  
  }  

当Fmt_vstring完成时,buf字段指向的内存空间可能过长,这也是Fmt_vstring调用RESIZE释放过多空间的原因。

Fmt_vfmt是所有格式化函数的终点。它会解释格式串,并对每个格式限定符调用适当的转换函数。对格式串中的其他字符,它调用put函数:

functions

 159〉+≡  
  void Fmt_vfmt(int put(int c, void *cl), void *cl,  
      const char *fmt, va_list ap) {  
      assert(put);  
      assert(fmt);  
      while (*fmt)  
          if (*fmt != '%' || *++fmt == '%')  
              put((unsigned char)*fmt++, cl);  
          else  
             〈format an argument

 164〉  
  }  

<format an argument 164>代码块中的大部分工作,都是在逐一处理各个标志、字段宽度和精度设置,以及处理转换限定符没有对应的转换函数的可能性。在该代码块中(如下),width给出了字段宽度,而precision给出了精度。

format an argument

 164〉≡  
  {  
      unsigned char c, flags[256];  
      int width = INT_MIN, precision = INT_MIN;  
      memset(flags, '\0', sizeof flags);  
     〈get optional flags

 165〉  
     〈get optional field width

 165〉  
     〈get optional precision

 166〉  
      c = *fmt++;  
      assert(cvt[c]);  
      (*cvt[c])(c, &ap, put, cl, flags, width, precision);  
  }  

cvt是指向转换函数的指针的数组,它通过格式符索引。在上述的代码块中需要将c声明为unsigned char,这确保了将*fmt解释为0~255范围内的整数。

cvt初始化时,只设置了对应默认转换限定符的转换函数,假定字符使用ASCII编码:

data

 160〉+≡  
  static T cvt[256] = {  
  /*   0-  7 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*   8- 15 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  16- 23 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  24- 31 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  32- 39 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  40- 47 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  48- 55 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  56- 63 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  64- 71 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  72- 79 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  80- 87 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  88- 95 */ 0,     0, 0,     0,     0,     0,     0,     0,  
  /*  96-103 */ 0,     0, 0, cvt_c, cvt_d, cvt_f, cvt_f, cvt_f,  
  /* 104-111 */ 0,     0, 0,     0,     0,     0,     0, cvt_o,  
  /* 112-119 */ cvt_p, 0, 0, cvt_s,     0, cvt_u,     0,     0,  
  /* 120-127 */ cvt_x, 0, 0,     0,     0,     0,     0,     0  
};  

Fmt_register通过将cvt中适当的元素设置为相应的函数指针,来设置一个新的转换函数。它返回该元素的原值:

functions

 159〉+≡  
  T Fmt_register(int code, T newcvt) {  
      T old;  
 
      assert(0 < code  
          && code < (int)(sizeof (cvt)/sizeof (cvt[0])));  
      old = cvt[code];  
      cvt[code] = newcvt;  
      return old;  
  }  

扫描转换限定符的代码块遵循图14-1给出的语法,扫描过程中会逐次对fmt加1。第一个代码块处理标志:

data

 160〉+≡  
  char *Fmt_flags = "-+ 0";  
 
〈get optional flags

 165〉≡  
  if (Fmt_flags) {  
      unsigned char c = *fmt;  
      for ( ; c && strchr(Fmt_flags, c); c = *++fmt) {  
          assert(flags[c] < 255);  
          flags[c]++;  
      }  
  }  

接下来处理字段宽度:

get optional field width

 165〉≡  
  if (*fmt == '*' || isdigit(*fmt)) {  
      int n;  
     〈n ← next argument or scan digits

 165〉  
      width = n;  
  }  

宽度或精度设置中都可能出现星号,而在这种情况下下一个整数参数提供了对应的值。

〈n ← next argument or scan digits

 165〉≡  
  if (*fmt == '*') {  
      n = va_arg(ap, int);  
      assert(n != INT_MIN);  
      fmt++;  
  } else  
      for (n = 0; isdigit(*fmt); fmt++) {  
          int d = *fmt - '0';  
          assert(n <= (INT_MAX - d)/10);  
          n = 10*n + d;  
      }  

如该代码所示,在参数指定了宽度或精度时,其值不能为INT_MIN,该值是保留的,作为默认值。在宽度或精度显式给出时,它不能大于INT_MAX,这等效于约束10 * n + d≤ INT_MAX,即10 * n + d不会上溢。我们必须在不导致上溢的情况下进行该测试,这也是在上述的断言中将约束重写的原因。

句点表明接下来是一个可选的精度设置:

get optional precision

 166〉≡  
  if (*fmt == '.' && (*++fmt == '*' || isdigit(*fmt))) {  
      int n;  
     〈n ← next argument or scan digits

 165〉  
      precision = n;  
  }  

请注意,句点如果没有后接星号或数字,那么将处理以及解释为显式忽略的精度。

14.2.2 转换函数

cvt_s是对应于%s的转换函数,在14.1.2节给出。cvt_d是对应于%d的转换函数,在格式化数字的转换函数中具有代表性。它会获取整数参数,将其转换为无符号整数,并在局部缓冲区中生成适当的字符串(转换从最高有效位开始)。它接下来调用Fmt_putd输出字符串。

conversion functions

 159〉+≡  
  static void cvt_d(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      int val = va_arg(*app, int);  
      unsigned m;  
     〈declare

 buf and

 p, initialize

 p 166〉  
 
      if (val == INT_MIN)  
          m = INT_MAX + 1U;  
      else if (val < 0)  
          m = -val;  
      else  
          m = val;  
      do  
          *--p = m%10 + '0';  
      while ((m /= 10) > 0);  
      if (val < 0)  
          *--p = '-';  
      Fmt_putd(p, (buf + sizeof buf) - p, put, cl, flags,  
          width, precision);  
  }  
 
〈declare

 buf and

 p, initialize

 p 166〉≡  
  char buf[43];  
  char *p = buf + sizeof buf;  

cvt_d使用无符号算术的原因,与Atom_int相同,请参见3.2节,其中还解释了为何buf有43个字符。

functions

 159〉+≡  
  void Fmt_putd(const char *str, int len,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      int sign;  
 
      assert(str);  
      assert(len >= 0);  
      assert(flags);  
     〈normalize

 width and

 flags 159〉  
     〈compute the sign

 167〉  
      { 〈emit

 str justified in

 width 167〉 } 
  }  

Fmt_putd必须按照flags、width和precision的规定输出str中的字符串。如果精度已经给出,那么它指定了必须输出的最小位数。必须输出精度指定的那么多位数,这可能需要在前部补0。Fmt_putd首先确定是否需要输出符号或在前部添加空格,然后将sign设置给该字符:

compute the sign

 167〉≡  
  if (len > 0 && (*str == '-' || *str == '+')) {  
      sign = *str++;  
      len--;  
  } else if (flags['+'])  
      sign = '+';  
  else if (flags[' '])  
      sign = ' ';  
  else  
      sign = 0;  

<compute the sign 167>代码块中if语句的次序,实现了+标志优先于空格标志的规则。转换结果的长度n,取决于精度、被转换的值和符号:

emit

 str justified in

 width 167〉≡  
  int n;  
  if (precision < 0)  
      precision = 1;  
  if (len < precision)  
      n = precision;  
  else if (precision == 0 && len == 1 && str[0] == '0')  
      n = 0;  
  else  
      n = len;  
  if (sign)  
      n++;  

n被赋值为需要输出的字符数,该代码还处理了以精度0对值0进行转换的特例,在这种情况下转换结果没有输出字符。

如果输出是左对齐的,那么Fmt_putd现在可以输出符号,如果输出是右对齐的,需要前部补0,那么现在可以输出符号和填充字符,而如果输出是右对齐,需要在前部添加空格,那么可以输出填充字符和符号。

emit

 str justified in

 width 167〉+≡  
  if (flags['-']) {  
     〈emit the sign

 168〉  
  } else if (flags['0']) {  
     〈emit the sign

 168〉  
      pad(width - n, '0');  
  } else {  
      pad(width - n, ' ');  
     〈emit the sign

 168〉  
  }  
 
〈emit the sign

 168〉≡  
  if (sign)  
      put(sign, cl);  

Fmt_putd最后可以输出转换结果,这可能包括前部添加的0(为满足精度要求),以及填充字符(如果输出是左对齐的):

emit

 str justified in

 width 167〉+≡  
  pad(precision - len, '0');  
 〈emit

 str[0..len-1] 159〉  
  if (flags['-'])  
      pad(width - n, ' ');  

cvt_u比cvt_d简单,但它可以使用Fmt_putd输出转换结果的所有机制。它将输出下一个无符号整数的十进制表示:

conversion functions

 159〉+≡  
  static void cvt_u(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      unsigned m = va_arg(*app, unsigned);  
     〈declare

 buf and

 p, initialize

 p 166〉  
  
      do  
          *--p = m%10 + '0';  
      while ((m /= 10) > 0);  
      Fmt_putd(p, (buf + sizeof buf) - p, put, cl, flags,  
          width, precision);  
  }  

八进制和十六进制转换类似于无符号十进制转换,但输出的基不同,这又简化了转换的过程。

conversion functions

 159〉+≡  
  static void cvt_o(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      unsigned m = va_arg(*app, unsigned);  
     〈declare

 buf and

 p, initialize

 p 166〉  
 
      do  
          *--p = (m&0x7) + '0';  
      while ((m >>= 3) != 0);  
      Fmt_putd(p, (buf + sizeof buf) - p, put, cl, flags,  
          width, precision);  
  }  
 
static void cvt_x(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      unsigned m = va_arg(*app, unsigned);  
     〈declare

 buf and

 p, initialize

 p 166〉  
 
     〈emit

 m in hexadecimal

 169〉  
  }  
 
〈emit

 m in hexadecimal

 169〉≡  
  do  
      *--p = "0123456789abcdef"[m&0xf];  
  while ((m >>= 4) != 0);  
  Fmt_putd(p, (buf + sizeof buf) - p, put, cl, flags,  
      width, precision);  

cvt_p将指针作为十六进制数输出。精度和-以外的所有标志都忽略。参数被解释为指针,它首先被转换为unsigned long,因为unsigned的位宽可能不足以容纳指针 [1]

conversion functions

 159〉+≡  
  static void cvt_p(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      unsigned long m = (unsigned long)va_arg(*app, void*);  
     〈declare

 buf and

 p, initialize

 p 166〉  
 
      precision = INT_MIN;  
     〈emit

 m in hexadecimal

 169〉  
  }  

cvt_c是与%c相关的转换函数,它格式化输出一个字符,左对齐或右对齐width个字符。它忽略精度和其他标志。

conversion functions

 159〉+≡  
  static void cvt_c(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
     〈normalize

 width 160〉  
      if (!flags['-'])  
          pad(width - 1, ' ');  
      put((unsigned char)va_arg(*app, int), cl);  
      if ( flags['-'])  
        pad(width - 1, ' ');  
  }  

cvt_c获取的参数是一个整数而不是字符,因为通过参数列表的可变部分传递的字符参数,会经由默认的参数类型“提升”而转换为整数进行传递。cvt_c将由此得到的整数转换unsigned char,这样有符号、无符号和普通的字符都能够以同样的方式输出。

将浮点值精确地转换为十进制表示的过程,很难以与机器无关的方式完成。与机器相关的算法更快速且准确,因此与转换限定符e、f和g关联的转换函数使用了下述代码块:

format a

 double argument into

 buf 170〉≡  
  {  
      static char fmt[] = "%.dd?";  
      assert(precision <= 99);  
      fmt[4] = code;  
      fmt[3] =      precision%10 + '0';  
      fmt[2] = (precision/10)%10 + '0';  
      sprintf(buf, fmt, va_arg(*app, double));  
  }  

将val的绝对值转换到buf中,接下来输出buf。

浮点转换限定符之间的差别在于,它们格式化浮点值各部分的方式不同。限定符%.99f的输出最长,可能需要DBL_MAX_10_EXP+1+1+99+1个字符。DBL_MAX_10_EXP和DBL_MAX定义在标准头文件float.h中。DBL_MAX是可以表示为double的值中最大的值,而DBL_MAX_10_EXP是log10 DBL_MAX,即,它是可以通过double表示的最大的十进制指数值。对应IEEE 754格式下的64位double值,DBL_MAX是1.797693*10308 ,而DBL_MAX_10_EXP是308。对fmt[2]和fmt[3]的赋值假定使用了ASCII码。

因而,如果用转换限定符%.99f转换DBL_MAX,结果的数位情况是:小数点之前可能有DBL_MAX_10_EXP+1个数位、小数点、小数点之后可能有99个数位、结束的0字符。将精度限制为99,可以限制用于容纳转换结果的缓冲区的大小,使得缓冲区的最大长度在编译时已知。其他转换限定符%e和%g的转换结果,比%f的结果字符数要少。cvt_f处理所有三种格式码:

conversion functions

 159〉+≡  
  static void cvt_f(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision) {  
      char buf[DBL_MAX_10_EXP+1+1+99+1];  
 
      if (precision < 0)  
          precision = 6;  
      if (code == 'g' && precision == 0)  
          precision = 1;  
     〈format a

 double argument into

 buf 170〉  
      Fmt_putd(buf, strlen(buf), put, cl, flags,  
          width, precision);  
  }  

14.3 扩展阅读

[Plauger,1992]描述了C库中printf一族输出函数的实现,包括字符串与浮点值之间双向转换的底层代码。他的代码还说明了如何实现其他printf风格的格式化标志和格式码。

[Hennessy and Patterson,1994]一书的4.8节描述了IEEE 754浮点标准,以及浮点加法和乘法的实现。[Goldberg,1991]综述了程序员最关心的浮点运算性质。

浮点转换已经实现过多次,但转换得不精确或速度太慢,很容易使得这些转换变为拙劣的工作。对这些转换正确性的判断测试是:如果给定浮点值x,输出转换由x生成一个字符串,输入转换从该字符串重新创建一个浮点值y,那么要求x和y“按位”相等(即x和y的二进制表示是完全相同的)。[Clinger,1990]描述了如何精确地进行输入转换,该论文还阐明,对某些x,这种转换需要任意精度算术的支持。[Steele and White,1990]描述了如何进行精确的输出转换。

14.4 习题

14.1 Fmt_vstring使用RESIZE来释放它返回的字符串中不使用的部分。设计一种方法,仅在释放空间可以带来回报时进行释放操作,即被释放的空间值得上释放操作的代价。

14.2 使用[Steele and White,1990]中描述的算法实现e、f和g转换。

14.3 编写一个转换函数,从下一个整数参数获取转换限定符,并将该函数关联到@。例如,

Fmt_string("The offending value is %@\n", x.format, x.value);  

将根据x.format中的格式码来格式化x.value。

14.4 编写一个转换函数,将一个Bit_T中的元素以整数序列的形式输出,其中连续的1输出为范围表示,例如,1 32-45 68 70-71。


[1] 位宽是体系结构/编译器高度相关的,目前的主流编译器gcc/icc/msvc中,32位环境下,sizeof(int)==sizeof (long)==sizeof(void*)==4,64位环境下,sizeof(int)==sizeof(long)==4,sizeof(void*)==sizeof (long long)==8。——译者注