0%

《C Primer Plus 第六版》读书笔记 - 第十三至十五章

  • 文件输入/输出
  • 结构和其他数据形式
  • 位操作

第13章 文件输入/输出

13.1 与文件进行通信

13.1.2 文本模式和二进制模式

为了规范文本文件的处理,C提供两种访问文件的途径:二进制模式和文本模式。在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。

除了以文本模式读写文本文件,还能以二进制模式读写文本文件。

13.1.3 I/O的级别

除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别)。底层I/O(low-level I/O)使用操作系统提供的基本I/O服务。标准高级I/O(standard high-level I/O)使用C库的标准包和stdio.h头文件定义。因为无法保证所有的操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包

C程序会自动打开3个文件,它们被称为标准输入(standard input)、标准输出(standard ouput)和标准错误输出(standard error output)。在默认情况下,标准输入是系统的普通输入设备,通常为键盘:标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。通常,标准输入为程序提供输入,它是getchar()和scanf()使用的文件。程序通常输出到标准输出,它是putchar()、puts()和printf()使用的文件。第8章提到的重定向把其他文件视为标准输入或标准输出。标准错误输出提供了一个逻辑上不同的地方来发送错误消息。例如,如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容仍然会被发送到屏幕上。这样很好,因为如果把错误消息发送至文件,就只能打开文件才能看到

13.2 标准I/O

与底层I/O相比,标准I/O包除了可移植以外还有两个好处。第一,标准I/O有许多专门的函数简化了处理不同I/O的问题。

第二,输入和输出都是缓冲的。也就是说,一次转移一大块信息而不是一字节信息(通常至少512字节)。

13.2.1 检查命令行函数

字符串argv[0]是该程序的名称。显式使用argv[0]而不是程序名,错误消息的描述会随可执行文件名的改变而自动改变。这一特性在像UNIX这种允许单个文件具有多个文件名的环境中也很方便。但是,一些操作系统可能不识别argv[0],所以这种用法并非完全可移植。

exit()函数关闭所有打开的文件并结東程序。exit()的参数被传递给一些操作系统,包括UNIX、Linux、Windows和MS-DOS,以供其他程序使用。

标准要求0或宏EXIT_SUCCESS用于表明成功结束程序,宏EXIT_FAILURE用于表明结束程序失败。这些宏和exit()原型都位于stdlib.h头文件中

13.2.2 fopen()函数

使用fopen()函数打开文件。该函数声明在stdio.h中。它的第1个参数是待打开文件的名称,更确切地说是一个包含改文件名的字符串地址。第2个参数是一个字符串,指定待打开文件的模式。

像UNIX和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同。

新的C11新增了带x字母的写模式,与以前的写模式相比具有更多特性。第一,如果以传统的一种写模式打开一个现有文件,fopen()会把该文件的长度截为0,这样就丢失了该文件的内容。但是使用带字母的写模式,即使fopen()操作失败,原文件的内容也不会被删除。第二,如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件。

如果使用任何一种"w"模式(不带X字母)打开一个现有文件,该文件的内容会被删除,以便程序在一个空白文件中开始操作。然而,如果使用带x字母的任何一种模式,将无法打开一个现有文件.

程序成功打开文件后,fopen()将返回文件指针(file pointer),其他I/O函数可以使用这个指针指定该文件。文件指针的类型是指向FILE的指针,FILE是一个定义在stdio.h中的派生类型。文件指针fp并不指向实际的文件,它指向一个包含文件信息的数据对象,其中包含操作文件的I/O函数所用的缓冲区信息。因为标准库中的I/O函数使用缓冲区,所以它们不仅要知道缓冲区的位置,还要知道缓冲区被填充的程度以及操作哪一个文件。标准I/O函数根据这些信息在必要时决定再次填充或清空缓冲区。fp指向的数据对象包含了这些信息

13.2.3 getc()和putc()函数

下面这条语句的意思是“从fp指定的文件中获取一个字符”:

1
ch = getc(fp);
与此类似,下面语句的意思是“把字符ch放入FILE指针fpout指定的文件中”:
1
putc(ch, fpout);
在putc()函数的参数列表中,第1个参数是待写入的字符,第2个参数是文件指针。

