一、结构的声明与定义
C语言中对结构的声明与定义有很多值得注意的细节。下面列举几个方面:
1、下面是两个结构声明:
struct //声明1 { int a; char b; float c; }x; struct //声明2 { int a; char b; float c; }y[20], *z;
以上两个声明被编译器当作两种不同的类型,即使它们的成员列表完全相同。因此,变量y、z的类型与x的类型不同,z = &x也是非法的。
2、标签允许多个声明使用同一个成员列表,并创建同一种类型的结构。例如:
struct SIMPLE { int a; char b; float c; };
以上声明把标签SIMPLE和成员列表联系在一起。该声明并没有提供变量列表,所以它并未创建任何变量。使用标签SIMPLE创建变量如下:
struct SIMPLE x; struct SIMPLE y[20], *z;
此时y、z和x都是同一种类型的结构变量。
3、声明结构时经常使用typedef创建一种新的类型。例如:
typedef struct { int a; char b; float c; }simple; simple x; simple y[20], *z;
此时simple是类型名而不是结构标签。当然,y、z和x也都是同一种类型的结构变量。
4、结构的自包含和自引用。
(1)结构不能自包含,无论是直接的还是间接的。例如以下声明是非法的:
struct SELF_PRE1 { int a; struct SELF_PRE1 b; float c; };
因为编译器无法计算sizeof值,也不知道该给这样的对象分配多少存储空间。
(2)结构的自引用是合法的。例如以下声明是合法的:
struct SELF_PRE2 { int a; struct SELF_PRE2 *b; float c; };
因为任何类型的指针大小都一样,给指针分配存储空间的时候不需要知道它指向对象的类型细节。
5、不完整声明。
不完整声明指的是:声明一个作为结构标签的标示符,把这个标签用在不需要知道这个结构的长度的声明中。例如声明指向这个结构的指针。
struct B; struct A { struct B *partner; }; struct B { struct A *partner; };
以上声明中,A的成员列表需要标签B的不完整声明。一旦A被声明之后,B的成员列表也可以被声明。
注意以下声明的陷阱:
typedef struct { int a; SELF_PRE3 *b; float c; }SELF_PRE3;
类型名SELF_PRE3直到声明的末尾才定义,所以在结构声明的内部它尚未定义。解决方案是定义一个结构标签来声明b。如下:
typedef struct SELF_PRE3_TAG { int a; struct SELF_PRE3_TAG *b; float c; }SELF_PRE3;
二、位域
多数情况,以字节为基本单位的存储模式会浪费大量内存空间。有些设备提供的存储空间有限,例如嵌入式系统,因此必须对存储开销“精打细算”。可使用位域和位运算来解决。
1、位域的声明
位域以单个的位(bit)为单位来设计一个结构所需要的存储空间。位域的声明是在结构的成员名后面加一个冒号和一个整数,这个整数指定该位域所占用的位的数目。声明位域时注意:
(1)C语言位域成员必须声明为int、signed int或unsigned int类型,C++还允许使用char、long等类型。
(2)signed int类型数据的正负符号要占用1位,因此该类型的位域成员长度应该至少为2。
(3)最好用signed或unsigned显式声明位域,因为如果把位域声明为int类型,它究竟被解释为有符号数还是无符号数由编译器决定。
struct CHAR { unsigned ch : 7; unsigned font : 6; unsigned size :19; }; struct CHAR ch;
以上是一个位域声明的例子。位域能够利用存储ch和font所剩余的位来增加size的位数,避免了声明一个32位的整数来存储size位域。在32位机器上,这个声明将根据下面两种可能的方法创建ch。
2、位域的使用
使用位域的理由是:能够把长度为奇数的数据包装在一起,节省存储空间;能够很方便地访问一个整型值的部分内容。使用位域时有以下几点需要注意:
(1)结构中不同长度的字段实际上存储于一个或多个整型变量中。
(2)不要定义超越类型最大位数的位域成员。(这与平台相关)例如上面声明的位域,许多16位整数机器的编译器会把这个声明标志为非法的,因为最后一个位段的长度超过了整型的长度。
(3)可定义非具名的位域成员,其作用相当于占位符,可用来隔离两个相邻的位域成员。例如:
struct DATETIME { unsigned int day : 5; unsigned int : 2; unsigned int hour : 5; };
由于第二个位域成员没有名字,因此不能直接访问它所在的位。
(4)可定义长度为0的位域成员,其作用是迫使下一个成员从下一个完整的机器字开始分配空间。例如:
struct DATETIME { unsigned int day : 5; unsigned int : 0; unsigned int hour : 5; };
VC6.0环境下sizeof(DATETIME) = 8。
(5)不能取一个位域对象的数据成员的地址,即使该成员完全与字节边界对齐,因为字节是编址的最小单位而不是位。但可以取位域对象的地址,即使位域所有成员的位数总和达不到整字节的倍数,位域对象也会对齐到机器字长。
(6)不能把位域成员当作位的数组,因此不能使用访问数组元素的方法来访问位域成员的单个位。
(7)使用位域节省存储空间会导致程序运行速度的下降,因为计算机无法直接寻址到单个字节的某些位,必须通过额外的代码来实现。这种矛盾是由计算机的基本原理决定的,在“内存空间”和“运行速度”无法同时优化的情况下,由应用需求来决定优化哪一个。
(8)在设计位域的时候,最好不要让一个位域成员跨越一个不完整的字节来存放,因为这样会增加计算机运算的开销。
(9)注重可移植性的程序应避免使用位域。由于下面几点与实现有关的依赖性,位域在不同的系统中可能有所不同。
1) int位域被当作有符号数还是无符号数。
2)位域中位的最大数目。许多编译器把位域成员的长度限制在一个整型值的长度之内,所以一个能够运行于32位整数的机器上的位域声明可能在16位整数的机器上无法运行。
3)位域中的成员在内存中是从左向右分配的还是从右向左分配。
4)当一个声明指定了两个位域,第2个位域比较大,无法容纳于第一个位域剩余的位时,编译器有可能把第2个位域放在内存的下一个字,也可能直接放在第1个位域后面,从而在两个内存位置的边界上形成重叠。
三、结构的初始化
结构的初始化有两种方法:
1、使用memset函数或其他类似的内存初始化函数。
2、定义的时候指定初始值。可以仅指定第一个成员的初值来初始化对象,后面的成员将自动初始化为0,就像数组的初始化一样。