结构和联合
结构体基础
聚合类型
聚合类型:能够同时存储超过一个单独的数据,C语言提供两种聚合数据类型数组和结构。
数组和结构的区别:
- 数组是相同类型的数据的聚合,结构是不同类型数据的聚合。
- 因为数组的元素长度相同可以采用下标方式访问,结构成员的长度不同,只能通过名字访问,不能通过下标来访问。
- 数组名在表达式中使用时被当做一个指针,而结构体变量在表达式中使用时不能被当做一个指针。
结构体的声明
结构的通常形式
struct tag(可选)
{
member-list
}variable-list(可选);
基于以上形式,结构可以有如下几种声明:
//形式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种形式所示。
注意:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*y;
第一个声明创建了一个x变量,它包含了三个成员,一个整数,一个字符和一个浮点数。
第二个声明创建了指向一个结构类型的指针。
需要注意的是虽然两者结构体的成员是一致的,但是编译器将它们认为是不同的变量类型,所以下面的表达式是不合法的:
y = &x
typedef 创建新的结构类型
声明结构时可以使用的另一种良好技巧是用typedef创建一种新的类型,如:
typedef struct
{
int age;
char name[10];
}PersonT;
PersonT Person1, Person2;
此时,PersonT 是一种typedef定义的新的类型,而不再是结构标签。
注意:
struct
{
}NewVarT;
typedef struct
{
}NewTypeT;
二者表达的意思截然不同。前者声明了一个结构变量,而后者则声明了一种新的类型。
结构的成员
间接访问与直接访问
struct PersonT
{
int age;
char name[10];
};
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;
}
前者是非法的,结构体中包含自己的成员,这样一直重复永无止境。
而后者则是合法的。实际上,更加高级的数据结构,如链表和树,都是用这种技巧实现的。
结构的初始化
结构的初始化方式和数组的初始化很类似:
- 一个位于一对花括号内部,由逗号分隔的初始列表可用于结构各个成员的初始化,这些值根据结构成员的顺序写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。
- 结构中如果包含数组和结构成员,其初始化方式类似于多维数组的初始化。
typedef struct
{
int i;
char ch;
float f;
}Simple;
struct INIT_EX
{
int a;
short b[10];
Simple c;
} x = {
10,
{ 1,2,3,4,5 },
{ 25,'x',1.9 }
};
结构的存储分配
对于结构:
struct Align
{
char a;
int b;
char c;
};
如果没有对边界对齐的要求,成员的存储位置是连续的。
如果存储成员时需要满足正确的边界对齐要求,则成员之间可能出现额外的内存空间。如果某个机器的整型值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这一个结构在内存中的存储将如下所示:
要求最严格的成员首先出现,而对边界要求最弱的成员最后出现。**这种做法可以最大限度地减少因边界对齐而带来的空间损失。以上结构可以重写为:
struct Align
{
int b;
char a;
char c;
};
h`)可以确定结构中某个成员的实际位置,返回一个size_t值,表示指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。
sizeof(Align)
offsetof(Align,a);
作为函数参数的结构
结构变量是一个标量。因此,把结构作为参数传递给一个函数时,根据C语言的传值调用方式,会实际传递给函数一份结构参数的拷贝,这样做效率很低。
一种更好的方式是,将指向结构的指针作为函数的参数。结构越大,使用指针的效率就越高。
如果不希望通过指针形参对结构变量进行修改,则可以使用const关键字:
void func(PersonT const *pPerson);
位段
位段和结构体类似,但是依然有不同之处:
- 位段的成员必须是int、unsigned int 、signed int。
- 位段的成员后边有一个冒号和一个数字,数字表明该位段占用的位数目。
位段涉及很多不确定因素,位段不跨平台,所以可移植的程序避免使用位段。
- int位段被当成有符号或是无符号是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)。
- 位段中的成员在内存中是从左向右分配,还是从右向左分配尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的还是利用,这是不确定的。
例如声明下面的位段:
struct CHAR {
unsigned ch : 7;
unsigned font : 6;
unsigned size :19;
};
struct CHAR ch1;
这个声明可以有两种形式创建ch1:
以方便的访问一个整型值的部分内容。
针对上面的内容我们可以设计一个位段,假如是在从右向左分配位段的机器上:
struct DISK_REGISTER_FORMAT {
unsigned command : 5;
unsigned sector : 5;
unsigned track : 9;
unsigned error_code : 8;
unsigned head_loaded : 1;
unsigned write_protected : 1;
unsigned disk_spinning : 1;
unsigned error_occured : 1;
unsigned ready : 1;
};
那么当我们知道磁盘寄存器的内存地址是0xc0200142
,访问寄存器时就可以按照如下操作:
#define DISK_REGISTER \
((struct DISK_REGISTER_FORMAT*)0xc0200142)
DISK_REGISTER->ready = READ;
DISK_REGISTER->command = COMMAND;
DISK_REGISTER->sector = SECTOR
上面位段实现的功能,都可以使用移位和屏蔽手段来实现:
#define DISK_REGISTER (unsigned int*)0xc0200142
*DISK_REGISTER &= 0xfffffc1f
*DISK_REGISTER |= (SECTOR & 0x1f) << 5;
第一条语句AND操作把sector字段清零,其它字段不受影响。
第二条语句AND操作确保这个值不会超过sector的宽度,之后左移合适的位置,并进行OR操作实现赋值。
联合
联合(union)外表与结构相似。但在内存布局上存在关键性的区别。在结构中每个成员依次存储,而在联合中,所有的成员都从偏移地址0开始存储。在某一时刻,只有一个成员真正存储于该位置。
声明
类似于结构,联合的声明形式为:
union tag(可选)
{
member-list
}variable-list(可选);
长度
联合的长度取决于它最长成员的长度,这样联合的长度总是足以容纳它最大的成员。
如果成员的长度相差悬殊,那么当存储长度较短的成员时,有大量的内存空间被浪费。在这种情况下,更好的方法是在联合中存储指向不同成员的指针,而不是直接存储成员本身。
初始化
联合变量被初始化时,初始值与联合的第一个成员的类型匹配,且必须位于一对花括号里,例如:
union
{
int a;
float b;
}x = {5};
把x.a
初始化为5。