13.2.4 文件结尾

C程序只有在读到超过文件末尾时才会发现文件的结尾

为了避免读到空文件,应该使用入口条件循环(不是do while循环)进行文件输入。鉴于getc()(和其他C输入函数)的设计,程序应该在进入循环体之前先尝试读取。

13.2.5 fclose()函数

fclose(fp)函数关闭fp指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件。如果成功关闭,fclose()函数返回0,否则返回EOF

如果磁盘已满、移动硬盘被移除或出现I/O错误,都会导致调用fclose()函数失败

13.2.6 指向标准文件的指针

stdio.h头文件把3个文件指针与3个标准文件相关联,C程序会自动打开这3个标准文件。

这些文件指针都是指向FILE的指针,所以它们可用作标准I/O函数的参数

13.4 文件I/O:fprintf()、fscanf()、fgets()和fputs()

13.4.1 fprintf()和fscanf()函数

文件I/O函数fprintf()和fscanf()函数的工作方式与printf()和scanf()类似,区别在于前者需要用第1个参数指定待处理的文件。

13.4.2 fgets()和fputs()函数

fgets()函数的第1个参数和gets()函数一样,也是表示储存输入位置的地址(char * 类型);第2个参数是一个整数,表示待输入字符串的大小:最后一个参数是文件指针,指定待读取的文件

fgets()函数读取输入直到第1个换行符的后面,或读到文件结尾,或者读取STLEN-1个字符(以上面的fgets()为例)。然后,fgets()在末尾添加一个空字符使之成为一个字符串。字符串的大小是其符数加上一个空字符。如果fgets()在读到字符上限之前已读完一整行,它会把表示行结尾的换行符放在空字符前面。fgets()函数在遇到EOF时将返回NULL值,可以利用这一机制检查是否到达文件结尾;如果未遇到EOF则之前返回传给它的地址。

fputs()函数接受两个参数:第1个是字符串的地址:第2个是文件指针。该函数根据传入地址找到的字符串写入指定的文件中。和puts()函数不同,fputs()在打印字符串时不会在其末尾添加换行符。

13.5 随机访问:fseek()和ftell()

13.5.1 fseek()和ftell()的工作原理

fsek()的第1个参数是FILE指针,指向待査找的文件,fopen()应该已打开该文件。

fseek()的第2个参数是偏移量(offset)。该参数表示从起始点开始要移动的距离(参见表13.3列出的起始点模式)。该参数必须是一个long类型的值,可以为正(前移)、负(后移)或0(保持不动)。

fseek()的第3个参数是模式,该参数确定起始点。根据ANSI标准,在stdio.h头文件中规定了几个表示模式的明示常量(manifest constant),如表13.3所示。

旧的实现可能缺少这些定义,可以使用数值0L、1L、2L分别表示这3种模式。L后缀表明其值是long类型。

下面是调用fseek()函数的一些示例,fp是一个文件指针:

1
2
3
4
5
fseek(fp, 0L, SEEK_SET); //定位至文件开始处
fseek(fp, 10L, SEEK_SET); //定位至文件中的第10个字节
fseek(fp, 2L, SEEK_CUR); //从文件当前位置前移2个字节
fseek(fp, 0L, SEEK_END); //定位至文件结尾
fseek(fp, -10L, SEEK_END); //从文件结尾处回退10个字节

如果一切正常,fseek()的返回值为0:如果出现错误(如试图移动的距离超出文件的范围),其返回值为-1。

ftell()函数的返回类型是long,它返回的是当前的位置。ANSI C把它定义在stdio.h中。在最初实现的UNIX中,ftell()通过返回距文件开始处的字节数来确定文件的位置。文件的第1个字节到文件开始处的距离是0,

13.5.2 二进制模式和文本模式

