【C语言】自定义类型(结构体/枚举体/共用体)
2026/7/17 12:52:29 网站建设 项目流程

目录

结构体

结构体的定义

typedef

匿名结构体

结构体的自引用

结构体变量的初始化

访问结构体成员的方式

结构体的内存对齐

如何设计结构体

位段

枚举体

1. 基本语法

2. 定义和使用示例

2.1 简单枚举

2.2 指定枚举值

3. 枚举变量定义方式

枚举 vs 宏定义

共用体

共用体的定义

共用体大小的计算


结构体

结构体是什么?

结构是一些值的集合 , 这些值称为成员变量。结构的每个成员可以是不同类型的变量。

数组也是一些值的集合,但这些值的类型是相同的。

结构体的定义

struct 结构体名 { 成员表列; }变量表列;

定义结构体的例子:

struct Stu { int age; char name[20]; }s1,s2;

定义了一种结构体叫 Stu ,它有两个成员:age 和 name ,类型分别是 int 和 char [20],随后定义了两个结构体变量 s1 和 s2。

如果结构体定义在 main 函数之前,那么 s1 和 s2 就是全局变量。结构体也可以在 main 函数内部定义。

定义结构体变量可以在定义结构体时就定义,如 s1 和 s2,也可以在 mian 函数内定义:

struct Stu { int age; char name[20]; }s1,s2; int main() { struct Stu s3, s4; return 0; }

typedef

如果每次定义结构体变量的时候不想写 struct ,可以在定义结构体变量时写上一个 typedef,如:

typedef struct student {...}Stu;

下次定义结构体变量时,就只需要写 Stu s1;而不需要写 struct student s1;

typedef 也可以在定义结构体之后再使用:

struct student {...}; typedef struct student stu;

分析以下代码:

typedef struct Node { int data; Node* next; }Node;

代码的原意是将结构体 struct Node 命名为 Node ,在定义结构体时就直接用 Node 来定义 next 指针,但这种做法是不合法的。

匿名结构体

匿名结构体就是在定义结构体时,不指定结构体的名字:

struct { int age; char name[20]; }s1,s2;

这种结构体只有在定义结构体后就定义结构体变量,而不能在其他地方定义结构体变量。

分析以下代码:

struct { int age; char name[20]; }s1; struct { int age; char name[20]; }*p; int main() { p = &s1; return 0; }

s1 和 p 的结构是一样的,但 p = &s1 ;是不合法的。

结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

struct Node { int data; struct Node next; };

这是不行的,因为这种结构体类型的大小是无穷大的,但是我们可以把 struct Node next 改为

struct Node * next;这样这种结构体类型的大小就是 8 或 12 了,并且这种结构体就叫作链表。

链表

struct Node { int data; struct Node *next; };

结构体变量的初始化

示例:

struct Stu { int age; char name[20]; }s1 = { 21,"zhangsan" }; struct Stu s2 = { 12,"wangwu" }; int main() { struct Stu s3 = { 18,"lisi" }; return 0; }

即按照定义结构体时成员变量的顺序,将要初始化的内容用大括号括起来,并且用逗号隔开。

如果结构体内定义有结构体,则初始化这种结构体时对结构体内的成员结构体初始化要加大括号:

struct A { int a; char arr1[20]; }; struct B { int b; struct A a1; char arr2[20]; }; struct B b1 = { 12,{30,"hehe"},"haha" };

可以不严格地按照定义结构体时成员的顺序来初始化,用成员运算符即可:

struct B b1 = { .a1.a = 30,.a1.arr1 = "hehe",.b = 12,.arr2 = "haha" };

访问结构体成员的方式

使用成员运算符 ‘ . ' 访问:

用上面的结构体变量 b1 为例,用 printf 输出 b1 的内容:

printf("%d %d %s %s\n", b1.b, b1.a1.a, b1.a1.arr1, b1.arr2);

即:结构体变量名 . 结构体成员名,如果要访问结构体内的结构体变量,则需要使用多个成员运算符。

通过结构体指针访问结构体成员:

使用 "->" 运算符访问:

