C陷阱与缺陷(四)PRINTF , VARARGS与 STDARG、Koenig和 Moo夫妇访谈

C陷阱与缺陷(四)PRINTF , VARARGS与 STDARG、Koenig和 Moo夫妇访谈,第1张

附录A:PRINTF , VARARGSSTDARG A.1 printf 函数族 简单格式类型

        因为格式字符串决定了其余参数的类型,而且可以到运行时才建立格式字符串,所以C语言实现要检查printf 函数的参数类型是否正确是异常困难的。

        其中的%d格式项将被对应的待输出整数的10进制值替换,替换时不会在整数值的前后添加空格字符。

        %u格式项与%d格式项类似,只不过要求打印无符号10进制整数。

        %o、%x和%X格式项用于打印8进制或16进制的整数。

        %s格式项所对应输出的字符串必须以一个空字符('\0')作为结束标志

        %og、%f 和%e这3个格式项用于打印浮点值。

        %g格式项用于打印那些不需要按列对齐的浮点数特别有用。它在打印出对应的数值(必须为浮点型或双精度类型)时,会去掉该数值尾缀的零,保留六位有效数字。

        占用的空间大小相同。对于比较小的数值,除非该数的指数小于或等于-5,%g格式项才会采用科学计数法来表示。

        %e格式项用于打印浮点数时,要求一律显式地使用指数形式:π在使用%e格式项时将被写成3.141593e+00。%e格式项将打印出小数点后6位有效数字,而并非如%g 格式项打印出的数是总共6位有效数字

        %f格式项则恰好相反,强制禁止使用指数形式来表示浮点数,因此π就被写成3.141593。在数值精度方面,%f格式项的要求与%e格式项相同,即小数点后6位有效数字。

        %E和%G格式项与它们对应的%e和%g格式项在行为方式上基本相同,除了用大写的E代替了小写的e来表示指数形式。

        %%格式项用于打印出一个%字符。

修饰符

        整数有3种不同类型,对应3种不同长度:short,long和正常长度。如果一个short整数作为任何一个函数(也包括printf函数)的参数出现,它会被自动地扩展为一个正常长度的整数。但是,我们仍然需要一种方式,来通知printf 函数某个参数是long型整数。我们可以在格式码之前紧挨着插入一个长度修饰符l,创造出%ld、% lo、%lx和%lu作为新的格式码。

        利用宽度修饰符,我们可以轻松做到在固定长度的域内打印数值。宽度修饰符出现在%符号和格式码的中间,其作用是指定它所修饰的格式项所应打印的字符数。如果待打印的数值不能填满位置,它的左侧就会被补上空格字符以使这个数值的宽度满足要求。

        精度修饰符的确切含义与格式码有关:

        对于整数格式项%d、%o、%x和%u,精度修饰符指定了打印数字的最少位数。如果待打印的数值并不需要这么多位数的数字来表示,就会在它的前面补上0。因此,

        对于%e、%E和%f 格式项,精度修饰符指定了小数点后应该出现的数字位数。除非标志另有说明,仅当精度大于0时打印的数值中才会实际出现小数点。

        对于%g 和%G格式项,精度修饰符指定了打印数值中的有效数字位数。除非标志另有说明,非有效数字的0将被去掉,如果小数点后不跟数字则小数点也将被删除。

        对于%s格式项,精度修饰符指定了将要从相应的字符串中打印的字符数。如果该字符串中包含的字符数少于精度修饰符所指定的字符数,输出的字符数就会少于精度修饰符指定的数目。如果需要,我们可以通过域宽修饰符来加长输出。

        对于%c和%%格式项,精度修饰符将被忽略。

