详解C语言中的结构体、枚举、联合、位段
1.结构体
结构是一些值的集合,这些值称为成员变量。结构的成员可以是不同类型的变量。
- 声明
声明的整体框架为:
struct 结构体名{
类型 变量名;
...
类型 变量名;
}变量名;
举个栗子:
struct Student{ //声明一个学生结构体
int id; //学号
char name[20]; //姓名
char sex[2]; //性别
}s1,s2;
上面这段代码声明了一个学生结构体,里面包含有三个变量,再定义了两个变量s1和s2为此学生结构体类型。
结构体在声明时可以匿名,即结构体名可以不写,虽然可以通过编译,但不推荐在实际操作中这样使用。
结构体的成员可以是变量,数组,指针,也可以是其他的结构体。
- 结构体成员的访问
1.可以通过.
操作符来访问
以上面声明好的学生类型为例:
printf("%d %s %s\n",s1.id,s1.name,s1.sex);
这样就可以输出学生s1的学号,姓名和性别。
2.还可以通过->
操作符来访问
struct student *p=&s2; //声明一个学生指针并指向s2
printf("%d %s %s\n",p->id,p->name,p->sex); //用指针输出s2的信息
- 结构体的自引用
在结构体中不能包含一个类型为自身的结构体。
例如:
struct A{
int n;
struct A a;
}
这种定义是不允许的,因为每当定义一个结构体对象时就要开辟一段内存来存放这个结构体变量,按照上面这段代码的话这个结构体类型里面又有一个自身的结构体类型的变量,一方面会不停地开辟空间,另一方面此结构体所占空间的大小是不确定的(或者说是无穷大),所以这种自引用方式是非法的。
正确的自引用方法如下:
struct A{
int n;
struct A *a;
}
在结构体中包含的是一个结构体指针,这样就可以完成结构体的自引用。
- 结构体自引用的重命名
在定义结构体时可以用typedef来把这个结构体进行重命名。
typedef struct {
int a;
}A;
在没有结构体的自引用时这样定义是可以的。
typedef struct{
int data;
Node *next;
}Node;
在包含结构体的自引用时这样子定义就会出错,要像下面这样定义:
typedef struct Node{
int data;
struct Node *next;
}Node;
- 结构体的初始化
在定义一个结构体变量时经常需要顺便把它进行初始化,结构体的初始化要用大括号
依次将与结构体里面类型相对应
的常量
括起来,并且用逗号
隔开。
听起来很复杂,看代码就很简单:
typedef struct Student{
int id;
char name[20];
int gender[2];
}Stu;
int main()
{
Stu s1={101,"小明","男"}; //定义一个学生变量s1并进行初始化
return 0;
}
//结构体中有嵌套结构体时的初始化方法:
struct Date{
int year;
int month;
int day;
};
typedef struct Student{
int id;
char name[20];
int gender[2];
struct Date Brithday;
}Stu;
int main()
{
Stu s1={101,"小明","男",{1998,1,1}}; //定义一个学生变量s1并进行初始化
return 0;
}
- 结构体内存对齐
结构体的变量是存在内存对齐
的,举栗子:
struct s1{
char c1;
int a;
char c2;
};
printf("%d\n",sizeof(struct s1));
这段代码输出的结果并不是1+4+1(在win32环境下,下同),而是12.
struct s1{
char c1;
char c2;
int a;
};
printf("%d\n",sizeof(struct s1));
而这段代码的结果既不是1+4+1,也不是12,而是8.
这说明结构体中的变量在内存中不是简单的紧挨着保存的,而是有选择
地保存,这便是内存对齐的结果。
如图:
第一种结构体定义在内存中的存储方式↑
第二种结构体定义在内存中的存储方式↑
在内存中实际上这两个结构体是如上图这样子保存的。
下面是内存对齐的具体规则:
1. 第一个变量在与结构体变量偏移量为0的地址处
2. 其他成员变量要对其到某个数字(对其数)
的整数倍
的地址处。
对其数=编译器默认的对齐数与该成员大小的较小值
vs的对齐数默认为8,Linux中对齐数默认为4.
3. 结构体总大小为最大对齐数的整数倍
4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
为什么会存在内存对齐?
- 平台原因(移植原因)
不是所有的硬件都能访问任意地址上的数据,某些硬件平台只能在某些地址处取某些特定的数据,否则会出现硬件一异常。- 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
因为为了访问未对齐的内存,处理器需要做两次访问动作, 而对齐的内存处理器只需要进行一次内存访问即可读出数据。
总的来说:内存对齐是用空间换取时间的做法,那么在设计结构体中,我们既想节省空间又想节省时间就要尽可能地让占用小空间的成员集中在一起。
修改默认对齐数
默认对齐数是可以被修改的,修改默认对齐数的语句是:
#pragma pack(a) //这里的a为要修改成的数值
#pragma pack() //取消修改的默认对齐数,还原为原来的默认值
2.位段
- 概念
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。(百度百科)
信息的存取一般以字节为单位。实际上,有时存储一个信息不必用一个或多个字节,例如,“真”或“假”用0或1表示,只需1位即可。在计算机用于过程控制、参数检测或数据通信领域时,控制信息往往只占一个字节中的一个或几个二进制位,常常在一个字节中放几个信息。
通俗地说,位段就是一种节省空间的保存数据的方式。
- 定义
struct A
{
int a:2; //a占两位
int b:5; //b占5位
int c:10; //c占10位
int d:30; //d占30位
};
位段定义的规则:
- 位段的成员必须是int ,unsigned int 或signed int.
- 位段的成员名后面有一个冒号和数字.
位段的内存分配
1.位段的空间是按照需要以4个字节或1个字节的方式来开辟的
2.位段涉及很多不确定的因素,位段是不跨平台的,注重可移植性的程序应尽量不用位段位段的跨平台问题
1.int位段被当作有符号数还是无符号数是不确定的
2.位段中最大位的数目是不确定的
3.位段中的成员在内存中是从左到右存放的还是从右向左存放的标准尚未定义
4.当一个结构包含两个位段,第二个位段成员较大,无法容纳于第一个位段剩余的位段时,是舍弃剩下的位还是利用,这也是不确定的。
3.枚举
枚举类型就是在结构中把可能的值一一列举出来
- 定义
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun,
};
这就是一个枚举类型,{}中的内容是枚举类型中的可能取值,也叫枚举常量
,默认从0开始,每次递增1,也可以手动设置,举个栗子:
enum Color
{
RED=1;
GREEN=2;
BLUE=4;
};
- 使用
以上面的enum Color为例:
enum Color clr=GREEN;
clr=4; //这两种方法的效果是一样的。
4.联合(共用体)
- 概念
联合是一种特殊的自定义类型,这种类型里面也包含一个或多个成员,特殊的是这些成员共用同一块空间
。 - 定义
举个栗子:
union U
{
char c;
int a;
};
union U un; //这就定义了一个联合变量un
内存分配
因为联合的成员是共用同一块空间的,这样一个联合的大小至少是最大成员的大小(因为这样联合草有能力保存下最大的那个成员)。
当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。联合妙用:可以用内存共用的原理来判断当前计算机的大小端存储。