许多MS-DOS编辑器都用Ctrl+Z标记文本文件的结尾。以文本模式打开这样的文件时,C能识别这个作为文件结尾标记的字符。但是,以二进制模式打开相同的文件时,Ctrl+Z字符被看作是文件中的一个字符,而实际的文件结尾符在该字符的后面。文件结尾符可能紧跟在Ctrl+Z字符后面,或者文件中可能用空字符填充,使该文件的大小是256的倍数

二进制模式和文本模式的另一个不同之处是:MS-DOS用。以文本模式打开相同的文件时,C程序把“看成”。但是,以二进制模式打开该文件时,程序能看见这两个字符。

ftell()函数在文本模式和二进制模式中的工作方式不同。许多系统的文本文件格式与UNIX的模型有很大不同,导致从文件开始处统计的字节数成为一个毫无意义的值。ANSI C规定,对于文本模式,ftell(返回的值可以作为fseek()的第2个参数。对于MS-DOS,ftell()返回的值把。

13.5.4 fgetpos()和fsetpos()函数

ANSI C新增了两个处理较大文件的新定位函数:fgetpos()和fsetpos()。这两个函数不使用long类型的值表示位置,它们使用一种新类型:fpos_t(代表file position type,文件定位类型)。fpos_t类型不是基本类型,它根据其他类型来定义。fpos_t类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型,除此之外,没有其他限制

ANSI C定义了如何使用fpos_t类型。fgetpos()函数的原型如下:

1
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
调用该函数时,它把fpos_t类型的值放在pos指向的位置上,该值描述了文件中的一个位置。如果成功,fgetpos()函数返回0:如果失败,返回非0。

fsetpos()函数的原型如下:

1
int fsetpos(FILE * stream, const fpos_t * pos);
调用该函数时,使用pos指向位置上的fpos_t类型值来设置文件指针指向该值指定的位置。如果成功,fsetpos()函数返回0:如果失败,则返回非0。fpos_t类型的值应通过之前调用fgetpos()获得。

13.6 标准I/O的机理

通常,使用标准I/O的第1步是调用fopen()打开文件(前面介绍过,C程序会自动打开3种标准文件)。fopen()函数不仅打开一个文件,还创建了一个缓冲区(在读写模式下会创建两个缓冲区)以及一个包含文件和缓冲区数据的结构。另外,fopen()返回一个指向该结构的指针,以便其他函数知道如何找到该结构。假设把该指针赋给一个指针变量fp,我们说fopen()函数“打开一个流”。如果以文本模式打开该文件,就获得一个文本流:如果以二进制模式打开该文件,就获得一个二进制流。

这个结构通常包含一个指定流中当前位置的文件位置指示器。除此之外,它还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。

我们主要考虑文件输入。通常,使用标准I/O的第2步是调用一个定义在stdio.h中的输入函数,如fscanf()、getc()或fgets()。调用这些函数,文件中的数据块就被拷贝到缓冲区中。缓冲区的大小因实现而异,一般是512字节或是它的倍数,如4096或16384(随着计算机硬盘容量越来越大,缓冲区的大小也越来越大)。最初调用函数,除了填充缓冲区外,还要设置fp所指向的结构中的值。尤其要设置流中的当前位置和拷贝进缓冲区的字节数。通常,当前位置从字节0开始。

在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据。在它读取数据时,文件位置指示器被设置为指向刚读取字符的下一个字符。由于stdio.h系列的所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始。

当输入函数发现已读完缓冲区中的所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。以这种方式,输入函数可以读取文件中的所有内容,直到文件结尾。函数在读取缓冲区中的最后一个字符后,把结尾指示器设置为真。于是,下一次被调用的输入函数将返回EOF。

输出函数以类似的方式把数据写入缓冲区。当缓冲区被填满时,数据将被拷贝至文件中。

13.7 其他标准I/O函数

13.7.1 int ungetc(int c, FILE *fp)函数

int ungetc()函数把c指定的字符放回输入流中。如果把一个字符放回输入流,下次调用标准输入函数时将读取该字符

ANSI C标准保证每次只会放回一个字符。如果实现允许把一行中的多个字符放回输入流,那么下一次输入函数读入的字符顺序与放回时的顺序相反。

13.7.2 int fflish()函数

fflush()函数的原型如下:

1
int fflush(FILE * fp);
调用fflush()函数引起输出缓冲区中所有的未写入数据被发送到fp指定的输出文件。这个过程称为刷新缓冲区。如果fp是空指针,所有输出缓冲区都被刷新。在输入流中使用fflush()函数的效果是未定义的。只要最近一次操作不是输入操作,就可以用该函数来更新流(任何读写模式)

13.7.3 int setvbuf()函数

setvbuf()函数的原型是:

1
int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size);
setvbuf()函数创建了一个供标准I/O函数替换使用的缓冲区。在打开文件后且未对流进行其他操作之前,调用该函数。指针fp识别待处理的流,buf指向待使用的存储区。如果buf的值不是NULL,则必须创建一个缓冲区。