标志

        在显示宽度大于被显示位数时,数据尾部都以显示区的右端对齐,左端被填充空白字符。标志字符-的作用是,要求显示方式改为左端对齐,在右端填乞空白字符。因此,仅当域宽修饰符存在时,标志字符 --- 才有意义。(否则,填充己白字符就无从说起。)

        标志字符+的作用是,规定每个待打印的数值在输出时都应该以它的符号(正号或负号)作为第一个字符。

        空白字符作为标志字符时,它的含义是:如果某数是一个非负数,就在它的前面插入一个空白字符。

        如果我们希望在固定栏内按科学计数法打印数值,格式项% e和%+e要比正常的格式项%e 有用得多。因为,这时出现在非负数前面的正号(或者空白)保证了所有输出数值的小数点都会对齐。

        标志字符#的作用是对数值输出的格式进行微调,具体的方式与特定格式项有关。给%o格式项加上标志字符#的效果是:当有必要时增加数值输出的精度(这只需让输出的第1个数字为0就已经做到了)。这么规定的意义在于,让八进制数值输出的格式与大多数C程序员惯用的形式一致。#o与0%o并不相同,因为0%o把数值0打印成00,而%#o 的打印结果是0。同理,格式项t#x与%#x要求打印出来的16进制数值前面分别加上0x或0X。

        标志字符#对浮点数格式的影响有两方面:其一,它要求小数点必须被打印出来,即使小数点后没有数字也是如此;其二,如果用于%g 或%G格式项,打印出的数值尾缀的0将不会被去掉。

可变域宽与精度

        考虑到这些,printf 函数因此允许间接指定域宽和精度。要做到这一点,我们只需用*替换域宽修饰符或精度修饰符其中之一,或者两者都替换。在这种情况下,printf函数首先从参数列表中取得将要使用的域宽或精度的实际数值,然后使用该数值来完成打印任务。因此,上面的例子可以写成这样:

                 printf ( " ... %.*s ...", ... , NAMES1ZE,name,... );

        如果我们使用*同时替换域宽修饰符与精度修饰符,那么后面的参数列表中将依次出现代表域宽的参数、代表精度的参数以及代表要打印的值的参数。因此,

                printf ("%*.*s\n" , 12, 5, str) ;

        与下式完全等效

                printf ("%12.5s\n", str) ;

新增的格式码

        ANSIC标准的定义中新增了两个格式码:%p和%n。%p用于以某种形式打印一个指针,具体的形式与特定的C语言实现有关(译注:一般是打印出该指针所指向的地址)。%n用于指出已经打印的字符数,这个数被存储在对应参数(一个整型指针)所指向的整数中。执行完以下代码之后,

                int n;

                printf ( "hello\n%n" , &n);

        n的值就是6。

废止的格式码

%D和%O格式项曾经与%1d和%1o的含义相同。

A.2使用varargs.h来实现可变参数列表

        大多数C语言实现都是通过一组总称为 varargs 的宏定义来达到上述目的。这些宏的确切性质虽然与特定的C语言实现有关,但是只要我们在程序中运用得当,还是能够在相当多的机型上使用可变参数列表。

                #include

        在程序中把相关的宏定义包括进来。varargs.h头文件中定义了宏名va_list,va_dcl,va_start,va_end 以及va_arg。va_alist一般由编程者来定义,我们马上将讨论如何来做。需要强调,应该避免混淆va_list 与 va_alist。

        这些信息存储在一个类型为va_list 的对象中。因此,当我们声明了一个名称为ap的类型为va_list的对象后,只需要给定ap与第1个参数的类型就可以确定第1个参数的值。

        因为一个va_list 中包括了存取全部参数的所有必要信息,函数f可以为它的参数创建一个va_list,然后把它传递给另一个函数g。这样,函数g就能够访问到函数f的参数。

        被调用时带有可变参数列表的函数,必须在函数定义的首部使用va_alist 和va_dcl宏。如下所示:

                #include

                void error (va_alist ) va_dcl

        宏va_alist将扩展为特定C实现所要求的参数列表,这样函数就能够处理变长参数。而宏va_dcl将扩展为与参数列表对应的声明,必要时还包括一个作为语句结束标志的分号。

#include 

void error (va_alist) va_dcl

