C语言结构体与共用体的深度解析及实战应用

💡 学习目标:掌握结构体与共用体的定义和使用方法,理解二者的内存布局差异,能够在实际开发中灵活运用结构体和共用体组织复杂数据;学习重点:结构体的定义与初始化、结构体指针的使用、结构体嵌套、共用体的特性及应用场景。
42.1 结构体的基本概念与定义
在C语言中,基础数据类型只能存储单一类型的数据。当需要描述一个包含多种属性的复杂对象时,就需要用到结构体。结构体可以将不同类型的数据组合成一个有机的整体。
42.1.1 结构体的定义语法
💡 结构体的定义使用 struct 关键字,语法格式如下:
struct 结构体名 {
数据类型 成员名1;
数据类型 成员名2;
// ... 更多成员
};
参数说明
结构体名:自定义的标识符,用于标识结构体类型。成员名:结构体内部的变量名称,成员可以是任意基础数据类型或自定义类型。
示例:定义学生结构体
#include <stdio.h>
// 定义学生结构体
struct Student {
// 姓名
char name[20];
// 年龄
int age;
// 学号
int id;
// 成绩
float score;
};
int main() {
printf("学生结构体大小:%zu 字节\n", sizeof(struct Student));
return 0;
}
运行结果:
学生结构体大小:28 字节
💡 技巧:结构体的大小不是成员变量大小的简单相加,而是存在内存对齐机制。目的是为了提高CPU的访问效率,这是我们后面要重点讲的内容。
42.1.2 结构体的内存对齐规则
⚠️ 注意:内存对齐是结构体的核心特性,也是面试的高频考点。C语言默认的对齐规则如下:
- 结构体的第一个成员,对齐到结构体变量起始地址的0偏移处。
- 其他成员变量,要对齐到自身数据类型大小的整数倍地址处。
- 结构体的总大小,必须是结构体中最大基本类型成员大小的整数倍。
示例:验证内存对齐
#include <stdio.h>
struct Test1 {
// 占1字节
char a;
// 占4字节
int b;
// 占2字节
short c;
};
struct Test2 {
char a;
short c;
int b;
};
int main() {
printf("struct Test1 大小:%zu 字节\n", sizeof(struct Test1));
printf("struct Test2 大小:%zu 字节\n", sizeof(struct Test2));
return 0;
}
运行结果:
struct Test1 大小:12 字节
struct Test2 大小:8 字节
✅ 结论:调整结构体成员的顺序,可以改变结构体的总大小。在实际开发中,合理排列成员顺序能够节省内存空间。
42.2 结构体变量的定义与初始化
定义结构体类型后,我们就可以创建结构体变量,并对其进行初始化操作。结构体变量的初始化有多种方式,我们逐一讲解。
42.2.1 结构体变量的定义方式
💡 结构体变量的定义有三种常见方式:
- 先定义结构体类型,再定义变量
- 定义结构体类型的同时定义变量
- 匿名结构体定义变量
示例:三种定义方式对比
#include <stdio.h>
// 方式1:先定义类型,再定义变量
struct Person {
char name[20];
int age;
float height;
};
// 方式2:定义类型的同时定义变量
struct Book {
char title[50];
char author[20];
float price;
} book1, book2;
// 方式3:匿名结构体定义变量
struct {
int x;
int y;
} point1, point2;
int main() {
// 方式1定义的变量
struct Person p1;
return 0;
}
💡 技巧:匿名结构体的缺点是只能使用一次,无法在其他地方定义新的变量。适合只需要创建少量变量的场景。
42.2.2 结构体变量的初始化
结构体变量的初始化可以使用 {} 进行赋值,支持顺序初始化和指定成员初始化两种方式。
示例:结构体变量的初始化
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int age;
int id;
float score;
};
int main() {
// 方式1:顺序初始化,必须与结构体成员顺序一致
struct Student s1 = {"张三", 18, 2024001, 95.5};
// 方式2:指定成员初始化,顺序可以任意
struct Student s2 = {
.id = 2024002,
.name = "李四",
.score = 88.0,
.age = 19
};
// 方式3:先定义,后赋值
struct Student s3;
strcpy(s3.name, "王五");
s3.age = 20;
s3.id = 2024003;
s3.score = 92.0;
// 输出结构体内容
printf("学生1:%s %d %d %.1f\n", s1.name, s1.age, s1.id, s1.score);
printf("学生2:%s %d %d %.1f\n", s2.name, s2.age, s2.id, s2.score);
printf("学生3:%s %d %d %.1f\n", s3.name, s3.age, s3.id, s3.score);
return 0;
}
运行结果:
学生1:张三 18 2024001 95.5
学生2:李四 19 2024002 88.0
学生3:王五 20 2024003 92.0
⚠️ 注意:使用字符串给 char 数组成员赋值时,不能直接用 =,需要使用 strcpy 函数。否则会导致编译错误。
42.3 结构体指针的使用
结构体指针是指向结构体变量的指针,它存储的是结构体变量的首地址。使用结构体指针访问成员可以提高程序的执行效率,是实际开发中的常用方式。
42.3.1 结构体指针的定义与成员访问
💡 结构体指针的定义语法:struct 结构体名 *指针变量名;
访问结构体指针成员的两种方式:
- 解引用方式:
(*指针变量名).成员名 - 箭头方式:
指针变量名->成员名
示例:结构体指针的使用
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int age;
float score;
};
int main() {
struct Student s;
// 定义结构体指针,指向结构体变量s
struct Student *p = &s;
// 方式1:通过结构体变量访问成员
strcpy(s.name, "赵六");
s.age = 21;
s.score = 90.5;
printf("方式1:%s %d %.1f\n", s.name, s.age, s.score);
// 方式2:通过结构体指针解引用访问成员
strcpy((*p).name, "孙七");
(*p).age = 22;
(*p).score = 87.0;
printf("方式2:%s %d %.1f\n", (*p).name, (*p).age, (*p).score);
// 方式3:通过箭头访问成员(推荐使用)
strcpy(p->name, "周八");
p->age = 23;
p->score = 93.0;
printf("方式3:%s %d %.1f\n", p->name, p->age, p->score);
return 0;
}
运行结果:
方式1:赵六 21 90.5
方式2:孙七 22 87.0
方式3:周八 23 93.0
✅ 结论:箭头运算符 -> 是访问结构体指针成员的简洁方式,在实际开发中推荐使用。
42.3.2 动态结构体指针
结合前面学习的动态内存分配,我们可以创建动态结构体指针,在堆区分配内存,灵活管理结构体的生命周期。
示例:动态结构体指针的使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 动态分配内存
struct Student *p = (struct Student *)malloc(sizeof(struct Student));
if (p == NULL) {
printf("内存申请失败!\n");
return 1;
}
// 给动态结构体赋值
strcpy(p->name, "吴九");
p->age = 24;
p->score = 89.5;
printf("动态结构体:%s %d %.1f\n", p->name, p->age, p->score);
// 释放内存
free(p);
p = NULL;
return 0;
}
运行结果:
动态结构体:吴九 24 89.5
💡 技巧:动态结构体指针是实现链表、树等复杂数据结构的基础。掌握它的使用方法,是C语言进阶的关键一步。
42.4 结构体嵌套与结构体数组
结构体的成员可以是另一个结构体,这就是结构体嵌套。当需要存储多个同类型结构体数据时,可以使用结构体数组。
42.4.1 结构体嵌套
示例:嵌套结构体表示学生信息
#include <stdio.h>
#include <string.h>
// 定义日期结构体
struct Date {
int year;
int month;
int day;
};
// 定义学生结构体,嵌套日期结构体
struct Student {
char name[20];
int age;
// 生日
struct Date birthday;
float score;
};
int main() {
struct Student s;
strcpy(s.name, "郑十");
s.age = 25;
// 访问嵌套结构体成员
s.birthday.year = 1999;
s.birthday.month = 10;
s.birthday.day = 1;
s.score = 91.0;
printf("姓名:%s\n", s.name);
printf("年龄:%d\n", s.age);
printf("生日:%d-%d-%d\n", s.birthday.year, s.birthday.month, s.birthday.day);
printf("成绩:%.1f\n", s.score);
return 0;
}
运行结果:
姓名:郑十
年龄:25
生日:1999-10-1
成绩:91.0
42.4.2 结构体数组
示例:结构体数组存储多个学生信息
#include <stdio.h>
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 定义并初始化结构体数组
struct Student stu_arr[3] = {
{"张三", 18, 95.5},
{"李四", 19, 88.0},
{"王五", 20, 92.0}
};
// 遍历结构体数组
for (int i = 0; i < 3; i++) {
printf("第%d个学生:%s %d %.1f\n", i+1, stu_arr[i].name, stu_arr[i].age, stu_arr[i].score);
}
// 使用指针遍历结构体数组
struct Student *p = stu_arr;
for (int i = 0; i < 3; i++) {
printf("指针访问:%s %d %.1f\n", p->name, p->age, p->score);
p++;
}
return 0;
}
运行结果:
第1个学生:张三 18 95.5
第2个学生:李四 19 88.0
第3个学生:王五 20 92.0
指针访问:张三 18 95.5
指针访问:李四 19 88.0
指针访问:王五 20 92.0
💡 技巧:结构体数组的数组名同样是常量指针,指向数组的第一个元素。使用指针遍历结构体数组可以提高代码效率。
42.5 共用体的定义与特性
共用体(Union)也是一种自定义数据类型,它可以存储不同类型的数据,但同一时刻只能存储其中一种类型的数据。共用体的大小等于最大成员的大小。
42.5.1 共用体的定义与使用
💡 共用体的定义使用 union 关键字,语法格式与结构体类似:
union 共用体名 {
数据类型 成员名1;
数据类型 成员名2;
// ... 更多成员
};
示例:共用体的基本使用
#include <stdio.h>
union Data {
char c;
int i;
float f;
};
int main() {
union Data d;
printf("共用体大小:%zu 字节\n", sizeof(d));
// 存储字符类型
d.c = 'A';
printf("存储字符:%c\n", d.c);
// 存储整型,覆盖之前的字符数据
d.i = 100;
printf("存储整数:%d\n", d.i);
// 此时访问c,数据已经被覆盖
printf("覆盖后访问c:%c\n", d.c);
// 存储浮点型,覆盖之前的整数数据
d.f = 3.14f;
printf("存储浮点数:%.2f\n", d.f);
printf("覆盖后访问i:%d\n", d.i);
return 0;
}
运行结果:
共用体大小:4 字节
存储字符:A
存储整数:100
覆盖后访问c:d
存储浮点数:3.14
覆盖后访问i:1078523331
✅ 结论:共用体的所有成员共享同一块内存空间,修改一个成员会影响其他成员的值。
42.5.2 共用体的应用场景
⚠️ 注意:共用体的核心优势是节省内存空间,常用于以下场景:
- 数据类型转换:利用共用体的内存共享特性,实现不同数据类型之间的转换。
- 节省内存:当数据有多种类型,但同一时刻只使用其中一种时,使用共用体可以节省内存。
- 判断系统大小端:这是共用体的经典应用,也是面试高频考点。
示例:使用共用体判断系统大小端
#include <stdio.h>
union EndianTest {
int i;
char c;
};
int main() {
union EndianTest et;
et.i = 1;
// 小端:低字节存低地址;大端:高字节存低地址
if (et.c == 1) {
printf("当前系统是小端模式\n");
} else {
printf("当前系统是大端模式\n");
}
return 0;
}
运行结果:
当前系统是小端模式
💡 技巧:目前绝大多数计算机系统都采用小端模式,如x86架构。大端模式主要用于一些嵌入式设备和网络传输中。
42.6 结构体与共用体的实战案例
42.6.1 案例1:通讯录管理系统(结构体数组版)
🔧 需求:实现一个简单的通讯录管理系统,支持添加、显示、查询联系人信息。
#include <stdio.h>
#include <string.h>
#define MAX 100
// 定义联系人结构体
struct Contact {
char name[20];
char phone[12];
char email[30];
};
// 定义通讯录结构体
struct AddressBook {
// 联系人数组
struct Contact contacts[MAX];
// 当前联系人数量
int count;
};
// 添加联系人
void addContact(struct AddressBook *ab) {
if (ab->count >= MAX) {
printf("通讯录已满,无法添加!\n");
return;
}
struct Contact c;
printf("请输入姓名:");
scanf("%s", c.name);
printf("请输入电话:");
scanf("%s", c.phone);
printf("请输入邮箱:");
scanf("%s", c.email);
ab->contacts[ab->count] = c;
ab->count++;
printf("添加成功!\n");
}
// 显示所有联系人
void showContacts(struct AddressBook *ab) {
if (ab->count == 0) {
printf("通讯录为空!\n");
return;
}
printf("\n通讯录列表(共%d人):\n", ab->count);
printf("姓名\t\t电话\t\t邮箱\n");
for (int i = 0; i < ab->count; i++) {
printf("%s\t\t%s\t\t%s\n",
ab->contacts[i].name,
ab->contacts[i].phone,
ab->contacts[i].email);
}
}
// 查询联系人
void searchContact(struct AddressBook *ab) {
char name[20];
printf("请输入要查询的姓名:");
scanf("%s", name);
for (int i = 0; i < ab->count; i++) {
if (strcmp(ab->contacts[i].name, name) == 0) {
printf("查询结果:\n");
printf("姓名:%s\n", ab->contacts[i].name);
printf("电话:%s\n", ab->contacts[i].phone);
printf("邮箱:%s\n", ab->contacts[i].email);
return;
}
}
printf("未找到该联系人!\n");
}
int main() {
struct AddressBook ab;
// 初始化联系人数量为0
ab.count = 0;
int choice;
while (1) {
printf("\n===== 通讯录管理系统 =====\n");
printf("1. 添加联系人\n");
printf("2. 显示联系人\n");
printf("3. 查询联系人\n");
printf("0. 退出系统\n");
printf("请输入您的选择:");
scanf("%d", &choice);
switch (choice) {
case 1:
addContact(&ab);
break;
case 2:
showContacts(&ab);
break;
case 3:
searchContact(&ab);
break;
case 0:
printf("退出系统!\n");
return 0;
default:
printf("输入错误,请重新选择!\n");
}
}
return 0;
}
运行结果:
===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:1
请输入姓名:张三
请输入电话:13800138000
请输入邮箱:zhangsan@example.com
添加成功!
===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:2
通讯录列表(共1人):
姓名 电话 邮箱
张三 13800138000 zhangsan@example.com
===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:0
退出系统!
42.6.2 案例2:共用体实现数据类型转换
🔧 需求:使用共用体将浮点数转换为二进制表示,查看其在内存中的存储形式。
#include <stdio.h>
union FloatUnion {
float f;
unsigned char bytes[4];
};
void printBinary(unsigned char c) {
for (int i = 7; i >= 0; i--) {
printf("%d", (c >> i) & 1);
}
printf(" ");
}
int main() {
union FloatUnion fu;
fu.f = 3.14f;
printf("浮点数 %.2f 的十六进制表示:\n", fu.f);
for (int i = 0; i < 4; i++) {
printf("%02x ", fu.bytes[i]);
}
printf("\n");
printf("浮点数 %.2f 的二进制表示:\n", fu.f);
for (int i = 0; i < 4; i++) {
printBinary(fu.bytes[i]);
}
printf("\n");
return 0;
}
运行结果:
浮点数 3.14 的十六进制表示:
c8 f5 48 40
浮点数 3.14 的二进制表示:
11001000 11110101 01001000 01000000
42.7 结构体与共用体的常见问题与解决方案
42.7.1 问题1:结构体赋值错误
❌ 错误代码:
struct Student {
char name[20];
int age;
};
struct Student s1 = {"张三", 18};
struct Student s2;
// 错误:数组不能直接用=赋值
s2.name = s1.name;
✅ 解决方案:
- 使用
strcpy函数复制字符串成员。 - 直接赋值整个结构体变量,
s2 = s1;。
42.7.2 问题2:结构体传参效率低
❌ 错误原因:结构体传参时,如果直接传递结构体变量,会拷贝整个结构体,效率较低。
✅ 解决方案:传递结构体指针,避免拷贝开销。
42.7.3 问题3:混淆结构体与共用体
❌ 错误原因:不清楚结构体和共用体的内存布局差异,导致数据访问错误。
✅ 解决方案:
- 结构体成员各自拥有独立的内存空间。
- 共用体成员共享同一块内存空间。
- 结构体大小遵循内存对齐规则,共用体大小等于最大成员的大小。
42.8 本章小结
✅ 结构体是C语言中组织复杂数据的核心工具,其大小遵循内存对齐规则,合理排列成员顺序可以节省内存。
✅ 结构体指针是访问结构体成员的高效方式,箭头运算符 -> 是结构体指针的常用操作符。
✅ 共用体的所有成员共享同一块内存空间,同一时刻只能存储一种类型的数据,适合节省内存的场景。
✅ 结构体嵌套和结构体数组是实现复杂数据结构的基础,动态结构体指针是链表等数据结构的核心。
✅ 结构体和共用体是C语言的重点和难点,也是面试的高频考点,需要熟练掌握。
799

被折叠的 条评论
为什么被折叠?



