【C语言进阶】自定义类型:结构体、枚举、联合
本章目标:
学会计算结构体的大小(内存对齐),了解什么是位段和枚举,学会使用联合体并能计算联合体的大小。
本章重点:
-
结构体
- 结构体类型的声明
- 结构的自引用
- 结构体变量的定义和初始化
- 结构体内存对齐
- 默认对齐数
- 结构体传参
-
结构体实现位段(位段的填充&可移植性)
- 是什么是位段
- 位段的内存分配
- 位段的跨平台问题
- 位段的应用
-
枚举类型
- 枚举类型的定义
- 枚举的优点
- 枚举的使用
-
联合体(共用体)
- 联合类型的定义
- 联合的特点
- 联合大小的计算
一、结构体
1 结构体的声明
1.1 介绍
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的对象(普通变量、数组、指针、结构体等)。
1.2 结构体类型的声明
结构体类型的声明方式:
//语法形式
struct tag
{
member-list;
}variable-list;
//struct tag -- 结构体类型名
//tag -- 结构名
//member-list -- 成员列表
//variable-list -- 结构体变量列表
//注意结构体末尾的";"
示例:声明一个学生结构体类型
struct Stu
{
char name[20]; //姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
};
1.3 特殊的声明(匿名结构体类型)
在声明结构体的时候,可以不完全的声明。
省略结构名,即定义匿名结构体类型。
示例:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;//只能再此处创建匿名结构体对象
struct
{
int a;
char b;
float c;
}a[10], *p;
说明:
上述两个结构体在声明的时候,省略掉了结构体标签 tag
(即结构名)。
疑问:
p = &x;//是否合法?
//error,类型不兼容
警告:编译器会把上述两个匿名结构体当做不同的结构体类型。
2 结构体的自引用
在结构体中包含一个同类型的结构体成员(一般是同类型的结构体指针)。
2.1 示例1
示例:
struct Node
{
int data;
struct Node next;//不可行,sizeof(struct Node)的大小不确定
};
说明:上述结构体中包含自身类型的普通结构体变量是不可行的,因为这样就无法准确计算 sizeof(struct Node)
的大小。
2.2 示例2
示例:正确的自引用方式
struct Node
{
int data;
struct Node* Next;//可行,因为只要是指针变量,其大小都是4/8
};
说明:上述写法可行,因为无论何种类型的指针变量,其大小都是4/8个字节,那么此时就可以准确得计算出 sizeof(struct Node)
的大小。
2.3 示例3
示例:
//匿名结构体的重命名
typedef struct
{
int data;
Node* next;//不可行,此时Node未定义
}Node;
说明:不能在匿名结构体类型未重命名前就使用重命名之后的结构体类型名。
也就是说上述代码中,Node* next
的声明在类型重命名之前,而此时的 Node
是未知的(未定义的)。
解决方案:
//代码1
typedef struct
{
int data;
struct Node* next;
}Node;
//代码2
typedef struct Node//可以使用匿名结构体的重命名,但是添加上结构名比较好
{
int data;
struct Node* next;
}Node;
说明:建议采用代码2的书写规范。
3 结构体变量的定义和初始化
3.1 示例
代码示例:
#include<stdio.h>
struct Point
{
int x;
int y;
}p1;//在声明结构体的同时定义一个全局结构体变量
struct Point p2;//定义一个全局结构体变量
struct Point p3 = { 1,1 };//在定义变量的同时进行初始化(赋初始值)
struct Stu
{
char name[15];//姓名
int age;//年龄
};
struct Stu s = { "张三",18 };//全局结构体变量的初始化
struct Node
{
int data;
struct Point p;//一个结构体变量充当另一个结构体的成员
struct Node* next;//结构体的自引用
}n1 = { 10,{3,4},NULL };//结构体嵌套初始化
struct Node n2 = { 15,{3,2},NULL };//结构体嵌套初始化
//不按照成员顺序初始化的变量的初始化
struct Node n3 = { .next = NULL, .p.y = 2, .p.x = 2, .data = 20 };
int main()
{
//结构体变量的定义和初始化
printf("n3.data = %d\n" "n3.P.x = %d\n" "n3.p.y = %d\n" "n3.next = %p\n",
n3.data, n3.p.x, n3.p.y, n3.next);
return 0;
}
4 结构体内存对齐
如何计算机构体的大小:结构体的内存对齐。
4.1 offsetof()
原型:
offsetof (type,member);
介绍:
1 offsetof()
可以计算结构体成员相较于结构体起始位置的偏移量。
2 offsetof
是一种宏,所需包含的头文件是 <stddef.h>
。
4.2 练习题
4.2.1 练习1
代码示例:
#include<stdio.h>
#include<stddef.h>//offsetof
struct S1
{
char c1;//1 | 8 -> 1
int i; //4 | 8 -> 4
char c2;//1 | 8 -> 1
//成员 偏移量
//c1 --> 0
//浪费--> 1~3
//i --> 4~7
//c2 --> 8
//
//最大对齐数:1、4、1 --> 4
//
//9不是为4的倍数(0~8:9)
//浪费--> 9~11
//12是4的倍数(0~11:12)
//
};
int main()
{
//练习1
printf("%d\n", sizeof(struct S1));//结构体类型的大小:12
//offsetof —— 偏移量
printf("%d\n", offsetof(struct S1, c1));//0
printf("%d\n", offsetof(struct S1, i)); //4
printf("%d\n", offsetof(struct S1, c2));//8
return 0;
}
图示说明:
4.2.2 练习2
代码示例:
#include<stdio.h>
#include<stddef.h>//offsetof
struct S2
{
char c1;//1 | 8 -> 1
char c2;//1 | 8 -> 1
int i; //4 | 8 -> 4
//成员 偏移量
//c1 --> 0
//c2 --> 1
//浪费--> 2~3
//i --> 4~7
//
//最大对齐数:1、1、4 --> 4
//8是4的倍数(0~7:8)
//
};
int main()
{
//练习2
printf("%d\n", sizeof(struct S2));//结构体类型的大小:8
//offsetof —— 偏移量
printf("%d\n", offsetof(struct S2, c1));//0
printf("%d\n", offsetof(struct S2, c2));//1
printf("%d\n", offsetof(struct S2, i)); //4
return 0;
}
图示说明:
4.2.3 练习3
代码示例:
#include<stdio.h>
#include<stddef.h>//offsetof
struct S3
{
double d;//8 | 8 --> 8
char c; //1 | 8 --> 1
int i; //4 | 8 --> 4
//成员 偏移量
//d --> 0~7
//c --> 8
//浪费--> 9~11
//i --> 12~15
//
//最大对齐数:8、1、1 --> 8
//16是8的倍数(0~15:16)
//
};
int main()
{
//练习3
printf("%d\n", sizeof(struct S3));//结构体类型的大小:16
//offsetof —— 偏移量
printf("%d\n", offsetof(struct S3, d));//0
printf("%d\n", offsetof(struct S3, c));//8
printf("%d\n", offsetof(struct S3, i));//12
return 0;
}
图示说明:
4.2.4 练习4
代码示例:
#include<stdio.h>
#include<stddef.h>
struct S3
{
double d; //8 | 8 --> 8
char c; //1 | 8 --> 1
int i; //4 | 8 --> 4
//成员 偏移量
//d --> 0~7
//c --> 8
//浪费--> 9~11
//i --> 12~15
//
//最大对齐数:8、1、1 --> 8
//16是8的倍数(0~15:16)
//
};
struct S4
{
char c; //1 | 8 --> 1
struct S3 s;//8 | 8 --> 8
double d; //8 | 8 --> 8
//成员 偏移量
//c --> 0
//浪费--> 1~7
//s --> 8~23
//d --> 24~31
//
//最大对齐数:1、8、8 --> 8
//32是8的整数倍(0~31:32)
//
};
int main()
{
//练习4--嵌套结构体求大小
printf("%d\n", sizeof(struct S4));//结构体类型的大小:32
//offsetof —— 偏移量
printf("%d\n", offsetof(struct S4, c));//0
printf("%d\n", offsetof(struct S4, s));//8
printf("%d\n", offsetof(struct S4, d));//24
return;
}
图示说明:
4.3 如何计算结构体的大小?
结构体的对齐规则:
1.第一个结构体成员在相较于结构体变量起始位置偏移量为0的地址处。
2.其他成员要对齐到自身对齐数的整数倍的地址处。
对齐数:编译器默认的对齐数与结构体成员相比的较小者。
VS编译器中默认对齐数为8。
Linux中没有默认对齐数,对齐数就是成员自身的大小。
3.最大对齐数为所有成员中对齐数最大者。
4.结构体类型的大小为成员中最大对齐数的整数倍。
5.如果出现了结构体嵌套的情况,嵌套的结构体首先对齐到自身的最大对齐数的整数倍处(先处理并获得最内层的结构体的大小),而结构体的整体大小就是所有成员(包括成员是结构体的成员)中对齐数最大者的整数倍。
4.4 为什么会存在内存对齐?
4.4.1 介绍
一些参考资料是这么说的:
1.平台原因(移植原因)
不是所哟与的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问。
没有对齐,读取两次(从内存边界开始读取):
内存对齐,读取一次(从内存边界开始读取):
总体来说:
结构体的内存对齐是拿空间来换取时间的。
4.4.2 示例
在设计结构体的时候,能不能做到既能满足对齐,又能节省空间呢?
让占用空间较小的成员尽可能的集中在一起。
代码示例:
#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));//结构体S1的大小:12
printf("%d\n", sizeof(struct S2));//结构体S2的大小:8
return;
}
解释说明:
1 结构体S1和S2的成员一模一样,但是它们的大小却不相同。
让占用内存较小的成员尽量集中在一起。
5 修改默认对齐数
5.1 介绍
使用
#pragma
预处理指令,可以改变默认对齐数。
代码示例:
#include<stdio.h>
#pragma pack(8)//设置默认对齐数为8(默认对齐数原本就是8)
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为原始默认
#pragma pack(1)
struct S2
{
char c1;//1 | 1 --> 1
int i; //4 | 1 --> 1
char c2;//1 | 1 --> 1
//成员 偏移量
//c1 --> 0
//i --> 1~4
//c2 --> 5
//
//最大对齐数为:1、1、1 --> 1
//6是最大对齐数1的整数倍(0~5:6)
//
};
#pragma pack()//取消设置的默认对齐数,还原为原始默认
int main()
{
//修改默认对齐数
//#pragma pack()
printf("%d\n", sizeof(struct S1));//结构体S1的大小;12
printf("%d\n", sizeof(struct S2));//结构体S2的大小:6
return 0;
}
解释说明:
1 将默认对齐数设置为1,那么每个成员的对齐数都成了1,最大对齐数也是1。
2 此时就意味着没有内存对齐了。
结论:
在结构体的对齐方式不合适的时候,可以手动设置默认对齐数。
5.2 笔试题
百度笔试题:写一个宏,计算结构体中某变量相对于首地址(起始地址)的偏移量,并给处说明。
示例:offsetof
宏的实现
示例代码:
#include<stdio.h>
struct S
{
char c1;
int i;
char c2;
//1|8 ->1 0
//4|8 ->4 4
//1|8 ->1 8
};
#define OFFSETOF(type, member) (size_t)&(((type*)0)->member)
//成员相较于类型起始位置的偏移量
int main()
{
//模拟实现offset宏
printf("%d\n", OFFSETOF(struct S, c1)); //0
printf("%d\n", OFFSETOF(struct S, i)); //4
printf("%d\n", OFFSETOF(struct S, c2)); //8
return 0;
}
6 结构体传参
代码示例:
#include<stdio.h>
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;
}
解释说明:首选 print2
函数(传递结构体变量的地址)
函数在传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。
传递一个结构体对象时,如果结构体本身过大,那么参数在压栈的时候系统开销会比较大,会导致性能的下降。
结论:在结构体传参的时候,尽量传结构体变量的地址。
二、位段
1 什么是位段
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段” 或称 “位域”( bit field) 。利用位段能够用较少的位数存储数据。
位段的声明和结构体的声明是类似的,但却有两个不同之处:
1.位段的成员必须是
int
、unsigned int
、signed int
中的一种。2.位段的成员名后边有一个冒号和一个数字。
代码示例:
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
//数字表示大小 --> 二进制位
//2+5+10+30 --> 47bit
//
//第一次申请:32bit < 47bit
//第二次申请:32bit + 32bit > 47bit
//32bit+32bit --> 8byte
//
};
int main()
{
//位段
printf("%d\n", sizeof(struct A));//8
return 0;
}
2 位段的内存分配
1.位段的成员可以是
int
、unsigned int
、signed int
、char
等的类型。2.位段的空间上是按照需要并以每4个字节(
int
)大小或者1个字节(char
)的方式来开辟空间的。3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段。
代码示例:
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
//空间是如何开辟的?
//16bit --> 2Byte?
};
int main()
{
printf("%d\n", sizeof(struct S));//3
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
解释说明:
1 空间是如何开辟的?(猜测是16bit->2字节,但结果是3个字节)
2 位段成员最大存储数字:
成员 | 位段 | 最大数字 |
---|---|---|
a | 3 bit | 7(1+2+4) |
b | 4 bit | 15(1+2+4+8) |
c | 5 bit | 31(1+2+4+8+16) |
d | 4 bit | 15(1+2+4+8) |
3 存储结果:62 03 04
4 位段的数据存储图示:
3 位段的跨平台问题
1.
int
位段被当成有符号数(singned
)还是无符号数(unsigned
)是不确定的。2.位段中最大位的数目不能确定。(16位机器最大为16,32位机器最大为32;如果写成27 ,那么在16位机器下会出现问题)
3.位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,而第一个位段占用后剩余位无法容纳第二个位段时,是舍弃剩余的位还是利用剩余的位,这是不确定的。
总结:
跟结构体相比,位段可以达到同样的效果,并且可以很好地节省空间,但是存在跨平台的问题,而具体的平台有各自对应的代码实现。
4 位段的应用
IP数据包:
三、枚举
枚举:一一列举。
把可能的取值一一列举。
1 枚举类型的定义
代码示例:
#include<stdio.h>
enum Day//星期
{
//枚举常量
Mon,//0
Tue,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
MALE,
FEMALE,
SECRET
};
enum Color
{
RED = 3,
GREEn = 1,
BLUE = 5
};
int main()
{
//枚举类型
return 0;
}
解释说明:
1 以上定义的 enum Day
、 enum Sex
、 enum Color
都是枚举类型。
2 {}
中的成员是枚举类型变量可能的取值,即枚举常量。
3 这些枚举常量默认从0开始,往后依次递增1。
4 在声明枚举类型的同时,也可以给这些枚举常量赋初值。
2 枚举的优点
1.增加代码的可读性和可维护性。
2.和 #define
定义的标识符常量相比,枚举有类型检查,会更加严谨。
3.便于调试。( #define
预处理指令在预处理阶段就完成了替换 )
4.使用方便,一次可以定义多个变量。
3 枚举的使用
3.1 示例1
代码示例:
#include<stdio.h>
enum Color
{
RED = 11,
GREEN = 2,
BLUE = 5
};
int main()
{
//枚举的应用1
enum Color clr = GREEN;//一般只能用枚举常量给枚举类型的变量来赋值
clr = 5;//C语言中可以这样写;但在C++中有类型检查,会报错
return 0;
}
3.2 示例2
代码示例:
#include<stdio.h>
enum Day//星期
{
EXIT,
Mon,
Tue,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
//枚举的应用2
int day = 0;
do
{
printf("请输入星期(1~7):>");
scanf("%d", &day);
switch (day)
{
case Mon:
printf("星期一\n");
break;
case Tue:
printf("星期二\n");
break;
case Wed:
printf("星期三\n");
break;
case Thur:
printf("星期四\n");
break;
case Fri:
printf("星期五\n");
break;
case Sat:
printf("星期六\n");
break;
case Sun:
printf("星期日\n");
break;
case EXIT:
printf("成功退出!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (day);
return 0;
}
四、联合体(共用体)
1 联合体类型的定义
联合体也是一种自定义类型。
- 该类型定义的变量也会包含一系列成员,不过这些成员会共用同一块内存空间(联合体也称共用体)。
代码示例:
#include<stdio.h>
//联合体的声明
union Un
{
char c;
int i;
};
int main()
{
//联合体变量的定义
union Un un;
//计算联合体的大小
printf("%d\n", sizeof(un));//4
return 0;
}
解释说明:
1 联合体的大小竟然为最大类型成员的大小。
2 联合体的特点
联合体的成员共用同一块内存空间,那么一个联合体变量的大小至少是最大成员的大小。(因为联合体至少得有能力保存它的最大的成员)
在同一时间,一般只能使用联合体变量的一个成员。
2.1 示例1
代码示例:
#include<stdio.h>
union Un
{
int i;
char c;
};
union Un un;//全局联合体变量
int main()
{
//联合体的特点
//联合体成员的地址相同
printf("&un <--> %p\n", &un); //00A1A13C
printf("&(un.i) <--> %p\n", &(un.i));//00A1A13C
printf("&(un.c) <--> %p\n", &(un.c));//00A1A13C
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);//11223355
return 0;
}
解释说明:
1 联合体成员的地址相同,且与联合体变量的地址也相同。
printf("%p\n", &un);
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
//三者地址相同
图示说明:
2.2 笔试题
判断当前计算机的大小端存储。
代码示例1:使用联合体实现判断
#include<stdio.h>
//代码1
union Un
{
int i;
char c;
};
int main()
{
//判断大小端存储 —— 联合体实现
union Un un;
un.i = 1;
if (1 == un.c)
printf("小端存储\n");//输出结果
else
printf("大端存储\n");
return 0;
}
代码示例2:封装一个函数,使用联合体实现判断
#include<stdio.h>
//代码2
int check_sys()
{
union
{
int i;
char c;
}un = { .i = 1 };
return un.c;
}
int main()
{
//判断大小端存储 —— 封装一个函数,通过联合体实现
int ret = check_sys();
if (ret == 1)
printf("小端存储\n");//输出结果
else
printf("大端存储\n");
return 0;
}
代码示例3:其他实现
#include<stdio.h>
int main()
{
//判断大小端存储 —— 其他实现
int a = 1;
if (*(char*)&a == 1)
printf("小端存储\n");//输出结果
else
printf("大端存储\n");
return 0;
}
3 联合体大小的计算
-
联合体的大小至少是最大成员的大小。
-
当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
代码示例:
#include<stdio.h>
union Un1
{
char c[5]; //1 | 8 --> 1
int i; //4 | 8 --> 4
//最大对齐数为4
//5 > 4 --> 4*2=8 --> 8>5
//
};
union Un2
{
short c[7]; //2 | 8 --> 2
int i; //4 | 8 --> 4
//最大对齐数为4
//14>4 --> 4*4=16 --> 16>14
//
};
int main()
{
//联合体的大小
printf("%d\n", sizeof(union Un1));//联合体Un1的大小:8
printf("%d\n", sizeof(union Un2));//联合体Un2的大小:16
return 0;
}
图示说明:
1 联合体 Un1
的对齐:
2 联合体 Un2
的对齐:
总结:
本节介绍了C语言中自定义类型中的结构体、枚举、联合体;重点讲解了结构体的内存对齐和默认对齐数、结构体如何实现位段、联合体大小的计算。
感谢您的阅读!如有任何错误,欢迎您的批评指正!