变量size告诉setvbuf()数组的大小。mode的选择如下:_IOFBE表示完全缓冲(在缓冲区满时刷新);_IOLBF表示行缓冲(在缓冲区满时或写入一个换行符时);_IONBF表示无缓冲。如果操作成功,函数返回0,否则返回一个非零值。

13.7.4 二进制I/O:fread()和fwrite()

如果以程序所用的表示法把数据储存在文件中,则称以二进制形式储存数据。不存在从数值形式到字符串的转换过程。对于标准I/O,fread()和fwrite()函数用于以二进制形式处理数据

13.7.5 size_t fwrite()函数

fwrite()函数的原型如下:

1
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
指针ptr是待写入数据块的地址。size表示待写入数据块的大小(以字节为单位),nmemb表示待写入数据块的数量。和其他函数一样,fp指定待写入的文件。

fwrite()函数返回成功写入项的数量。正常情况下,该返回值就是nmemb,但如果出现写入错误返回值会比nmemb小。

13.7.6 size_t fread()函数

size_t fread()函数的原型如下:

1
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
fread()函数接受的参数和fwrite()函数相同。在fread()函数中,ptr是待读取文件数据在内存中的地址,fp指定待读取的文件。该函数用于读取被fwrite()写入文件的数据。

fread()函数返回成功读取项的数量。正常情况下,该返回值就是nmemb,但如果出现读取错误或读到文件结尾,该返回值就会比nmemb小

13.7.7 int feof(FILE * fp)和int ferror(FILE * fp)函数

如果标准输入函数返回EOF,则通常表明函数己到达文件结尾。然而,出现读取错误时,函数也会返回EOF。feof()和ferror()函数用于区分这两种情况。当上一次输入调用检测到文件结尾时,feof()函数返回一个非零值,否则返回0。当读或写出现错误,ferror()函数返回一个非零值,査则返回0。

第14章 结构和其他数据形式

14.3 定义结构变量

结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。程序中创建结构变量的一行是:

1
struct book library;
声明结构的过程和定义结构变量的过程可以组合成一个步骤。如下所示,组合后的结构声明和结构变量定义不需要使用结构标记:
1
2
3
4
5
struct { /*无结构标记*/
char title[MAXTITL];
char author[MAXAUTL];
float value;
} library;
然而,如果打算多次使用结构模板,就要使用带标记的形式:或者,使用本章后面介绍的typedef。

14.3.1 初始化结构

初始化一个结构变量(ANSI之前,不能用自动变量初始化结构:ANSI之后可以用任意存储类别)与初始化数组的语法类似:

1
2
3
4
5
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用退号分隔。

14.3.3 结构的初始化器

C99和C11为结构提供了指定初始化器(designated initializer),其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素。例如,只初始化book结构的value成员,可以这样做:

1
struct book surprise = { .value = 10.99};
可以按照任意顺序使用指定初始化器:
1
2
3
struct book gift = { .value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad"};
与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值。

14.6 指向结构的指针

14.6.1 声明和初始化结构指针

和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符

在有些系统中,一个结构的大小可能大于它各成员大小之和。这是因为系统对数据进行校准的过程中产生了一些“缝隙”。

14.6.2 用指针访问成员

第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系

如果him==&barney,那么him->income即是barney.income

如果him==&fellow[0],那么him->income即是fellow[0].income

换句话说,->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.incone,因为him不是结构名)。

