9.C 结构体 共用体 枚举
C 结构体 共用体 枚举
1.结构体
C 数组允许定义可存储相同类型数据项的变量,结构体是 C 编程中另一种用户自定义的可用的数据类型,它允许存储不同类型的数据项。结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。
1.结构体类型定义
声明只是表示结构体的类型,并不占内存的空间,只有结构体变量初始化或者进行改变的时候,才会占用内存空间。
在C语言中,可以使用结构体来存放一组不同类型的数据。结构体的声明形式为:
struct 结构体名{
结构体所包含的变量或数组
};
2.结构体变量定义
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu {
int id;
char name[20];
// 其他成员...
};
// 定义结构体变量,这里必须使用 struct 关键字
struct stu stu1, stu2;
在这个例子中,声明和初始化 stu1 和 stu2 作为 struct stu 类型的变量时,struct 关键字是必需的。stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
也可以在定义结构体的同时定义结构体变量,将变量放在结构体定义的最后即可:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{ //没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
3.结构体变量的初始化
和其它类型变量一样,对结构体变量可以在定义时指定初始值。
#include <stdio.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
int main()
{
printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}
4.访问结构成员
为了访问结构的成员,我们使用成员访问运算符(.)
结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
#include <stdio.h>
int main(){
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
return 0;
}
5.指向结构的指针
当一个指针变量指向结构体时,我们就称它为结构体指针。方式与定义指向其他类型变量的指针相似,如下所示:
struct Books *struct_pointer;
struct 结构体名 *变量名;
可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,把 & 运算符放在结构名称的前面,如下所示:
struct_pointer = &Book1;
为了使用指向该结构的指针访问结构的成员,必须使用 -> 运算符,如下所示:
struct_pointer->title;
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;
也可以在定义结构体的同时定义结构体指针:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;
注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&
6.获取结构体成员
通过结构体指针可以获取结构体成员,一般形式为:
(*pointer).memberName
或者:
pointer->memberName
第一种写法中,.的优先级高于*,(*pointer)两边的括号不能少。如果去掉括号写作*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就完全不对了。
第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。
上面的两种写法是等效的,我们通常采用后面的写法,这样更加直观。
7.结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
8.结构体数组
一个结构体变量中可以存放一组数据(如一个学生的学号,姓名,成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数据值型数组不同之处在于每个数组元素都一个结构体类型的数据,它们分别包括各个成员项。
1.定义结构体数组
和定义结构体变量的方法相仿,只需说明其为数组即可。
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct student stu[3];
以上定义了一个数组 stu,其元素为 struct student 类型数据,数组有 3 个元素。也可以直接定义一个结构体数组。如:
struct student
{
int num;
....
}stu[3];
或
struct
{
int num;
...
}stu[3];
2.结构体数组的初始化
与其它类型数组一样,对结构体数组可以初始化如:
struct student
{
int mum;
char name[20];
char sex;
int age;
float score;
char addr[30];
}stu[3] = {{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}};
//当对数组中全部元素赋值时,也可不给出数组长度
struct student
{
int mum;
char name[20];
char sex;
int age;
float score;
char addr[30];
}stu[] = {{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}};
//结构体数组的使用也很简单,例如,获取 Wang ming 的成绩:
stu[2].sex = 'B';
9.浅拷贝和深拷贝
1.浅拷贝
浅拷贝是指简单地将源结构体的所有字段值按位复制到目标结构体中。如果结构体包含指针成员,那么浅拷贝仅复制指针本身的值(即内存地址),而不复制指针所指向的数据。这意味着复制后的结构体和源结构体共享相同的指针指向的内存区域。例如:
typedef struct {
int id;
char *name;
} Person;
Person p1 = {1, strdup("Alice")}; // 注意:使用strdup动态分配字符串内存
Person p2;
// 浅拷贝:仅复制p1的字段值到p2
p2 = p1;
// 此时,p1.name和p2.name都指向同一个字符串内存
在上述例子中,浅拷贝完成后,p1和p2的id字段各自拥有独立的副本,但它们的name字段都指向同一个动态分配的字符串。如果后续通过其中一个结构体修改或释放了该字符串,会影响到另一个结构体。
2.深拷贝
深拷贝则是创建源结构体的一个完全独立的副本,不仅复制所有字段值,还对指针成员所指向的数据进行复制。也就是说,如果结构体包含指针成员,深拷贝会为这些指针分配新的内存,并将原指针指向的数据复制到新内存中。这样,复制后的结构体与源结构体各自拥有独立的内存空间,修改一方不会影响另一方。例如:
Person p1 = {1, strdup("Alice")};
Person p2;
// 深拷贝:不仅复制p1的字段值,还为指针成员分配新内存并复制数据
p2.id = p1.id;
p2.name = strdup(p1.name);
// 此时,p1.name和p2.name分别指向不同的字符串内存,内容相同
在上述深拷贝示例中,p2.name通过strdup函数分配了新的内存并复制了p1.name指向的字符串内容。现在,即使修改或释放任何一个结构体的name字段,都不会影响另一个结构体。
总结来说,结构体指针变量赋值是单纯地复制指针本身的值(即内存地址),而浅拷贝和深拷贝是针对结构体实例的复制操作,区别在于是否复制指针成员所指向的数据。浅拷贝仅复制指针值,导致复制后的结构体与源结构体共享指针指向的数据;深拷贝则复制指针所指向的数据,确保复制后的结构体拥有独立的数据副本。
10.结构体变量堆区分配
结构体变量在堆区创建通常通过 malloc()、calloc() 或 realloc() 等内存分配函数实现。一旦结构体变量在堆区分配好,其成员变量自然也位于同一堆区内。操作这样的结构体变量及其成员变量时,需要注意以下几点:
1.分配结构体内存
使用内存分配函数为结构体变量分配内存。例如,假设有一个名为 MyStruct 的结构体类型:
typedef struct {
int a;
double b;
char *c;
} MyStruct;
MyStruct *struct_ptr = (MyStruct *)malloc(sizeof(MyStruct));
上述代码为 MyStruct 类型的结构体变量在堆区分配了足够的内存,并将分配得到的内存地址保存在指针 struct_ptr 中。
2.初始化结构体成员
结构体在堆区分配后,需要对其成员变量进行初始化。对于基本类型成员,可以直接赋值;对于指针成员,可能需要进一步分配内存并初始化。
struct_ptr->a = 10;
struct_ptr->b = 3.14;
// 对于指针成员 c,首先分配内存,然后初始化
struct_ptr->c = (char *)malloc(10 * sizeof(char));
strcpy(struct_ptr->c, "Hello");
上述代码分别初始化了整型成员 a、浮点型成员 b,以及字符指针成员 c。指针成员 c 先分配了足够的内存(这里假设最多存储10个字符),然后通过 strcpy() 函数将字符串 "Hello" 复制到分配的内存中。
3.访问和修改成员变量
通过结构体指针访问和修改成员变量,如同操作栈区的结构体变量一样,只需使用指针运算符 ->。
printf("a: %d, b: %.2f, c: %s\n", struct_ptr->a, struct_ptr->b, struct_ptr->c);
struct_ptr->a = 20;
struct_ptr->b = 2.71;
strcpy(struct_ptr->c, "World");
上述代码展示了如何通过指针 struct_ptr 访问和修改结构体成员变量的值,并打印结构体的状态。
4.释放结构体及成员指针所占内存
当结构体变量及其成员不再使用时,必须手动释放它们占用的内存,以避免内存泄漏。先释放成员指针所指向的内存,再释放结构体本身的内存。
free(struct_ptr->c); // 释放指针成员 c 分配的内存
free(struct_ptr); // 释放结构体变量本身占用的内存
注意释放顺序:先释放成员指针指向的内存,后释放结构体变量。如果顺序相反,结构体变量被释放后,其成员指针将变为无效,无法再正确释放指针所指向的内存。
11.结构体的自动对齐
先介绍以下概念:
-
自身对齐值:数据类型本身的对齐值,例如char类型的自身对齐值是1,short类型是2;
-
指定对齐值:编译器或程序员指定的对齐值,32位单片机的指定对齐值默认是4;
-
有效对齐值:自身对齐值和指定对齐值中较小的那个。
-
分配单位长度:结构体中最大的基本类型长度为结构体的分配单位长度(若有嵌套结构体成员,则包括嵌套的结构体中的最大成员)。
-
成员偏移量:成员自身有效对齐值的整数倍(若有嵌套结构体成员,则嵌套结构体的整体偏移量为该结构体中最大基本类型的整数倍)。
-
结构体大小:结构体的大小为最大数据成员的基本类型长度的整数倍,即分配单位长度的整数倍(嵌套结构体整体所占的大小为该结构体成员内部最大元素的整数倍,不足补齐)。
下面根据示例对自动对齐分为以下步骤:
struct Example {
char a; // 1字节,对齐要求为1字节
short b; // 2字节,对齐要求为2字节
double c; // 8字节,对齐要求为8字节
int d; // 4字节,对齐要求为4字节
}Example1;
下面是结构体Example自动对齐的详细步骤:
1. 确定分配单位长度
分配单位长度由结构体中最大的基本类型长度决定。在这个例子中,最大的基本类型是double,其长度为8字节。因此,结构体Example的分配单位长度为8字节。
2. 确定每个成员的有效对齐值
char a:自身对齐值为1,假设指定对齐值为4(32位单片机的默认指定对齐值),有效对齐值取两者中较小的,即有效对齐值为1。short b:自身对齐值为2,有效对齐值取两者中较小的,即有效对齐值为2。double c:自身对齐值为8,有效对齐值也为8。int d:自身对齐值为4,有效对齐值为4。
3. 根据有效对齐值确定成员偏移量
char a:作为结构体的第一个成员,其偏移量为0,满足其有效对齐值(1字节)。short b:紧随其后,其偏移量为1。为了满足有效对齐值(2字节),需要在a和b之间插入1个字节的填充,使b从偏移量2开始,满足2字节对齐。double c:其偏移量应为2+1=3。为了满足有效对齐值(8字节),需要在b和c之间插入5个字节的填充,使c从偏移量8开始,满足8字节对齐。int d:紧随其后,其偏移量为8+8=16。满足有效对齐值(4字节),无需额外填充。
4. 确认结构体大小:
结构体Example的大小为所有成员的大小(包括填充字节)之和,即:
a(1字节) + 填充(1字节) + b(2字节) + 填充(5字节) + c(8字节) + d(4字节)
= 1 + 1 + 2 + 5 + 8 + 4 = 21字节。
然而,结构体的总大小还必须是其分配单位长度(8字节)的整数倍。由于21不是8的整数倍,所以在d之后需要添加3个填充字节,使结构体总大小达到24字节,即:
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| a | | b | | | | | c | | | | | d | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
现在,结构体Example的总大小24字节是其分配单位长度(8字节)的整数倍(3倍),符合对齐规则。
12.结构体的强制对齐
结构体的强制对齐是指通过特定的编译器指令或属性,人为地指定结构体及其成员变量的对齐方式,以覆盖默认的自动对齐规则。这样做通常是为了满足特定硬件接口要求、节省内存、提高特定平台上的性能,或者确保跨平台数据交换的一致性。以下是结构体强制对齐的一些常见方法和注意事项:
方法一:使用编译器指令
许多编译器提供了预处理指令来改变结构体的对齐方式。例如,在C语言中,#pragma pack指令可用于设置结构体的对齐边界:
#pragma pack(push, n) // 开始一个新的对齐设置,n为所需的对齐值(如1、2、4、8等)
struct MyStruct {
...
};
#pragma pack(pop) // 恢复之前的对齐设置
在上述代码中,n表示新的对齐边界。#pragma pack(push, n)开启一个新的对齐设置,作用范围直到遇到相应的#pragma pack(pop)为止。在push和pop之间的结构体将按照指定的对齐值n进行对齐。如果不指定n,则表示使用最小可能的对齐(通常是1字节对齐)。
方法二:使用编译器特定的属性
某些编译器提供了特定的属性或关键字来强制对齐结构体。例如,在GCC和Clang中,可以使用__attribute__((aligned(n)))来指定结构体或成员变量的对齐方式:
struct __attribute__((aligned(8))) MyStruct {
...
};
上述代码中,__attribute__((aligned(8)))指定MyStruct结构体按照8字节对齐。同样,该属性也可以用于单个成员变量,强制其按照特定对齐值进行对齐。
注意事项:
-
性能影响:强制对齐可能会改变结构体的内存布局,导致访问效率降低。特别是当强制对齐值小于成员变量的自然对齐值时,可能违反处理器的最佳访问模式,增加内存访问成本。
-
内存开销:强制对齐可能导致结构体大小增加,因为编译器可能需要插入更多的填充字节来满足新的对齐要求。在内存资源有限的环境中,这可能成为一个问题。
-
兼容性问题:强制对齐可能引入平台依赖性,使得代码在不同的编译器版本、目标平台或编译选项下表现不一致。在编写跨平台代码或依赖特定硬件接口时,需要特别注意对齐要求的一致性。
-
异常处理:在某些严格要求数据对齐的硬件平台上,强制对齐不当可能导致未对齐访问异常。在使用强制对齐时,应确保结构体及其成员变量在所有预期的访问场景下都符合硬件对齐要求。
-
明确意图:在代码中使用强制对齐时,最好通过注释明确说明原因和对齐值,以便其他开发者理解并避免意外修改。
2.位域
位域(bit field)是C语言中一种特殊的结构体成员定义方式,允许在结构体中精确地控制成员变量所占用的二进制位数。以下是关于位域的定义、存储和使用的详细说明:
1.位域的定义
位域在结构体或联合体定义中通过特殊的语法来指定成员变量所占用的位数。基本语法如下:
struct BitFieldStruct {
type member_name : width;
};
其中:
type:位域成员的数据类型,通常为intunsigned intsigned int或char,也可以是_Bool(C99及以后版本)。member_name:位域成员的名称,用于标识和访问该位域。位域可以是无名位域,这时它只用来作填充或调整位置。width:位域成员的位宽,即该成员占用的二进制位数,是一个无符号整数常量表达式。位域的宽度不能超过它所依附的数据类型的长度,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。
例如:
struct Flags {
unsigned int is_enabled : 1; // 占用1位的无符号整数位域
unsigned int has_error : 1; // 另一个占用1位的无符号整数位域
unsigned int color : 3; // 占用3位的无符号整数位域,表示颜色索引
};
一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 从下一单元开始存放 */
unsigned c:4
}
在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。
2.位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名.位域名
位域变量名->位域名
位域变量也是可以使用指针的,但是位域成员变量不能使用指针。因为位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
struct Flags flags;
// 设置位域值
flags.is_enabled = 1; // 设置is_enabled位域为1(启用)
flags.has_error = 0; // 设置has_error位域为0(无错误)
flags.color = 3; // 设置color位域为3(假设3代表某种颜色)
// 读取位域值
if (flags.is_enabled) {
printf("Flags structure is enabled.\n");
}
int temp = flags.color;
位域也可以通过位操作(如按位与、按位或、按位异或等)来进行更复杂的位级操作:
struct Flags flags;
// 修改位域值
flags.is_enabled |= 1; // 置位is_enabled位域
flags.has_error &= ~1; // 清零has_error位域
// 检查位域状态
if ((flags.color & 0x3) == 0x⅓) {
printf("Color bit field has a specific value.\n");
}
注意事项
- 位域宽度限制:位域的宽度不能超过其所在整数类型的位数。超出范围的位宽定义将导致编译错误。
- 未初始化的位域:与普通变量一样,未初始化的位域其值是未定义的。在使用位域前,确保对其进行适当的初始化。
- 编译器实现差异:尽管C标准对位域的语法做了规定,但具体实现细节(如位域的内部排列、填充位的处理等)可能因编译器和平台而异。跨平台编程时需谨慎处理。
- 数据类型选择:根据位域所代表的数据性质(如是否需要负值、所需表示的范围等),选择合适的整数类型(如
unsigned int、signed int或_Bool)。
应用场合
位域广泛应用于需要高效利用内存和操作二进制位的场景,如:
- 存储一组布尔标志(如文件权限、状态标志等)。
- 实现紧凑的数据编码方案(如网络协议头、压缩数据格式等)。
- 与硬件寄存器交互,直接操作硬件寄存器中的特定位。
3.共用体
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
1.共用体类型定义
为了定义共用体,必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
union 共用体名{
成员列表
};
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
虽然所有成员占用同一块内存,但是各个成员所访问到内存空间的大小由自身的类型决定。
2.共用体变量定义
共用体也是一种自定义类型,可以通过它来创建变量,例如:
union data{
int n;
char ch;
double f;
};
union data a, b, c;
上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:
union data{
int n;
char ch;
double f;
} a, b, c;
如果不再定义新的变量,也可以将共用体的名字省略:
union{
int n;
char ch;
double f;
} a, b, c;
3.访问共用体成员
为了访问共用体的成员,我们使用成员访问运算符(.)。
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
除了用变量名访问共用体和结构体,还可以用指针访问
union 共用体类型名 *共用体指针名;
指针名=&共用体变量名
(*指针名).成员名
或
指针名->成员名
4.枚举
1.枚举类型定义
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。
枚举语法定义格式为:
enum 枚举名 {枚举元素1,枚举元素2,……};
enum DAY
{
MON=1,
TUE,
WED,
THU,
FRI,
SAT,
SUN
};
注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。如果没有指定,那么默认从 0 开始递增。
也可以在定义枚举类型时改变枚举元素的值:
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
- 枚举列表中的标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
- Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
- 枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
2.枚举变量的定义
我们可以通过以下三种方式来定义枚举变量
1、先定义枚举类型,再定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
2、定义枚举类型的同时定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
3、省略枚举名称,直接定义枚举变量
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

浙公网安备 33010602011771号