C陷阱与缺陷阅读笔记(下)

时间:2022-12-28 11:08:03

关于printf函数

printf函数将数据写到标准输出,fprintf函数将数据写到任何文件,sprintf函数格式化字符串。这三个函数的返回值都是已经处理的字符数。

sprintf函数作为输出数据结束标志的空字符不计入返回值,printf和fprintf函数在试图写入时出现的I/O错误,将返回一个负值,此时我们无法得出究竟有多少字符已经被写出。sprintf不进行I/O操作,因此不会返回负值。当然也不排除因为C语言实现的某种原因返回负值。

对printf所输出的字符串,遵循通常的C语言习惯,即以空字符结束,因此,要用双引号括起来,把这个字符串写成字符串常量的形式,能够自动保证它以空字符结尾。该函数把格式说明字符串中的字符逐一复制到标准输出中,直到格式字符串结束或者遇到%字符,此时printf查看%后紧跟的字符而不打印%字符,以获得有关如何转换其下一个参数的指示。

sprintf和printf的第一个参数不一样,其他都一样。

关于printf函数的参数,有如下说明:

%o,%x,%X都是以无符号数据打印的。

%s,格式项对于的输出字符串必须以空字符结束,舍此别无他法。如果不是以空字符结束,则printf函数将不断打印其后的字符,直到内存某处找到空字符,这种情况下,最终打印的数据将会是乱码,且可能会很长很长。

%g,用来打印那些不需要按列对齐的浮点数。即会舍弃数值尾的零,保留6位有效数字。如果实际数据超过6位有效数字,则用科学计数法表示。对于较小的数字,只有该数的指数小于等于5时,采用科学计数法处理。

   printf("%g\n", 3.14159e-4);0.000314159
   printf("%g\n", 3.14159e-5);3.14159e-005

%e,打印小数点后6位有效数字,用0填充。%f也是 如此。

修饰符

长度修饰符l只对用于整型的格式码有效。

宽度修饰符对所有的格式码都有效,甚至%%也不例外。如果不能填满设置的宽度域,左边用空格字符填充,超过则挤占同一行右侧紧邻数值的位置。

精度修饰符与格式码有关,对于整型,指定了打印的数字的最少位数。不够则补0,对于%e,%E,%f,表示小数点后的位数,%.0f,除非标志另有说明,否则不打印小数点。%g,%G精度修饰符指定了打印数值中的有效数字位数,除非标志另有说明,非有效数字的0被去掉,如果小数点后不跟数字,则小数点被删除。对于%s,精度修饰符指定将要从相应的字符串中打印的字符数。

标志符:

在显示宽度大于被显示的位数时,数据尾部都是以显示区的右端对齐的,标识符-的作用是将其改为左端对齐。因此,仅当域宽修饰符存在时,标识符-才有意义。

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

+和空白字符一起出现时,以+的作用为主。

标示符#的作用与具体的格式项有关:

给%o加上#的作用是,当有必要时增加数值的输出精度。

%#x,%#X,输出0xXX,,,0XXX,,,,

%#o,输出0XXX

%#f,要求小数点必须被打印出来,即使小数点后没有数字。

%#g,打印出的数值尾缀不会被去掉,共6位有效数字。

可变域宽与精度

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

%p,打印指针地址

%n,打印打印出的字符数。


可变参数列表设计

类似于printf函数的可变参数设计,其机制是类似的,只需要知道第一个参数的类型,就可以对其进行存取,一旦第n个参数被成功的存取,第n+1个参数就可以在仅知道类型的情况下进行存取,并且按这种方式存取一个参数的时间不应该太多。需要注意的是,对于参数的存取顺序,都没有必要,另外参数列表是否结束通常也不必要。

大多数C语言实现上述机制,都是通过一组varargs的宏定义。虽然这些宏的实现与特定的C语言实现有关,但是只要运用得到,还是能够在大多数机器上使用可变参数列表。

首先,包含这组宏:

#include <varargs.h>

这组宏包含:

va_list :该宏是用来定义一个数据类型,我们用该宏申明一个对象ap,则只需要给定ap与第1个参数的类型就可以确定第1个参数的值。实际上,在任何一个C实现中,对于可变参数列表的第n个参数,在已知其类型的情况下,要对其进行存取还需要知道一些信息,这些信息通过已经可以存取的第一个参数到第n-1个参数而间接得到,因此可以把ap看成是指向参数列表内部的指针。通过va_list存取一个参数后,va_list被更新,指向下一个参数。

va_start:初始化ap,即指向参数列表头

va_end:清空ap

va_arg:获取参数

va_alist:该宏将扩展为特定C实现所要求的参数列表,这样函数就能够处理变长参数。

va_dcl:va_alist 的声明(UNIX version only) ,即将扩展为与参数列表对应的申明。

也就是说:

如果要定义一个可变参数的函数,则定义如下:

int fun(int first, ...); ANSI 版本

int fun(va_alist) va_dcl ;UNIX版本

这里举例说明:

#define END -1

int va_sum (int first_num, ...)
{
    // (1) 定义参数列表
    va_list ap;
    // (2) 初始化参数列表
    va_start(ap, first_num);

    int result = first_num;
    int temp = 0;
    // 获取参数值
    while ((temp = va_arg(ap, int)) != END) //va_arg 用于对一个参数进行存取,它的两个参数为va_list变量名和希望存取的参数类型,va_arg将取得这个参数,并更新va_list指向下一个参数、
    {
        result += temp;
    }

    // 关闭参数列表,一定要调用该宏,在大多数C实现上,调用va_end与否并无区别,但某些机器上,为了方便va_list遍历,实际采用动态分配内存。
    va_end(ap);

    return result;
}

vprintf,vfprintf,vsprintf与相应的函数在使用方式上类似,只是用va_list替换了格式字符串后的参数序列。用法如下:

char *format;

va_start(ap);

format = va_arg(ap, char *);

fprintf(stderr, "error: ");

vfprintf(stderr, format, ap);

va_end(ap);


实现varargs.h

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]

因此,

int fun(va_alist) va_dcl ;

被扩展为:

int fun(va_alist)  int va_alist ;

实际上这种实现方式隐含了要求函数参数在内存中连续储存。

va_arg有个陷阱要注意:

第2个参数不能用char ,short, float类型,因为char short类型被自动换成int类型,float会被换成double类型。


ANSI 版varargs.h

ANSIC对varargs.h 做了一种扩展,但前者能够运行的环境更多,可移植性更强,ANSIC的头文件为stdarg.h

其用法见例子,stdarg.h中并没有va_alist,va_dcl宏。