前言
C语言的数据类型包括基本类型(内置类型)、构造类型(自定义类型)、指针类型和空类型(void),其中基本类型就是我们常见的整形、浮点型,而自定义类型则包括数组、结构体、枚举、联合(共用体),数组我们已经非常熟悉了,今天我们主要学习自定义类型中其他几种类型:结构体、枚举以及联合。
一、结构体
结构体是一些值的集合,这些值称为成员变量;结构的每个成员可以是不同类型的变量,所以结构常用来描述复杂对象。
1、结构体的声明
一般声明
结构体的声明一般由结构体关键字 + 结构体标签 + 成员列表组成:
struct tag //struct:结构体关键字 tag:结构体标签 { member - list; //成员列表 }variable - list; //变量列表(可以省略)
例如描述一本书:
struct Book { char name[20]; //书名 char author[20]; //作者 char num[12]; //编号 float price; //价格 }; //注意最后面的这个分号不能丢
特殊声明
结构体声明的时候,可以不完全声明,即省略结构体标签,这种结构体被称为匿名结构体:
//匿名结构体 struct { member-list; }x;
由于匿名结构体没有名字,所以不能在程序的其他位置使用该结构体创建结构体变量,而只能在结构体声明的同时定义结构体变量,也就是说,匿名结构体只能使用一次。
我们可以用匿名结构体来描述一个学生:
struct { char name[20]; //名字 int age; //年龄 char sex[5]; //性别 char id[20]; //学号 }stu; //结构体变量
2、结构体的自引用
错误的自引用方式
struct Node { int data; struct Node next; };
上面这种结构体的声明方式是错误的,因为struct Node 中包含了一个struct Node 的Next,而Next中又会包含一个struct Node 的next,这样无限套娃,使得我们无法计算这个结构体的大小;正确的结构体自引用应该是一个结构体中包含指向该结构体的指针,如下所示:
正确的自引用方式
struct Node { int data; struct Node* next; };
一个结构体中包含了一个指向该结构体的指针,实现了结构体的自引用,同时,由于指针的大小是固定的(4/8个字节),所以该结构体的大小也是可计算的。
3、结构体变量的定义和初始化
结构体定义变量一共有两种方式,一种是在进行结构体声明的同时定义结构体变量,另一种是利用结构体类型来定义结构体变量。
struct Point { int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //利用结构体类型来定义变量p2
结构体变量的初始化和数组变量的初始化十分类似,在定义结构体变量的同时赋初值即可。
struct Stu { char name[15]; int age; }s1 = { "zhangsan", 20 }; //初始化 struct Stu s2 = { "lisi", 22 }; //初始化 struct Node { int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化 struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
4、结构体传参
结构体传参分为两种方式:一种是传递整个结构体,这时形参需要创建一个与源结构体同等大小的空间来接收,结构体过大浪费空间的同时时会十分影响效率;另一种是传递结构体的地址,这时无论源结构体有多大,形都参只需要用一个结构体指针来接收,节省空间的同时提高效率。
struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4}, 1000 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s); //传结构体 print2(&s); //传结构体地址 return 0; }
print1 和 print2 相比,首选 print2 函数,原因如下:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。
结论:结构体传参的时候,要传结构体的地址。
5、结构体内存对齐(重要)
结构体内存对齐是结构体大小的计算规则,是校招笔试和面试过程中一个十分热门的考点,希望大家认真对待。
在学习结构体内存对齐之前,我们先给两组计算结构体大小的题目,看看你能否做对:
//计算结构体大小 #include <stdio.h> struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
对答案有疑问的同学不要慌,我们在学习结构体内存对齐的过程中来分析答案的由来。
结构体内存对齐的规则
关于结构体内存对齐规则,大部分参考资料是这样说的:
- 第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到它的对齐数的整数倍的地址处。
对齐数 = 编译器默认的对齐数与该成员变量大小的较小值。
VS的默认对齐数是8.
只有VS编译器下才有默认对齐数的概念,其他编译器下变量的对齐数 = 变量的大小
结构体总大小为最大对齐数的整数倍。(最大对齐数为所有变量的对齐数的最大值)
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小为所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
知道了最大对齐数的对齐规则,我们再来看上面的练习题:
struct S1 { char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1 int i; //变量大小为4,默认对齐数为8 -> 对齐数为4 char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1 };
我们假设struct S1的起始位置为图中箭头所示位置,则各位置的偏移量如图;由内存对齐的规则:
第一个成员在与结构体变量偏移量为0的地址处:所以c1在偏移量为0处,且c1占一个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 i 的对齐数是4,所以 i 只能从偏移量为4的位置开始存储,且 i 占四个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 c2 的对齐数是1,所以 c2 紧挨着 i 存储,且 c2 占一个字节;
结构体总大小为最大对齐数的整数倍:由于最大对齐数为4,所以总对齐数要为4的倍数,大于9的最小的4的倍数为12,所以整个结构体的大小为12个字节。
struct S2 { char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1 char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1 int i; //变量大小为4,默认对齐数为8 -> 对齐数为4 };
如图:
c1 从0偏移处开始,占一个字节;c2 对齐数为1,所以紧挨着 c1 存储,占一个字节;i 对齐数为4,所以在4的整数倍位置 – 4偏移处开始存储,占4个字节;存放完毕后0~7一共占8个字节,因为最大对齐数为4,8为4的整数倍,所以不变。
6、offsetof 宏
offsetof 的介绍
offsetof 是C语言中定义的一个用于求结构体成员在结构体中的偏移量的一个宏,其对应的头文件是 ,由于 offsetof 的使用方法与函数一样,所以它经常被错误的认为是一个函数;我们可以在VS中右键单击offsetof转到定义,查看offsetof的在VS中的实现方式。
offsetof 的参数
size_t offsetof( structName, memberName ); # size_t 返回值,返回成员的偏移量; # structName 参数,结构体变量名; # memberName 参数,成员变量名;
offsetof 的使用
#include <stdio.h> #include <stddef.h> //offsetof对应头文件 struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\t", offsetof(struct S1, c1)); printf("%d\t", offsetof(struct S1, i)); printf("%d\n", offsetof(struct S1, c2)); printf("%d\t", offsetof(struct S2, c1)); printf("%d\t", offsetof(struct S2, c2)); printf("%d\n", offsetof(struct S2, i)); return 0; }
offsetof 的模拟实现
我们以上面的 struct S1为例,经过上面的分析我们已经知道了 struct S1的大小为12,并且画出来具体的图示:
我们观察后发现:结构体成员在结构体中的偏移量 = 结构体成员的地址 - 结构体的起始地址,比如 struct S1中 i 的地址 - 结构体的起始地址可以得到结构体成员 i 的偏移量等于4;那么如果结构体的起始地址在0处,那么结构体成员的偏移量 = 结构体成员的地址 - 0 = 结构体成员地址,所以我们可以把0强转为对应结构体指针类型,然后返回结构体成员的地址即可得到结构体成员的偏移量,具体代码如下:
#include <stdio.h> #define OFFSETOF(type, member) (size_t)&(((type*)0)->member) struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", OFFSETOF(struct S1, c1)); printf("%d\n", OFFSETOF(struct S1, i)); printf("%d\n", OFFSETOF(struct S1, c2)); return 0; }
7、为什么存在内存对齐
从上面的例子我们可以看到,结构体内存对齐会浪费一定的内存空间,但是计算机不是要尽可能的做到不浪费资源吗?那为什么还要存在内存对齐呢?关于内存对齐存在的原因,大部分的参考资料是这样说的:
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。所以内存对齐能够提高访问效率。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
这里我对原因中的第二点做一下解释:
大家都知道,我们的机器分为32位机器和64位机器,这里的32位和64位其实指的是CPU的位数,而CPU的位数对应着CPU的字长,而字长又决定着CPU读取数据时一次访问多大即空间,即一次读取几个字节,我们以32位机器为例:
如图,32位机器一次访问四个字节的大小,如果不存在内存对齐,那么要取出 i 中的数据需要两次读取,存在内存对齐则只需要读取一次。
设计结构体的技巧
在了解了结构体的对齐规则之后,有没有一种方法能让我们在设计结构体的时候既满足对齐规则,又能尽量的节省空间呢?其实是有的,方法就是:**让占用空间小的成员尽量集中在一起。**就像的习题,我们把占用空间下的 c1 和 c2 放在一起,从而使得 struct S2 比 struct S1 小了四个字节。
8、修改默认对齐数
我们可以使用 “#pragma pack(num)” 命令来修改VS中的默认对齐数。例如:
#include <stdio.h> #pragma pack(8)//设置默认对齐数为8 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 #pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
在 struct S2 中,我们通过 " #pragma pack(1) " 命令把VS的默认对齐数设置为1(相当于不对齐),使得其大小变为6。
9、结构体大小计算习题
习题1
#include <stdio.h> struct S3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct S3)); return 0; }
d 从0偏移处开始存储,占8个字节,所以0~7;c 紧挨 d 存储,占一个字节,所以8,i 从4的整数倍即12处开始存储,占4个字节,所以12~15;所以0 ~ 15合计16个字节,16为最大对齐数8的倍数,所以不变。
习题2
#include <stdio.h> struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { printf("%d\n", sizeof(struct S4)); return 0; }
c1 从0偏移位置开始存储,占一个字节,所以0;struct S3 s3 我们上面已经算出占16个字节,又因为嵌套的结构体对齐到自己的最大对齐数的整数倍处,所以从8的整数倍即8偏移处开始存储,所以8~23;d 从8的整数倍即24偏移处开始存储,占8个字节,所以24~31;合计32个字节,且为最大偏移数8的整数倍,所以不变。
习题3
#include <stdio.h> #pragma pack(4) struct tagTest1 { short a; char d; long b; long c; }; struct tagTest2 { long b; short c; char d; long a; }; struct tagTest3 { short c; long b; char d; long a; }; #pragma pack() int main(int argc, char* argv[]) { struct tagTest1 stT1; struct tagTest2 stT2; struct tagTest3 stT3; printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3)); return 0; }
stT1:
a: 0~1 d:2 b:4~7 c:8~11 合计:0~11 = 12(4的倍数);
stT2:
b:0~3 c:4~5 d:6 a:8~11 合计:0~11 = 12(4的倍数);
stT3:
c:0~1 b:4~7 d:8 a:12~15 合计:0~15 = 16(4的倍数);