C语言:结构体
摘要:本文系统介绍了C语言结构体的核心知识,包括结构体定义(支持嵌套)、初始化方式(完全/部分初始化)、成员访问运算符(.和->)、字节对齐规则(内存优化布局)以及结构体数组与传参技巧。重点讲解了memcpy和memset函数在结构体内存操作中的应用,对比了浅拷贝与深拷贝的区别,并提供了动态内存分配示例。掌握这些内容能有效提升结构体编程能力,为处理复杂数据结构和优化程序性能奠定基础。
一、结构体:用户自定义类型的 “组装器”
结构体的本质是将多个基础数据类型(如 int、char、float 等)或其他自定义类型 “组装” 在一起,形成一个新的复合类型。其定义语法如下:
struct 类型名
{
数据类型1 变量1;
数据类型2 变量2;
// ... 更多成员
};
示例:日期与人员信息的结构体定义
// 日期结构体:包含年、月、日
struct date
{
int year;
int month;
int day;
};
// 人员结构体:包含姓名、年龄、分数
struct Person
{
char name[50];
int age;
float score;
};
结构体的定义也支持嵌套,比如我们可以把struct date嵌套到struct Person中,用来表示人员的生日:
struct PersonWithBirth
{
char name[50];
int age;
float score;
struct date birth; // 嵌套日期结构体
};
二、结构体的初始化:从完全到部分
定义结构体后,我们需要对其变量进行初始化,C 语言提供了完全初始化和部分初始化两种方式。
1. 完全初始化
将结构体的所有成员逐一赋值,示例:
struct Person per = {"zhangsan", 20, 90.5};
// 嵌套结构体的完全初始化
struct PersonWithBirth per_birth = {"lisi", 22, 88.0, {2002, 5, 18}};
2. 部分初始化
未初始化的成员会被自动填充为 0,还可以通过指定成员名的方式灵活赋值:
struct Person per2 =
{
.name = "wangwu",
.age = 18,
// score未初始化,自动为0
};
// 嵌套结构体的部分初始化
struct PersonWithBirth per_birth2 =
{
.name = "zhaoliu",
.birth.year = 2001,
.birth.month = 8
// age、score、birth.day自动为0
};
三、结构体的成员访问:.与->的分工
访问结构体成员时,需根据变量是结构体变量还是结构体指针,选择不同的运算符。
1. 结构体变量:用.运算符
printf("姓名:%s,年龄:%d,分数:%f\n", per.name, per.age, per.score);
// 访问嵌套结构体成员
printf("姓名:%s,生日:%d年%d月%d日\n",
per_birth.name,
per_birth.birth.year,
per_birth.birth.month,
per_birth.birth.day);
2. 结构体指针:用->运算符
struct Person *point = &per;
printf("姓名:%s,年龄:%d,分数:%f\n", point->name, point->age, point->score);
// 访问嵌套结构体指针的成员
struct PersonWithBirth *p_birth = &per_birth;
printf("姓名:%s,生日:%d年%d月%d日\n",
p_birth->name,
p_birth->birth.year,
p_birth->birth.month,
p_birth->birth.day);
注意:per.score 和 point->score 都是表达式,其类型与成员变量(如 float)的类型一致,值为成员变量的具体值。
四、结构体字节对齐:为 CPU 高效读写 “铺路”
为了让 CPU 能高效地读写内存,结构体存在字节对齐规则,这是理解结构体大小的关键。
规则 1:成员变量的地址必须是 “自身类型大小的整数倍”
例如:
struct per2
{
char a; // char占1字节,地址需是1的整数倍(任意地址都满足)
short b; // short占2字节,地址需是2的整数倍,a后填充1字节
char c; // char占1字节,b后无填充
int d; // int占4字节,c后填充3字节
};
其内存布局因对齐规则会产生 “填充字节”,最终大小为12 字节(可结合内存示意图理解)。
规则 2:结构体最终大小必须是 “最大成员基础类型大小的整数倍”
例如:
struct per3
{
double a; // double占8字节
char c; // char占1字节,a后无填充
short s; // short占2字节,c后填充1字节
};
最大成员是double(8 字节),因此结构体最终大小需是 8 的整数倍,实际为16 字节(c 和 s 共占 4 字节,后填充 4 字节)。
调整字节对齐(编译器指令)
我们可以通过编译器指令(如#pragma pack)调整对齐规则,例如:
#pragma pack(1) // 设置按1字节对齐
struct per4
{
char a;
int b;
};
#pragma pack() // 恢复默认对齐
此时struct per4的大小为5 字节(无填充),但可能降低 CPU 读写效率。
五、结构体数组与传参:效率优先的实践
当处理多个结构体对象时,我们会用到结构体数组;而结构体传参时,为了效率通常选择地址传递。
示例:结构体数组定义与传参
struct PER
{
char name[50];
float height;
};
// 结构体数组传参:使用地址传递(避免拷贝,提升效率)
void show_array(struct PER *per, int len)
{
for (int i = 0; i < len; i++)
{
printf("第%d人:姓名%s,身高%.2f\n", i+1, per[i].name, per[i].height);
}
}
// 修改结构体数组元素
void update_height(struct PER *per, int index, float new_height)
{
per[index].height = new_height;
}
int main()
{
// 定义并初始化结构体数组
struct PER per[3] = {
{"zhangsan", 1.75},
{"lisi", 1.70},
{"wangmazi", 1.95}
};
// 直接访问数组元素
printf("初始化数据:\n");
for (int i = 0; i < 3; i++)
{
printf("第%d人:姓名%s,身高%.2f\n", i+1, per[i].name, per[i].height);
}
// 修改数组元素
update_height(per, 1, 1.78);
// 函数传参(地址传递)
printf("\n修改后数据:\n");
show_array(per, 3);
return 0;
}
六、结构体与指针的进阶应用:动态内存分配
结构体结合动态内存分配,可灵活管理数据,例如:
struct Student
{
char *name; // 动态分配字符串
int id;
};
int main()
{
struct Student *stu = (struct Student *)malloc(sizeof(struct Student));
if (stu == NULL) exit(1); // 检查分配是否成功
stu->name = (char *)malloc(20 * sizeof(char));
strcpy(stu->name, "sunqi");
stu->id = 2024001;
printf("学生:%s,学号:%d\n", stu->name, stu->id);
// 释放内存
free(stu->name);
free(stu);
stu = NULL; // 避免野指针
return 0;
}
七、memcpy 与 memset:结构体的内存操作利器
在处理结构体时,我们经常需要对内存进行批量复制或初始化,memcpy(内存拷贝)和memset(内存填充)是高效的内存操作函数,能简化结构体的赋值与初始化流程。
1. memset:批量初始化内存
memset用于将某一块内存的内容全部设置为指定的值(通常是 0),语法为:
void *memset(void *ptr, int value, size_t num);
ptr:要操作的内存起始地址value:填充的字节值(通常用 0 初始化)num:要填充的字节数
结构体初始化场景:当结构体成员较多时,用memset可快速将所有成员置 0,避免逐个赋值:
struct Person per;
// 将per的所有字节初始化为0(char数组、int、float均置0)
memset(&per, 0, sizeof(struct Person));
注意:memset按字节填充,若用于非字符类型(如 int),仅适合赋值 0 或 0xFF(全 1),否则可能出现意外结果(如用 1 填充 int 会变成0x01010101)。
2. memcpy:批量拷贝内存
memcpy用于将一块内存的内容拷贝到另一块内存,语法为:
void *memcpy(void *dest, const void *src, size_t num);
dest:目标内存地址src:源内存地址num:要拷贝的字节数
结构体拷贝场景:直接赋值结构体时(如per2 = per1),本质是浅拷贝;用memcpy可实现相同效果,尤其适合需要精确控制拷贝范围的场景:
struct Person per1 = {"zhangsan", 20, 90.5};
struct Person per2;
// 将per1的内容完整拷贝到per2
memcpy(&per2, &per1, sizeof(struct Person));
结构体数组拷贝:批量拷贝结构体数组时,memcpy比循环赋值更高效:
struct PER per[3] = {{"zhangsan", 1.75}, {"lisi", 1.70}, {"wangmazi", 1.95}};
struct PER per_copy[3];
// 拷贝整个结构体数组
memcpy(per_copy, per, sizeof(per));
注意事项:
memcpy是浅拷贝:若结构体包含指针(如char *name),仅拷贝指针地址,而非指针指向的内容,可能导致内存重复释放或野指针问题。此时需手动实现深拷贝(先拷贝指针指向的数据,再拷贝指针)。- 确保目标内存足够大,避免越界访问。
3. 结构体深拷贝示例(结合 memcpy 与动态内存)
若结构体含动态分配的指针成员,需用memcpy拷贝指针指向的数据:
struct Student stu1;
stu1.name = (char *)malloc(20);
strcpy(stu1.name, "sunqi");
stu1.id = 2024001;
struct Student stu2;
// 深拷贝:先分配内存,再拷贝内容
stu2.name = (char *)malloc(strlen(stu1.name) + 1);
memcpy(stu2.name, stu1.name, strlen(stu1.name) + 1); // 含'\0'
stu2.id = stu1.id;
八、memcpy/memset 的优势与适用场景
- 效率:两者均为库函数优化实现(通常是汇编级),比手动循环赋值更快,尤其适合大数据量的结构体 / 数组操作。
- 场景:
memset:结构体初始化、清空内存(如重置状态)。memcpy:结构体 / 数组的批量拷贝、内存块迁移。
总结
结构体是 C 语言中实现 “复合数据类型” 的核心工具,它让我们能按需组装数据,支持嵌套、动态内存分配等进阶操作。掌握类型定义、初始化、成员访问、字节对齐和数组传参这些知识点,就能灵活运用结构体解决复杂的编程问题,为后续学习链表、队列等高级数据结构打下坚实基础。结构体结合memcpy和memset,能更高效地管理内存数据。需注意浅拷贝与深拷贝的区别,避免因指针成员导致的内存问题。掌握这些工具,可进一步提升结构体操作的灵活性与性能,为复杂数据处理(如网络数据传输、文件解析)提供支撑。
更多推荐

所有评论(0)