这里要着重理解him是一个指针,但是him->income是该指针所指向结构的一个成员。所以在该例中,him->income是一个float类型的变量。

第2种方法是,以这样的顺序指定结构成员的值:如果him==fellow[0],那么*him==fellow[0],因为&和*是一对互逆运算符。因此,可以做以下替代:

1
fellow[0].income == (*him).income
必须要使用圆括号,因为.运算符比*运算符的优先级高。

14.7 向函数传递结构的信息

14.7.1 传递结构成员

ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。

14.7.4 其他结构特性

现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也就是说,如果n_data和o_data都是相同类型的结构,可以这样做:

1
o_data = n_data;//把一个结构赋值给另一个结构
这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一个结构:
1
2
struct names right_field = {"Ruthie", "George"};
struct names captain= right_field;//把一个结构初始化为另一个结构
现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数:把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信,因此可以选择任一种方法来解决编程问题

14.7.5 结构和结构指针的选择

把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C新增的const限定符解决了这个问题。

把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。

传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。

14.7.8 复合字面量和结构(C99)

复合字面量在所有函数的外部,具有静态存储期:如果复合字面量在块中,则具有自动存储期。复合字面量和普通初始化列表的语法规则相同。这意味着,可以在复合字面量中使用指定初始化器。

14.7.9 伸缩型数组成员(C99)

利用这项特性声明的结构,其最后个数组成员具有一些特性。第1个特性是,该数组不会立即存在。第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。

首先,声明一个伸缩型数组成员有如下规则: 伸缩型数组成员必须是结构的最后一个成员结构中必须至少有一个成员: 伸缩数组的声明类似于普通数组,只是它的方括号中是空的。

1
2
3
4
5
6
struct flex
{
int count;
double average;
double scores[]; //伸缩型数组成员
}
声明一个struct flex类型的结构变量时,不能用scores做任何事,因为没有给这个数组预留存储空间。实际上,C99的意图并不是让你声明struct flex类型的变量,而是希望你声明一个指向struct flex类型的指针,然后用malloc()来分配足够的空间,以储存struct flex类型结构的常规内容和伸缩型数组成员所需的额外空间。例如,假设用scores表示一个内含5个double类型值的数组,可以这样做:
1
2
3
struct flex * pf;//声明一个指针
//请求为一个结构和一个数组分配存储空间
pf = malloc(sizeof(struct flex) + 5 * sizeof(double));
现在有足够的存储空间储存count、average和一个内含5个double类型值的数组。可以用指针pf访问这些成员:
1
2
pf->count = 5; //设置count成员
pf->scores[2] = 18.5; //访问数组成员的一个元素
带伸缩型数组成员的结构确实有一些特殊的处理要求。第一,不能用结构进行赋值或拷贝
1
2
3
struct flex * pf1, pf2; //*pf1和*pf2都是结构
...
*pf2 = *pf1; //不要这样做
这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用memcpy()函数。 第二,不要以按值方式把这种结构传递给结构。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。 第三,不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。

14.7.10 匿名结构(C11)

在C11中,可以用嵌套的匿名成员结构定义person

1
2
3
4
5
struct person
{
int id;
struct {char first[20]; char last[20];}; //匿名结构
};
初始化ted的方式相同:
1
2
struct person ted = {8483, {"Ted", "Grass"}};
}
但是,在访问ted时简化了步骤,只需把first看作是person的成员那样使用它:
1
puts(ted.first);

14.8 把结构内容保存到文件中

储存在一个结构中的整套信息被称为记录( record),单独的项被称为字段(field)

14.8.1 保存结构的程序示例

更好的方案是使用fread()和fwrite()函数读写结构大小的单元。回忆一下,这两个函数使用与程序相同的二进制表示法。例如

1
fwrite(&primer, sizeof(struct book), 1, pbooks);
定位到primer结构变量开始的位置,并把结构中所有的字节都拷贝到与pbooks相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小,1表明一次拷贝一块数据。带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。以二进制表示法储存数据的缺点是,不同的系统可能使用不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一个系统,不同编译器设置也可能导致不同的二进制布局。

