一. 结构的基本知识
聚合数据类型能够存储多个数据,C语言提供了两种类型的聚合数据类型,数组和结构。数组是相同的数据,结构是不同类型的数据聚合。结构也是一些值得集合,这些值成为它的成员,每个结构都有它的名字,他们是通过名字来访问的。
1. 结构声明
在结构声明时,必须列出它包含的所有成员,这个列表包括每个成员的类型和名字。
struct tag {member-list}variable-list;
结构体的相关语法:
strict {
int a;
char b;
float c;
} x;
这个声明创建了一个名字叫做x的结构体变量。它包含了三个元素。
struct {
int a;
char b;
float c;
} y[20], *z;
这个声明中y创建了一个结构体数组,它包含了20个结构体,z是一个指向结构体的指针变量。
警告:虽然上面的两种结构的成员的数据类型一样,但是编译器会把它们当成是完全不同的数据类型,所以下面的这条语句是非法的:z=&x;
如果想定义一个指针变量使它可以指向x应该怎么定义呢,下面提供了两种方式:
我们可以在定义的时候给这个结构体加上一个标签,比如:
首先定义一个结构体变量
struct SIMPLE {
int a;
char b;
float c;
} x;
接着定义一个指向结构体变量的指针
struct SIMPLE *p;
这个时候我们就可以使用p =&x;了。
为了方便使用我们可以给这个变量定义一个新的类型
typedefstruct SIMPLE {
int a;
char b;
float c;
} SIMPLE;
这个时候,SIMPLE就是一个新的数据类型,如果我们想定义一个结构体变量,就可以使用SIMPLE x;定义一个指向结构体的指针就可以使用SIMPLE *p;这个时候我们可以使用p =&x;
2. 结构成员
结构体中可以有很多不同的类型,如:
struct COMPLEX{
float f;
int a[20];
long *lp;
struct SIMPLE s;
struct SIMPLE sa[10];
struct SIMPLE *sp;
};
这里我们不必要担心不同的结构体中的成员名相同,因为我们的结构体是不同的。
3. 结构成员的直接访问
如果我们已经知道结构变量的名字,这里我们通过操作符(.)进行访问,比如:struct COMPLEX comp;如果我们想要访问a,则可以通过以下的方式comp.a;这里我们还可以通过下面的方式访问结构体里面的结构体的一个成员(comp.s).a;这里就是我们上面说的不同的结构体中可以定义名字相同的变量。我们甚至可以使用下面更为复杂的方式进行访问((comp.sa)[4]).c;又因为下标引用和点操作符具有相同的优先级,他们的结合性都是从左到右。
4. 结构成员的间接访问
如果我们有一个指向结构体的指针,我们想通过这个指针访问这个结构体,我们可以使用间接访问操作符。举个例子,假定一个函数的参数是个指向结构的指针,如下面的原型所示:
voidfunc(struct COMPLEX *cp );
这个函数可以使用下面这个表达式来访问这个变量所指向的结构的成员f:
(*cp).f
但是我们平时并不使用这种方式访问一个结构体的成员变量,C语言提供了一个->操作符(也称箭头操作符)使用的方式如下:
cp-> f;
5. 结构体的自引用
结构体的自引用是否合法呢,请看下面的一个例子:
structSELF_REF1 {
int a;
int c;
structSELF_REF1 b;
}
这种类型的定义是非法的,因为成员b是另一个完整的结构,其内部还将包含其他的成员b。这样就永无止境的重复下去。
再来看下面的这种定义:
structSELF_REF1 {
int a;
int c;
structSELF_REF1 *b;
}
这个声明和上面的区别在于,声明b的时候不是声明的一个结构体,而是声明了一个指向结构体的一个指针,编译器在结构体的长度确定之前就已经知道了指针的长度,所以这种自引用是合法的。(这其实就是我们在学习数据结构的时候,使用链表的一种很好的体现)
但是我们需要警惕下面的这种声明:
typedefstruct {
int a;
int c;
SELF_FER3 *b;
}SELF_FER3;
这种声明是错误的,类型名在声明的末尾才定义,所以在结构体声明的时候,它还没有被定义。
解决的方法很简单,
typedefstructSELF_FER3_TAG {
int a;
int c;
struct SELF_FER3_TAG *b;
}SELF_FER3;
6. 不完整的声明
偶尔我们需要声明相互之间存在依赖关系的结构。即是一个结构中包含了另一个结构体的一个或者多个成员。和自引用结构体一样,至少有一个结构必须在另一个结构体内部以指针的形式存在。
这样我们就必须要使用不完整的声明,它声明一个作为结构标签的标识符。然后我们可以把这个标签于成员列表联系在一起。看下面的这个例子:
structB;
struct A{
struct B*partner;
};
struct B{
struct A*partner;
}
7. 结构体的初始化
一个位于一对花括号内部、由逗号分隔的初始值列表课用于结构体的各个成员的初始化。如果初始值列表的值不够,剩余的结构体成员将使用缺省的值进行初始化。例如:
struct INIT_EX{
int a;
short b[10];
Simple c;
} x = {10,{1,2,3,4,5},{25,’x’,1.9}}
二. 结构、指针和成员
三. 结构体的存储分配
1. 结构体中存在对齐的原则,即开辟结构体空间的时候并不是按照每个成员的总大小开辟存储空间,(在C++中类实例化的对象也是按照结构体的方式分配存储空间的)看下面这样的一个简单的例子:
struct ALIGN {
char a;
int b;
char c;
};
假设我们的机器是32位的,这个时候首先开辟一个字节,用于存储a,然后看下一个成员是4个字节,为了对齐,前面空3个字节,这时前面的字节数就是int字节数的倍数,接着就是从第五个字节开始开辟4个字节用于存储b,然后是c,它的字节是1个字节,前面已经有八个字节正好是1的倍数,所以直接在b的后面开辟一个字节用于存储c。
优化:我们在定义结构体的时候可以把复杂的放在前面,把简单的放在后面,比如上面的结构体可以如下定义:
struct ALIGN {
int b;
char a;
char c;
};
如果我们用sizeof测试第一个结构的时候,它所测出来的结构包括了空白的结构,即为了对齐而跳过的内存空间,它也被包含进结构体中了。
如果我们想要确定某个成员的实际存储位置,这个时候我们可以使用offsetof宏(定义于stddef.h)
offsetof(type,member)
type是结构体的类型,member是需要测试的成员名,表达式的结构是一个size_t值(无符号整型),表示这个结构成员开始的位置距离整个结构开始的位置偏移的字节数。例如对第一个结构进行下面的使用,offsetof(structALIGN,b);返回值是4。
2. 结构体的内存对齐问题(对上面问题的详细解读)
内存对齐的含义:当结构体中每次放入一个新的成员的时候,它的前面需要补充的空白的字节数。即是:先对齐,再放入。
(1) 对齐数:自身字节数和编译器默认字节数中的较小值(vs:8 linux:4)
(2) 第一个成员放入的时候不需要对齐
(3) 接下来的成员放入的时候,需要对齐到对齐数的整数倍(补充空白字节,使前面的所有字节数的和为对齐数的整数倍)
(4) 上面对齐完之后需要对整个结构体进行对齐,即在所有的成员开辟完空间之后,还需要加入空白的空间,以使得整个结构体对齐,方式为:结构体的总大小是最大对齐数的整数倍。结构体的对齐数是该结构体中的最大对齐数。(我们可以把结构体当成是一个数据类型,这个时候结构体也是有对齐数的,但是这个对齐数不是结构体的大小,而是结构体的最大对齐数的大小)
(5) 数组拆成单个元素进行对齐
修改对齐数的大小,在源文件的一开始处输入:#pragma(4),这条语句的作用是改变系统默认的对齐数,一般往小的改。
四. 作为函数参数的结构
把结构作为参数传递给一个函数是合法的,但是这种做法往往是并不适宜的。
这里我们定义一个结构体
typedefstruct
{
…
…
}Transaction;
这个结构体可能很大,我们编写一个传递结构体的一个函数
void print(Transaction trans)
{
…
…
}
如果我们在主函数中定义了一个定义了一个结构体Transaction current_trans;
然后调用函数print(current_trans);我们知道C语言参数传址调用方式要求把参数的一份拷贝传递给函数。如果这个结构体太大,我们必须把很大一个字节复制到堆栈中,以后再丢弃,这样很影响效率。
再来看下面的函数
void print(Transaction *trans)
{
…
…
}
调用的时候使用
print(¤t_trans);这时我们传递的是一个指针,指针传递的时候要比直接把结构体传递过去小的多,这样压栈的效率就高,传递指针访问结构体也是需要付出一定的代价的,就是我们在函数中要使用间接访问来访问结构体成员。但这对我们来说影响不大。
使用指针传递的时候还有一个缺陷就是,函数内可以对主函数中的结构体进行修改,如果我们想要防止在外部函数修改结构体,我们可以在函数中使用const进行修饰。
还有一点需要主要的是,结构体在传参的时候不会退化成地址,只会发生原始的拷贝,这也是我们使用指针传参的一个原因。
综上我们建议在使用结构体传递参数的时候,可以使用地址传递的方法。
五. 位段
位段的声明和任何普通的结构体成员相同,但是有两个例外。首先位段成员必须声明为int、signed int或者是unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。
这里建议用signed 或者unsigned进行声明,因为如果直接声明为int,它究竟是有符号的还是无符号的整型,这是由编译器而定的,可能会给我们造成不必要的麻烦。
这里通过程序来给大家说明一下位段的内存开辟规则,这里是在结构体内存开辟的规则基础之上的
#include<stdio.h>
#include<windows.h>
typedef struct Stu
{
char name : 2;
char age : 2;
char sex : 5;
}Stu;
int main()
{
Stu student;
printf("%d\n", sizeof(student));
system("pause");
return 0;
}
这里的结果是多少呢,答案是2,这里位段的作用就是想充分的利用内存空间,首先开辟一个字节大小即是八个位,用于存储name,但是这里name只占用了两个位,然后age需要占用两个位,回去看,刚刚开辟的字节还有六个位,于是拿出两个位给了age,接着是sex,它需要五个位,但是刚刚开辟的字节只有四个位了,不够五个位,于是又开辟了一个字节,拿出其中的五个位存放sex,第一次开辟的一个字节剩余的四个位就浪费掉了。这里我们再次把结构体给改一下,再来看一下测试的结果是多少呢
typedef struct Stu
{
char name : 1;
int age : 2;
char sex : 5;
char address : 3;
int add :1;
}Stu;
这里的测试结果是16,为什么呢,这是有原因的对吧,首先开辟一个字节给name,name占用了其中的一位,剩余七个位,然后遇到age,大家请注意age的类型是int型的,虽然只需要用到一个位,但是上面剩余的七个位不能给这个age,于是编译器开辟了四个字节,大家这里还容易犯的一个错误是,认为这四个字节直接在刚刚开辟的一个字节后面开辟,这是错误的。正确的方式是,需要对齐,前面开辟了一个字节,这里需要对齐到四,所以跳过了剩下了的三个字节,开辟了四个字节,这样之后就开辟了八个字节,然后下面的方式一样,在开辟一个字节用于存放sex和address,然后跳过三个字节,在开辟一个字节用于存放add,这样的结果就是16个字节,这里大家可以结合上文中的结构体对齐问题好好的探索一下。
提示
注重可移植性的程序应该注意,由于下面的这些实现相关的依赖性,位段在不同的系统中可能有不同的结果。
六. 联合体
联合体的所有成员引用的是内存中的相同位置,当我们想在不同的时候把不同的东西存储在同一个位置时,就可以使用联合体。联合体也需要对齐,它的对齐方式要适合所有的成员。
1. 变体记录
2. 联合的初始化
联合变量可以被初始化,但是这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。例如
union{
int a;
float b;
char c;
} x = {5};
把x.a初始化为5.
我们不能把这个类量初始化为一个浮点值或者字符值。如果给出的初始值是任何其他类型,它就会转换(如果有可能的话)为一个整数并赋值给x.a。