The C Programming Language - annotation, 6

Struct

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看作一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。

ANSI标准在结构方面最主要的变化是定义了结构的赋值操作——结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。多年以前,这一操作就已经被大多数的编译器所支持,但是,直到这一标准才对其属性进行了精确定义。在ANSI标准中,自动结构和数组现在也可以进行初始化。

1. 结构的基本知识

关键字struct引入结构声明。结构声明由包含在花括号内的一系列声明组成。关键字struct后面的名字是可选的,称为结构标记。结构标记用于为结构命名。在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。

结构中定义的变量称为成员。结构成员、结构标记和普通变量(即非成员)可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。另外,不同结构中的成员可以使用相同的名字,但是,从编程风格方面来说,通常只有密切相关的对象才会使用相同的名字。

struct声明定义了一种数据结构。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的。

如果结构声明的后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。但是,如果结构声明中带有标记,那么在以后定义结构实例时可以使用该标记定义。结构的初始化可以在定义的后面使用初值表进行。初值表中同每个成员对应的初值必须是常量表达式。自动结构也可以通过赋值初始化,还可以通过调用返回相应类型结构的函数进行初始化。

在表达式中,可以通过下列形式引用某个特定结构中的成员:结构名.成员。其中的结构成员运算符.将结构名与成员名连接起来。

结构可以嵌套。

2. 结构与函数

结构的合法操作只有几种:作为一个整体复制和赋值,通过&运算符取地址,访问其成员。其中,复制和赋值包括向函数传递参数以及从函数返回值。结构之间不可以进行比较。可以用一个常量成员值列表初始化结构,自动结构也可以通过赋值进行初始化。

至少可以通过3种可能的方法传递结构:一是分别传递各个结构成员,而是传递整个结构,三是传递指向结构的指针。这3种方法各有利弊。

注意,参数名和结构成员同名不会引起冲突。事实上,使用重名可以强调两者之间的关系。

…结构类型的参数和其他类型的参数一样,都是通过值传递的。

如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高。…结构成员运算符.的优先级比*的优先级高。

结构指针的使用频度非常高,为了使用方便,C语言提供了另一种简写方式。假定p是一个指向结构的指针,可以用 p->结构成员 这种形式引用相应的结构成员。

运算符.->都是从左至右结合的。

3. 结构数组

C语言提供了一个编译时(compile-time)一元运算符sizeof,它可用来计算任一对象的长度。表达式 sizeof 对象 以及 sizeof(类型名) 将返回一个整型值,它等于指定对象或类型占用的存储空间字节数。(严格地说,sizeof的返回值是无符号整型值,其类型为size_t,该类型在头文件<stddef.h>中定义。)其中,对象可以是变量、数组或结构;类型可以是基本类型,也可以是派生类型。

条件编译语句#if中不能使用sizeof,因为预处理器不对类型名进行分析。但预处理器并不计算#define语句中的表达式,因此,在#define中使用sizeof是合法的。

4. 指向结构的指针

但是,千万不要认为结构的长度等于各成员长度的和。因为不同的对象有不同的对齐要求,所以,结构中可能会出现未命名的“空穴”(hole)。使用sizeof运算符可以返回正确的对象长度。

…具体采用哪种写法属于个人的习惯问题,可以选择自己喜欢的方式并始终保持自己的风格。

5. 自引用结构

一个包含其自身实例的结构是非法的。

…自引用结构的一种变体:两个结构相互引用。

对齐要求一般比较容易满足,只需要确保分配程序始终返回满足所有对齐限制要求的指针就可以了,其代价是牺牲一些存储空间。

6. 表查找

7. 类型定义(typedef)

C语言提供了一个称为typedef的功能,它用来建立新的数据类型名。

注意,typedef中声明的类型在变量名的位置出现,而不是紧接在关键字typedef之后。typedef在语法上类似于存储类externstatic等。

从任何意义上讲,typedef声明并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。typedef声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通方式声明的变量具有相同的属性。实际上,typedef类似于#define语句,但由于typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。

除了表达方式更简洁之外,使用typedef还有另外两个重要原因。首先,它可以使程序参数化,以提高程序的可移植性。如果typedef声明的数据类型同机器有关,那么,当程序移植到其他机器上时,只需改变typedef类型定义就可以了。一个经常用到的情况是,对于各种不同大小的整数值来说,都使用通过typedef定义的类型名,然后,分别为各个不同的宿主机选择一组合适的shortintlong类型大小即可。标准库中有一些例子,例如size_tptrdiff_t等。

typedef的第二个作用是为程序提供更好的说明性。

8. 联合

联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息。

联合的目的——一个变量可以合法地保存多种数据类型中任何一种类型的对象。其语法基于结构。

…必须足够大,以保存最大的一种,具体长度同具体的实现有关。这些类型中的任何一种类型的对象都可赋值给u,且可使用在随后的表达式中,但必须保证是一致的:读取的类型必须是最近一次存入的类型。程序员负责跟踪当前保存在联合中的类型。如果保存的类型与读取的类型不一致,其结果取决于具体的实现。

可以通过下列语法访问联合中的成员:联合名.成员联合指针->成员,它与访问结构的方式相同。

联合可以使用在结构和数组中,反之亦可。访问结构中的联合(或反之)的某一成员的表示法与嵌套结构相同。

实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员,并且,其对齐方式要适合于联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中的一个成员。

联合只能用其第一个成员类型的值进行初始化。

9. 位字段

在存储空间很宝贵的情况下,有可能需要将多个对象保存在一个机器字中。一种常用的方法是,使用类似于编译器符号表的单个二进制位标志集合。外部强加的数据格式(如硬件设备接口)也经常需要从字的部分位中读取数据。

最简洁的方法就是使用一个charint对象中的位标志集合。通常采用的方法是,定义一个与相关位的位置对应的“屏蔽码”集合。

C语言提供了一种可替代的方法,即直接定义和访问同一个字中的位字段的能力,而不需要通过按位逻辑运算符。位字段(bit-bield),或简称字段,是“字”中相邻位的集合。“字”(word)是单个的存储单元,它同具体的实现有关。

单个字段的引用方式与其他结构成员相同。字段的作用与小整数相似。同其他整数一样,字段可出现在算术表达式中。

字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界由具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界上对齐。

某些机器上字段的分配是从字的左端至右端进行的,而某些机器上则相反。这意味着,尽管字段对维护内部定义的数据结构很有用,但在选择外部定义数据的情况下,必须仔细考虑哪端优先的问题。依赖于这些因素的程序是不可移植的。字段也可以仅仅声明为int,为了方便移植,需要显示声明该int类型是signed还是unsigned类型。字段不是数组,并且没有地址,因此对它们不能使用&运算符。