14.10 联合简介

14.10.1 使用联合

联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型

创建联合和创建结构的方式相同,需要一个联合模板和联合变量。可以用一个步骤定义联合,也可以用联合标记分两步定义。下面是一个带标记的联合模板:

1
2
3
4
5
union hold {
int digit;
double bigfl;
char letter;
};
根据以上形式声明的结构可以储存一个int类型、一个double类型和char类型的值。然而,声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值。

下面定义了3个与ho1d类型相关的变量:

1
2
3
union hold fit; //hold类型的联合变量
union hold save[10]; //内含10个联合变量的数组
union hold * pu; //指向hold类型联合变量的指针
可以初始化联合。需要注意的是,联合只能储存一个值,这与结构不同。有3种初始化的方法:把个联合初始化为另一个同类型的联合;初始化联合的第1个元素:或者根据C99标准,使用指定初始化器:
1
2
3
4
5
union hold valA;
valA.letter = 'R';
union hold valB = valA; //用另一个联合来初始化
union host valC = {88}; //初始化联合的digit成员
union hold valD = {.bigfl = 118.2}; //指定初始化器
下面是联合的一些用法
1
2
3
fit.digit = 23; //把23储存在fit,占3字节
fit.bigfl = 2.0; //清除23,储存2.0,占8字节
fit.letter = 'h'; //清除2.0,储存h,占1字节
点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。即使有足够的空间,也不能同时储存一个char类型值和一个int类型值。

和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符

1
2
fit.letter = 'A';
flnum = 3.02 * fit.bigfl; //错误
以上语句序列是错误的,因为储存在fit中的是char类型,但是下一行却假定fit中的内容是double类型。

14.10.2 匿名联合(C11)

匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。

14.11 枚举类型