用上面的结构体变量 b1 为例:

string B* pb = &b1; printf("%d",pb->b1);

即:结构体指针名 -> 结构体成员名,-> 的作用相当于(*结构体指针名) . 结构体成员名

结构体的内存对齐

先来看一个例子:

struct A { char b; int a; char c; }; struct B { char b; char c; int a; }; int main() { printf("%d\n",sizeof(struct A)); printf("%d\n",sizeof(struct B)); return 0; }

最后打印的结果是:12 和 8。

产生了两个疑问:

1、为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但打印的结果不同。

2、为什么打印的结果不是 6 而是 12 和 8 ?

结构体的对齐规则

1. 第一个成员在与结构体变量偏移量为 0 的地址处。

2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(如果该成员是数组类型,就将数组元素的类型与编译器默认的一个对齐数比较)。
ㅇVS中默认对齐数为 8

ㅇLinux gcc : 没有默认对齐数 , 对齐数就是结构体成员的自身大小

3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 与默认对齐数取小后的数的整数倍。

4. 如果嵌套了结构体的情况 , 嵌套的结构体对齐到自己的每个成员的对齐数的最大值的整数倍处 , 结构体的整体大小就是所有成员的对齐数的最大值 ( 含嵌套结构体的对齐数, 嵌套结构体的对齐数就是自己的每个成员的对齐数的最大值) 的整数倍。

易错:第一个成员的偏移量是 0 !

具体解释:

1. 第一个成员在与结构体变量偏移量为 0 的地址处。

所谓的偏移量,就是指某个内存单元与结构体变量第一个成员的存储单元的距离。

拿上面的 struct A 举例:

第一个成员的类型占多少字节就占多少格子,如果 struct A 的第一个成员是 int 类型的,就占 4 个格子。

2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
ㅇVS中默认对齐数为 8

也就是从第二个成员开始 , 每个成员都要对齐到 ( 一个对齐数 ) 的整数倍处

struct A 的第二个成员,也就是 int a,a 的大小是 4 个字节,现在使用 VS ,默认对齐数是 8,取较小值 4,也就是说 a 的存储空间的偏移量只要是 4 的最小倍数就行了,char c 同理。

3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 的整数倍。

这就非常好理解了,struct A 中 b 的对齐数是 1,a 的对齐数是 4,c 的对齐数也是 1,所以最大对齐数是 4, struct A 的总大小应该为 4 的整数倍,而现在 struct A 的总大小是 9,因此 struct A 应该再占 3 个字节,总大小变为 12 才对。

这就是为什么 struct A 的总大小是 12 而不是 6 的原因,同样,为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但大小不同也能解释了。

使用 offsetof 验证

offsetof 是一个宏,使用它需要包含头文件:#include <stddef.h>

它是用来计算结构体成员的偏移量的,它有两个参数,第一个参数是结构体名,第二个参数是结构体成员名,函数返回值就是偏移量,且是整形。以 struct A 为例:

为什么要内存对齐?

大部分的参考资料都是这样说的:

1. 平台原因 ( 移植原因 ) :

不是所有的硬件平台都能访问任意地址上的任意数据的 ; 某些硬件平台只能在某些地址处取某些特定类型的数据 , 否则抛出硬件异常。

2. 性能原因 :

数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于 :为了访问未对齐的内存 , 处理器需要作两次内存访问 ; 而对齐的内存访问仅需要一次访问。

总体来说 :
结构体的内存对齐是拿空间来换取时间的做法。

如何设计结构体

那在设计结构体的时候 , 我们既要满足对齐 , 又要节省空间 , 如何做到?

让占用空间小的成员尽量集中在一起。

例如上述的 struct A 和 struct B,它们的成员一模一样,只是顺序不同,但两者的大小有所区别,struct A 占 12 个字节,而 struct B 只占 8 个字节。

修改默认对齐数

可以使用#pragma pack( )修改默认对齐数:

#pragma pack(1)//修改默认对齐数为 1 struct B { char b; char c; int a; }; #pragma pack()//恢复默认对齐数为 8

结构在对齐方式不合适的时候,可以更改默认对齐数。