{

    va_list ap;

    va_start (ap) ;

    //这里是使用ap 的程序部分

    va_end (ap) ;

    //这里是不使用ap的其他程序部分

}

        宏va_arg用于对一个参数进行存取。它的两个参数分别为va_list变量名和希望存取的参数的数据类型。va_list宏将取得这个参数,并更新va_list变量,使其指向下一个参数。

#include 

void error (va_alist) va_dcl

{

    va_list ap;

    char *format ;

    va_start (ap) ;

    format = va_arg (ap, char * ) ;

    fprintf (stderr, "error : " );

    // (do something magic) //某些实现方式暂时未知的工作

    va_end (ap) ;

    fprintf (stderr, " \n" ) ;

    exit (1) ;

}

        幸运的是,ANSI C标准要求,而且很多C语言实现也提供了,分别称为vprintf、vfprintf和 vsprintf 的函数。这些函数与对应的printf函数族中的函数在行为方式上完全相同,只不过用va_list替换了格式字符串后的参数序列。这些函数之所以能够存在,理由有两个:其一,va_list变量可以作为参数传递;其二,va_arg宏可以独立出现在一个函数中,并不强制要求与 va_start宏(该宏的作用是初始化va_list变量)成对使用。

#include 

#include 

void error (va_alist) va_dcl

{

    va_list ap;

    char * format ;

    va_start (ap) ;

    format = va_arg (ap, char * );

    fprintf (stderr, "error: " ) ;

    vfprintf (stderr, format, ap);

    va_end (ap) ;

    fprintf (stderr, "\n" ) ;

    exit (1) ;

}

        下面还有一个例子,我们将演示利用vprintf 来实现 printf 函数的一种可行方式。注意,不要忘记保存vprintf 函数的结果,我们需要把这个结果返回给printf函数的调用者。

#include 

int printf (va_alist ) va_dcl

{

    va_list ap;

    char *format ;
    
    int n;

    va_start (ap) ;

    format = va_arg (ap, char * );

    n = vprintf (format, ap) ;

    va_end (ap) ;

    return n;

}
实现varargs.h

varargs.h 的一个典型实现包括一组宏,以及一个va_list 的typedef声明:

                typedef char *va_list ;

                #define va_dcl int va_alist;

                #define va_start (list) list = (char * ) &va_alist

                #define va_end (list)

                #define va_arg (list , mode) \

                ( (mode *)( list += sizeof (mode) ) ) [-1]

我们首先注意到,在这个版本的 varargs.h中,va_alist甚至不是一个宏:

                #include

                void error (va_alist) va_dcl

        将扩展为:

                typedef char *va_list ;

                void error (va_alist) int va_alist;

        因此,一个接受可变参数列表的函数表面上看来只有一个名称为va_alist 的 int型参数。

        这个例子实际上隐含了如下假定:底层的C语言实现要求函数参数在内存中连续存储,这样我们只需知道当前参数的地址,就能依次访问参数列表中的其他参数。因此, varargs.h的这个实现中,va_list就只是一个简单的字符指针。宏va_start把它的参数设置为va_alist 的地址(为避免 lint程序的警告,这里做了类型转换)。而宏va_end则什么也不做。

        最复杂的宏是va_arg。它必须返回一个由 va_list所指向的恰当类型的数值,同时递增va_list,使它指向参数列表中的下一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标(译注:即只能先赋值再作类型转换,而不能先类型转换再赋值),所以va_arg宏首先使用sizeof 来确定需要递增的大小,然后直接把它加到va_list 上,这样得到的指针再被转换为要求的类型。因为该指针现在指向的位置“过”了一个类型单位的大小,所以我们使用了下标-1来存取正确的返回参数。

        这里有一个“陷阱”需要避免:va_arg宏的第二个参数不能被指定为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类型的数值才会被转换。

        我们还应该注意到,不存在任何内建的方式来得知给定的参数数目。使用varargs系列宏的每个程序,都有责任通过确立某种约定或惯例来标志参数列表的结束。例如,printf 函数使用格式字符串作为第一个参数,来确定其余参数的数目与类型。