可以用枚举类型(enumerated type)声明符号名称来表示整型常量。使用enum关键字,可以创建一个“类型”并指定它可具有的值(实际上,enum常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同。例如,可以这样声明:

1
2
enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;
第1个声明创建了spetrum作为标记名,允许把enum spetrum作为一个类型名使用。第2个声明使color作为该类型的变量。第1个声明中花括号内的标识符枚举了spectrum变量可能有的值。因此,color可能的值是red、orange、yellow等。这些符号常量被称为枚举符(enumerator)。然后,便可这样用:
1
2
3
4
5
6
int c;
color = blue;
if (color == yellow)
...;
for (color = red; color <= violet; color++)
...;
虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。

C允许枚举变量使用++运算符,但是C++标准不允许。

14.11.1 enum常量

blue和red到底是什么?从技术层面看,它们是int类型的常量。例如,假定有前面的枚举声明,可以这样写:

1
printf("red = %d, orange = %d\n", red, orange);
其输出如下:
1
red = 0, orange = 1
red成为一个有名称的常量,代表整数0。类似地,其他标识符都是有名称的常量,分别代表1~5。只要是能使用整型常量的地方就可以使用枚举常量

14.11.2 默认值

默认情况下,枚举列表中的常量都被赋予0、1、2等。

14.11.3 赋值

在枚举声明中,可以为枚举常量指定整数值:

1
enum levels {low = 100, medium = 500, high = 2000};
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。例如,假设有如下的声明
1
enum feline {cat, lynx = 10, puna, tiger};
那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、12

14.11.4 enum的用法

枚举类型的目的是为了提高程序的可读性和可维护性。如果要处理颜色,使用red和blue比使用0和1更直观。注意,枚举类型只能在内部使用。如果要输入color中orange的值,只能输入1,而不是单词orange

因为枚举类型是整数类型,所以可以在表达式中以使用整数变量的方式使用enum变量

14.11.5 共享名称空间

C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别。作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。例如,在C中,下面的代码不会产生冲突:

1
2
struct rect { double x; double y; };
int rect;//在C中不会产生冲突
尽管如此,以两种不同的方式使用相同的标识符会造成混乱。另外,C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。

14.12 typedef简介

但是两者有3处不同:

  • 与#define不同,typedef创建的符号名只受限于类型,不能用于值。
  • typedef由编译器解释,不是预处理器。
  • 在其受限范围内,typedef比#define更灵活。

该定义的作用域取决于typedef定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。

1
typedef char * STRING;
没有typedef关键字,编译器将把STRING识别为一个指向char的指针变量。有了typedef关键字,编译器则把STRING解释成一个类型的标识符,该类型是指向char的指针。

用typedef来命名一个结构类型时,可以省略该结构的标签:

1
typedef struct {double x; double y;} rect;
假设这样使用typedef定义的类型名

1
2
rect r1 = {3.0, 6.0};
rect r2;

以上代码将被翻译成:

1
2
3
struct {double x; double y;} r1 = {3.0, 6.0};
struct {double x; double y;} r2;
r2 = r1;

这两个结构在声明时都没有标记,它们的成员完全相同(成员名及其类型都匹配),C认为这两个结构的类型相同,所以r1和r2间的赋值是有效操作。

1
typedef char (* FRPTC ()) [5];
把ERPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含5个char类型元素的数组

14.13 其他复杂的声明

1
2
3
4
5
6
7
int board[8][8]; //声明一个内含int数组的数组
int ** ptr; //声明一个指向指针的指针,被指向的指针指向int
int * risks[10]; //声明一个内含10个元素的数组,每个元素都是一个指向int的指针
int (* rusks)[10]; //声明一个指向数组的指针,该数组内含10个int类型的值
int * oof[3][4]; //声明一个3×4的二维数组,每个元素都是指向int的指针
int (* uuf)[3][4]; //声明一个指向3×4二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]; //声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int型元素的数组
  1. 数组名后面的和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高。因此下面声明的risk是一个指针数组,不是指向数组的指针
    1
    int * risks[10];
  2. []和()的优先级相同,由于都是从左往右结合,所以下面的声明中,在应用方括号之前,*先与rusks结合。因此rusks是一个指向数组的指针,该数组内含10个int类型的元素:
    1
    int (* rusks)[10];
  3. []和()都是从左往右结合。
    1
    2
    3
    char * fump(int); //返回字符指针的函数
    char (* frump)(int); //指向函数的指针,该函数的返回类型为char
    char (* flump[3])(int); //内含3个指针的数组,每个指针都指向返回类型为char的函数
    这3个函数都接受int类型的参数
    1
    2
    3
    4
    5
    6
    typedef int arr5[5];
    typedef arr5 * p_arr5;
    typedef parr5 arrp10[10];
    arr5 togs; //togs是一个内含5个int类型値的数组
    p_arr5 p2; //p2是一个指向教组的指针,该数组内含5个int类型的值
    arrp10 ap; //ap是一个内含10个指针的数组,每个指针都指向一个内含5个int类型值的数组

14.14 函数和指针

假设有一个指向int类型变量的指针,该指针储存着这个int类型变量储存在内存位置的地址。同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。

其次,声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。例如,考虑下面的函数原型

1
void ToUpper(char *); //把字符串中的字符转換成大写字符
ToUpper()函数的类型是“带char*类型参数、返回类型是void的函数”。下面声明了一个指针pf指向该函数类型:
1
void (*pf)(char *); //pf是一个指向函数的指针
从该声明可以看出,第1对圆括号把*和pf括起来,表明pf是一个指向函数的指针。因此,(*pf)是一个参数列表为(char *)、返回类型为void的函数。

所以,如果想声明一个指向某类型函数的指针,可以写出该函数的原型后把函数名替换成(*pf)形式的表达式,创建函数指针声明。前面提到过,由于运算符优先级的规则,在声明函数指针时必须把*和指针名括起来。

函数名可以用于表示函数的地址

1
2
3
4
5
6
7
8
void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis); //把ToUpper作用于mis(语法1)
pf = ToLower;
pf(mis); //把ToLower作用于mis(语法2)
1
2
3
typedef void (*V_FP_CHARP)(char *);
void show (V_FP_CHARP fp, char *);
V_FP_CHARP pfun;
1
V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dummy};
虽然没有函数数组,但是可以有函数指针数组。
1
while (strchr("ulton", ans) = NULL)
该函数在字符串"ulton"中查找字符ans首次出现的位置,并返回一个指向该字符的指针。如果没有找到该字符,则返回空指针。因此,上面的whi1e循环头可以用下面的whi1e循环头代替,但是上面的用起来更方便:
1
while (ans != 'u' && ans != 'l' && ans != 't' && ans != 'o' && ans != 'n')