结构体传参

结构体传参建议将函数参数设置为结构体指针变量

原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

位段

位段的定义

位段的定义和结构体是类似的 , 但有两个不同:

1.位段的成员类型必须是 int、unsigned int 、char、unsigned char 。
2.位段的成员名后边有一个冒号和一个数字(数字称为位,位的单位是比特,位不能超过该类型的大小)

例子:

struct A { int _a : 1; int _b : 5; int _c : 10; int _d : 30; };

A 就是一个位段 ,_a 是命名习惯。

位段的作用(优点)

有时候一个结构体中的 int 类型的数据不需要 32 个 bit 位来存储数据,而只需要少于 32 个 bit 位就可以完整的存储数据,此时就可以使用位段来节省空间,即指定一个 int 类型的变量的实际大小。上述 A 位段中,a 变量的实际大小就是 1 bit。

位段的空间分配

以上述位段 A 为例:先开辟一个 int 类型,即 32 个 bit 位,_a 使用这 32 个 bit 位的 1 位,而到底是从左开始使用还是从右开始使用取决于编译器(VS 从右开始使用),_b 使用 5 位,_c 使用 10 位,还剩 16 位,而 _d 要使用 30 个 bit 位,明显不够,再开辟一个 int 类型,即 32 位,而 _d 到底是使用剩余的 16 位和新开辟的空间的 14 位,还是直接全部使用新开辟的空间(VS 使用这种方式)取决于编译器。

位段的缺点

1、位段中 int 类型被当成是无符号还是有符号是不确定的。

2、在 16 位机器中 int 类型大小是 16 bit,32 位和 64 位机器中 int 类型的大小是 32 bit,假如指定 位为 27,在 16 位机器上会出现问题。

3、对于新开辟的空间,到底是从左开始使用还是从右开始使用取决于编译器。

4、当空间不够用时,到底是使用剩余的位和新开辟的空间的位,还是直接全部使用新开辟的空间的位取决于编译器。

枚举体

1. 基本语法

enum 枚举名 { 标识符1, 标识符2, ... 标识符n };

2. 定义和使用示例

2.1 简单枚举
#include <stdio.h> // 定义星期枚举 enum Weekday { MON, // 默认值为0 TUE, // 1 WED, // 2 THU, // 3 FRI, // 4 SAT, // 5 SUN // 6 }; int main() { enum Weekday today = WED; // enum Weekday today = 2; error:不能用 int 初始化 // 只能用定义的标识符 printf("today = %d\n", today); // 输出: 2 // 枚举比较 if (today == WED) { printf("今天是星期三\n"); } return 0; }
2.2 指定枚举值
#include <stdio.h> enum Color { RED = 1, GREEN = 3, BLUE = 5, YELLOW, // 自动为6 BLACK = 10, WHITE // 自动为11 };

3. 枚举变量定义方式

3.1 先定义类型,再定义变量

enum Season {SPRING, SUMMER, AUTUMN, WINTER}; enum Season s1, s2;

3.2 定义类型同时定义变量

enum Season {SPRING, SUMMER, AUTUMN, WINTER} s1, s2;

3.3 匿名枚举

enum {SPRING, SUMMER, AUTUMN, WINTER} s1, s2;

3.4 typedef定义枚举别名

typedef enum { SPRING, SUMMER, AUTUMN, WINTER } Season; Season s1 = SPRING;

枚举 vs 宏定义

// 使用宏定义 #define MON 0 #define TUE 1 #define WED 2 // 使用枚举 enum Weekday {MON, TUE, WED}; // 枚举的优势: // 1. 自动递增 // 2. 调试时可显示符号名称 // 3. 更好的类型检查 // 4. 作用域控制

共用体

共用体的定义

定义的基本形式与结构体相似:

union A { int a; char b; };

可以看到成员 c 和 i 共用同一段内存。

共用体大小的计算

共用体也存在内存对齐

1、共用体的大小至少是最大成员的大小。

2、当最大成员大小不是最大对齐数的整数倍的时候 , 就要对齐到最大对齐数的整数倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询