A.3 stdarg.h: ANSI版的varargs.h

        头文件 varargs.h中系列宏的历史最早可追溯到1981年,因此许多C语言实现都对其提供支持。然而,ANSI C标准却包括了另-种不同的机制(称为stdarg.h),来处理可变参数列表。

        在符合ANSI C标准的编译器中包括varargs.h作为功能上的一种扩展,这是个不错的主意,可以让早期的程序继续运行。因此,在编程实践中,使用varargs.h的程序比使用stdarg.h 的程序可移植性要强,能够运行其上的系统平台也要多些。但如果你要编写一个遵循ANSI C标准的程序,就必须使用stdarg.h,而且别无他选!这是一个让人左右为难的情形,不管作出何种选择,都必须付出相应代价。

        具有可变参数列表的函数,它们的第1个参数的类型在每次调用时实际上都是不变的。varargs.h和 stdarg.h的主要区别就来自于这一事实。类似printf 这样的函数,可以通过检查它的第1个参数,来确定它的第2个参数的类型。但是,从参数列表中我们却不能找到任何信息用以确定第1个参数的类型。因此,使用stdarg.h 的函数必须至少有一个固定类型的参数,后面可以跟一组未知数目、未知类型的参数。

        作为一个现成的例子,让我们再来看一下error 函数。它的第1个参数就是printf 函数中的格式字符串,为字符指针类型。因此,error函数可以如下声明:

                 void error (char * , ...) ;

        那么error函数的定义又是怎样呢? stdarg.h 头文件中并没有 varargs.h 中的va_arg和va_dcl宏。使用stdarg.h 的函数直接声明其固定参数,把最后一个固定参数作为va_start宏的参数,即以固定参数作为可变参数的基础。因此,error函数的定义如下所示:

#include  #include 

void error (char *format, ...)

{

    va_list ap;

    va_start (ap, format) ;
    
    fprintf (stderr, "error: " );
    
    vfprintf (stderr, format, ap) ;

    va_end (ap) ;

    fprintf (stderr, "\n" );

    exit (1);

}

        本例中,我们无需使用va_arg 宏,因为此处格式字符串属于参数列表的固定部分。

        作为另一个例子,下面演示了如何使用stdarg.h来编写printf(其中用到了vprintf):

#include 

int printf (char * format, ...)

{

    va_list ap;int n;

    va_start (ap , format) ;

    n = vprintf ( format , ap) ;

    va_end (ap ) ;

    return n;

}
附录B:Koenig和 Moo夫妇访谈 1、库优于语言细节

两个原因:

        首先,学生们可以不必费力包装低层次的语言细节,从而更容易建立整体语言的全局观念,了解到其真实威力。根据我们的经验,学生们首先掌握如何使用程序库之后,就会很容易理解类的概念,学会如何构造类的技术。如果首先去学习语言细节,那么就很难理解类的概念及其功能。这种理解上的缺陷,使他们很难设计和构造自己的类。

        不过,更重要的一点是,首先学习程序库,能够使学生培养起良好的习惯,就是复用库代码,而不是凡事自己动手。首先学习语言细节的学生,最后的编程风格往往是C类型的,而不是C++风格。他们不会充分地运用库,而自己的程序带有严重的C主义倾向——指针满天飞,整个程序都是低层次的。结果是,在很多情况下,你为C++的复杂性付出了高昂代价,却没有从中获得任何好处。

2、两句表达C++
  1. 用类来表示概念
  2. 避免重复。如果你发现自己在程序的两个不同部分里做了相同的事情,试着把这两个部分合并到一个子过程中。如果你发现两个类的行为相近,试着把这两个类的相似部分统一到基类或模板中。
3、最重要的建议
  1. 避免使用指针
  2. 提倡使用程序库
  3. 使用类来表示概念

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1325562.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存