结构
1. 结构的声明
结构的通常形式是:
struct 结构标签(可选)
{
类型1 标识符1;
类型2 标识符2;
...
}变量定义(可选);
基于以上形式,结构可以有如下几种声明:
//形式1
struct
{
int age;
char name[10];
} Person1, Person2;
//形式2
struct PersonT
{
int age;
char name[10];
} Person1;
struct PersonT Person2;
//形式3
struct PersonT
{
int age;
char name[10];
};
struct PersonT Person1, Person2;
- 在形式2和形式3种,使用结构标签可以在将来的声明中用struct tag 作为struct {内容...}的简写形式。
- 在第2种形式中,结构的声明和变量的定义混合在一起,使得代码可能不易阅读。
- 将结构的声明和变量的定义分开来写是一个更好的选择,如第3种形式所示。
使用typedef创建一个新的结构类型
声明结构时可以使用的另一种良好技巧是用typedef创建一种新的类型,如:
typedef struct
{
int age;
char name[10];
}PersonT;
PersonT Person1, Person2;
此时,PersonT是一种typedef定义的新的类型,而不再是结构标签。
注意:
struct
{
}NewVarT;
typedef struct
{
}NewTypeT;
二者表达的意思截然不同。前者直接声明了一个结构变量,而后者则声明了一种新的类型。
2. 结构的成员
对结构成员的访问可以采用直接访问和间接访问,以以上结构声明的第3种形式为例:
struct PersonT Person1;
struct PersonT *pPerson;
//直接访问
Person1.age;
Person1.name[0]; //.和[]的优先级相同,结合顺序为从左到右,因此可以省略括号
//间接访问
pPerson->age;
pPerson->name[0]; //->和[]的优先级也相同
结构的成员变量的类型不能是结构本身,但可以是指向自身结构的指针类型。例如:
struct SelfT
{
int a;
struct SelfT b;
}
struct SelfT
{
int a;
struct SelfT *b;
}
前者是非法的。
而后者则是合法的。实际上,更加高级的数据结构,如链表和树,都是用这种技巧实现的,每个结构指向链表的下一个元素或树的下一个分枝。
3. 结构的存储分配
对于结构:
struct Align
{
char a;
int b;
char c;
};
编译器按照成员列表的顺序一个接一个地给每个成员分配内存。如果没有对边界对齐的要求,成员的存储位置是连续的。
而如果存储成员时需要满足正确的边界对齐要求,则成员之间可能出现额外的内存空间。如果某个机器的整型值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这一个结构在内存中的存储将如下所示:
灰色表示存储时被跳过的字节。每个结构占据12个字节的内存空间,但实际只使用了其中的6个。
为了提高对内存空间的利用率,可以在结构声明中对结构的成员列表进行重新排列,让那些对边界要求最严格的成员首先出现,而对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。以上结构可以重写为:
struct Align
{
int b;
char a;
char c;
};
所包含的成员不变,但只占用了8个字节的空间。
sizeof操作符可以获得结构的整体长度,包括因边界对齐而跳过的字节。
offsetof宏(定义与stddef.h)可以确定结构中某个成员的实际位置,返回一个size_t值,表示指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。
offsetof(struct Align, b);
对于第一种结构,函数的返回值为4(字节)。
4. 作为函数参数的结构
结构变量是一个标量。因此,把结构作为参数传递给一个函数时,根据C语言的传值调用方式,会实际传递给函数一份结构参数的拷贝,这样做效率很低。
一种更好的方式是,将指向结构的指针作为函数的参数。结构越大,使用指针的效率就越高。向函数传递指针的缺陷在于函数可以通过指针形参对结构变量进行修改。如果不希望如此,则可以使用const关键字来防止这类修改:
形如:
void func(PersonT const *pPerson);
使得指针无法修改其指向的内容。
联合
联合(union)外表与结构相似。但在内存布局上存在关键性的区别。在结构中每个成员依次存储,而在联合中,所有的成员都从偏移地址0开始存储。在某一时刻,只有一个成员真正存储于该位置。
类似于结构,联合的声明形式为:
union 结构标签(可选)
{
类型1 标识符1;
类型2 标识符2;
...
}变量定义(可选);
联合的长度取决于它最长成员的长度,这样联合的长度总是足以容纳它最大的成员。如果成员的长度相差悬殊,那么当存储长度较短的成员时,有大量的内存空间被浪费。在这种情况下,更好的方法是在联合中存储指向不同成员的指针,而不是直接存储成员本身。
联合变量被初始化时,初始值必须是联合的第一个成员的类型,且必须位于一对花括号里,例如:
union
{
int a;
float b;
...
}x = {5};
把x.a初始化为5。不能把x初始化为一个浮点值。
联合一般被用来节省空间。因为有些数据项是不可能同时出现的,如果同时存储它们,显然颇为浪费。引用《C专家编程》里一个例子:
如果我们要存储一些关于动物种类的信息,首先想到的方法可能是:
struct creature
{
char has_backbone; //有脊椎
char has_fur; //有毛皮
short num_of_legs_in_excess_of_4; //超过4条腿
};
但是我们知道,所有的动物要么是脊椎动物,要么是无脊椎动物。我们还知道只有脊椎动物才可能有毛皮,只有无脊椎动物才可能有多于4条腿。因此有毛皮和多于4条腿是互斥的。这样,可以通过把两个互相排斥的字段存储于一个联合中来节省空间:
union secondary_characteristics
{
char has_fur;
short num_of_legs_in_excess_of_4;
};
struct creature
{
char has_backbone;
union secondary_characteristics form;
};
联合也可以把同一个数据解释成两种不同的东西。例如:
union bits32_tag
{
int whole; //一个32位的值
struct {char c0, c1, c2, c3;} byte; //4个8位的字节
}value;
这个联合允许程序员提取整个32位值(作为int),也可以提取单独的字节字段如value.byte.c0等。