第15章 位操作

15.3 C按位运算符

15.3.8 编程示例

使用limits.h中的CHAR_BIT宏,该宏表示char中的位数。

15.4 位字段

位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了一个4个1位的字段:

1
2
3
4
5
6
struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;
根据该声明,prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
1
2
prnt.itals = 0;
prnt.undln = 1;
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。

带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用1位,就不需要使用整个变量。内含位字段的结构允许在个存储单元中储存多个设置。

有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制1位大小。可以使用如下的代码

1
2
3
4
5
struct {
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 8;
} prcode;
以上代码创建了两个2位的字段和一个8位的字段。可以这样赋值:
1
2
3
prcode.code1 = 0;
prcode.code2 = 3;
prcode.code3 = 102;
但是,要确保所赋的值不超出字段可容纳的范围

如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。可以用未命名的字段宽度“填充”末命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐

1
2
3
4
5
6
7
struct {
unsigned int field1 : 1;
unsigned int : 2;
unsigned int field2 : 1;
unsigned int : 0;
unsigned int field3 : 1;
} stuff;
这里,在stuff.field1和stuff.field2之间,有一个2位的空隙:stuff.field3将储存在下一个unsigned int中。

字段储存在一个int中的顺序取决于机器。在有些机器上,存储的顺序是从左往右,而在另一些机器上,是从右往左。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。

15.4.1 位字段示例

C以unsigned int作为位字段结构的基本布局单元。因此,即使一个结构唯一的成员是1位字段,该结构的大小也是一个unsigned int类型的大小,unsigned int在我们的系统中是32位

初始化位字段结构与初始化普通结构的语法相同

类似地,也可以给位字段成员赋值

switch语句中也可以使用位字段成员,甚至还可以把位字段成员用作数组的下标

15.4.2 位字段和按位运算符

在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。

如果不想用结构成员表示法来访问不同的部分,也可以使用按位运算符来操作。一般而言,这种方法比较麻烦。

15.5 对齐特性(C11)

在这种上下文中,对齐指的是如何安排对象在内存中的位置。

_Alignof运算符给出一个类型的对齐要求,在关键字_Alignor后面的圆括号中写上类型名即可:

1
size_t d_align = _Alignof(float);
假设d_align的值是4,意思是float类型对象的对齐要求是4。也就是说,4是储存该类型值相邻地址的字节数。一般而言,对齐值都应该是2的非负整数次幂。较大的对齐值被称为stricter或stronger较小的对齐值被称为weaker

可以使用_Alignas说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。例如,如果float类型的对齐要求是4,不要请求其对齐值是1或2。该说明符用作声明的一部分,说明符后面的圆括号内包含对齐值或类型

1
2
3
_Alignas(double) char c1;
_Alignas(8) char c2;
unsigned char _Alignas(long double) c_arr[sizeof(long double)];
在我们的系统中,double的对齐值是8,这意味着地址的类型对齐可以被8整除。以0或8结尾的十六进制地址可被8整除。这就是地址常用两个double类型的变量和char类型的变量cz(该变量是double对齐值)。因为char的对齐值是1,对于普通的char类型变量,编译器可以使用任何地址。

在程序中包含stdalign.h头文件后,就可以把alignas和alignof分别作为_Alignas和_Alignof的别名。这样做可以与C++关键字匹配。

C11在stdlib.h库还添加了一个新的内存分配函数,用于对齐动态分配的内存。该函数的原型如下:

1
void *aligned_alloc(size_t alignment, size_t size);
第1个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,要使用free{)函数释放之前分配的内存。