C语言结构体:定义、访问、指针、嵌套
- C语言进阶
- 2小时前
- 17热度
- 0评论
C语言结构体是 C 语言中最常用的复合数据类型之一。它不仅能把多个不同类型的数据组织成一个整体,还经常出现在函数传参、链表设计、设备寄存器映射、通信协议解析等场景中。理解结构体,基本就掌握了 C 语言中“描述复杂对象”的核心手段。
一、C语言结构体是什么?
C语言结构体是一种用户自定义的数据类型,可以将多个不同类型的数据组合在一起,形成一个逻辑整体。它特别适合描述一个对象的多种属性,比如学生的姓名、年龄和成绩,或者设备的寄存器状态、通信数据帧等。
ℹ️ 核心理解:数组存储的是相同类型的数据集合,C语言结构体存储的是不同类型的数据集合。按类型分类,结构体属于聚合类型。
1.1 语法格式
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
...
};
1.2 typedef 简化写法
typedef struct {
数据类型 成员1;
数据类型 成员2;
...
} 别名;
💡 使用建议
使用
使用
typedef 之后,声明变量时可以省略 struct 关键字,代码会更简洁,也更适合日常开发。
二、C语言结构体的基本用法
先看最基础的结构体定义、赋值和输出,再看结构体数组这种更常见的组合形式。
2.1 基本示例
#include <stdio.h>
#include <string.h>
// 定义结构体
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 方式一:先声明再赋值
struct Student stu1;
strcpy(stu1.name, "张三");
stu1.age = 20;
stu1.score = 92.5;
// 方式二:声明时初始化
struct Student stu2 = {"李四", 21, 88.0};
printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", stu1.name, stu1.age, stu1.score);
printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", stu2.name, stu2.age, stu2.score);
return 0;
}
2.2 结构体数组
结构体数组就是“把多个相同结构体类型的数据,像数组一样连续存放并用下标访问的一组变量”
#include <stdio.h>
// 使用 typedef 简化结构体定义
typedef struct {
char name[20];
int age;
float score;
} Student;
int main() {
// 直接用 Student 声明,无需 struct 关键字
Student class[] = {
{"张三", 20, 92.5},
{"李四", 21, 88.0},
{"王五", 19, 95.0}
};
int n = sizeof(class) / sizeof(class[0]);
for (int i = 0; i < n; i++) {
printf("%-6s %d岁 %.1f分\n", class[i].name, class[i].age, class[i].score);
}
return 0;
}
三、结构体成员访问方式
结构体成员的访问方式,取决于你操作的是结构体变量,还是结构体指针。
💬 核心区别:结构体变量用
.(点运算符),结构体指针用 ->(箭头运算符)。
3.1 点运算符 .
通过结构体变量直接访问成员:
struct Student stu = {"张三", 20, 92.5};
// 访问成员
printf("%s\n", stu.name); // 张三
printf("%d\n", stu.age); // 20
// 修改成员
stu.age = 21;
stu.score = 95.0;
3.2 箭头运算符 ->
通过结构体指针访问成员,本质上是 (*p).成员 的简化写法:
struct Student stu = {"张三", 20, 92.5};
struct Student *p = &stu;
// 等价写法
p->age = 21; // 箭头运算符(推荐)
(*p).age = 21; // 解引用 + 点运算符
// 三种方式对比
printf("%s\n", stu.name); // 通过变量访问
printf("%s\n", p->name); // 通过指针访问(推荐)
printf("%s\n", (*p).name); // 通过解引用访问
💡 为什么优先使用
->?
(*p).name需要额外括号,因为.的优先级高于*p->name更简洁,也更容易一眼看出“这是一个指针在访问成员”
3.3 访问方式对比
| 场景 | 语法 | 示例 |
|---|---|---|
| 结构体变量访问成员 | 变量.成员 |
stu.age |
| 结构体指针访问成员 | 指针->成员 |
p->age |
| 指针解引用后访问 | (*指针).成员 |
(*p).age |
📋 速记
- 变量访问用
. - 指针访问用
-> p->name等价于(*p).name
四、结构体指针
结构体指针是一个指向结构体变量的指针。它最大的价值在于:可以避免传递整个结构体带来的拷贝开销,并且方便修改原对象内容。
ℹ️ 一句话理解:结构体指针 = 指针 + 结构体,用
-> 访问成员,等价于 (*p).成员。
4.1 语法格式
struct 结构体名 *指针变量名;
4.2 基本用法
#include <stdio.h>
struct Student {
char name[20];
int age;
float score;
};
int main() {
struct Student stu = {"张三", 20, 92.5};
struct Student *p = &stu; // 指针指向结构体变量
// 三种访问方式(结果相同)
printf("姓名: %s\n", stu.name); // 点运算符
printf("姓名: %s\n", p->name); // 箭头运算符(推荐)
printf("姓名: %s\n", (*p).name); // 解引用
// 通过指针修改成员
p->age = 21;
p->score = 95.0;
printf("修改后: %s, %d岁, %.1f分\n", p->name, p->age, p->score);
return 0;
}
4.3 结构体指针作为函数参数
#include <stdio.h>
typedef struct {
char name[20];
int age;
float score;
} Student;
// 传指针:避免拷贝整个结构体,高效!
void printStudent(const Student *p) {
printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", p->name, p->age, p->score);
}
// 通过指针修改结构体
void addBonus(Student *p, float bonus) {
p->score += bonus;
}
int main() {
Student stu = {"张三", 20, 88.0};
printStudent(&stu); // 姓名: 张三, 年龄: 20, 成绩: 88.0
addBonus(&stu, 5.0); // 加 5 分
printStudent(&stu); // 姓名: 张三, 年龄: 20, 成绩: 93.0
return 0;
}
4.4 动态内存分配结构体
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[20];
int age;
} Student;
int main() {
// 在堆上动态创建一个结构体
Student *p = (Student *)malloc(sizeof(Student));
if (p == NULL) {
printf("内存分配失败!\n");
return 1;
}
strcpy(p->name, "张三");
p->age = 20;
printf("姓名: %s, 年龄: %d\n", p->name, p->age);
free(p); // 释放内存
p = NULL; // 避免悬空指针
return 0;
}
⚠️ 注意:只要结构体是通过
malloc 动态申请的,就要记得配对 free。释放后最好把指针置为 NULL,避免悬空指针问题。
五、嵌套结构体
当一个结构体的成员本身也是结构体时,就形成了嵌套结构体。这种写法非常适合描述层级化数据,比如“人包含地址”“订单包含收货信息”“设备包含配置参数”等。
💡 核心理解
嵌套结构体就是“结构体套结构体”。访问成员时需要层层展开,因此会出现多个
嵌套结构体就是“结构体套结构体”。访问成员时需要层层展开,因此会出现多个
. 或 -> 连续使用的情况。
5.1 嵌套结构体示例
#include <stdio.h>
// 定义地址结构体
typedef struct {
char city[20];
char street[30];
int zipCode;
} Address;
// 定义学生结构体(嵌套地址)
typedef struct {
char name[20];
int age;
Address addr; // 嵌套结构体,无需 struct 关键字
} Person;
int main() {
// 初始化嵌套结构体
Person p = {
"张三",
25,
{"北京", "中关村大街1号", 100080}
};
// 访问嵌套成员:用多个点运算符
printf("姓名: %s\n", p.name);
printf("城市: %s\n", p.addr.city);
printf("街道: %s\n", p.addr.street);
printf("邮编: %d\n", p.addr.zipCode);
return 0;
}
5.2 通过指针访问嵌套成员
Person p = {"张三", 25, {"北京", "中关村大街1号", 100080}};
Person *ptr = &p;
// 方式一:箭头 + 点
printf("城市: %s\n", ptr->addr.city);
// 方式二:解引用 + 点
printf("城市: %s\n", (*ptr).addr.city);
⚠️ 容易写错的地方:
ptr->addr.city 不能写成 ptr->addr->city,因为 addr 本身是结构体变量,不是指针。
5.3 嵌套结构体的内存布局
嵌套结构体的总大小依然遵循对齐规则,内层结构体会作为一个整体参与对齐。
struct Inner {
char c; // 1 字节
int i; // 4 字节
}; // sizeof = 8(补到 4 的倍数)
struct Outer {
char a; // 1 字节
struct Inner s; // 8 字节(从 4 对齐地址开始)
char b; // 1 字节
}; // sizeof = 16
内存布局:
地址: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[a ] [pad] [pad] [pad] [ Inner s (8字节) ] [b ] [pad] [pad] [pad]
↑ char ↑ Inner 要 4 对齐 ↑ char ↑ 总大小要 4 的倍数
六、结构体内存对齐
结构体内存对齐(Structure Alignment / Padding)是指编译器在分配结构体成员内存时,会按照一定规则在成员之间或末尾插入填充字节(padding),让每个成员的地址满足对齐要求。
ℹ️ 为什么要对齐?:CPU 访问内存时,按 2 / 4 / 8 字节 整块读取通常效率更高。如果一个
int 跨越两个 4 字节块,CPU 往往需要多次读取再拼接,因此对齐本质上是用少量空间换访问效率。
6.1 三条核心规则
💬 核心规则
- 成员对齐:每个成员的起始地址必须是
min(该成员大小, 默认对齐数)的整数倍 - 结构体总大小:必须是
最大成员对齐数的整数倍 - 默认对齐数:不同编译器不同。MSVC 默认 8,GCC 通常按成员自身大小对齐
常见类型的大小和对齐要求如下:
| 类型 | 大小(字节) | 对齐要求 |
|---|---|---|
char |
1 | 1 字节对齐 |
short |
2 | 2 字节对齐 |
int |
4 | 4 字节对齐 |
float |
4 | 4 字节对齐 |
double |
8 | 8 字节对齐 |
指针 |
4 或 8 | 4 或 8 字节对齐 |
6.2 不同成员顺序的对比
6.2.1 不优化的顺序
struct Bad {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
// sizeof = ?
内存布局:
地址: 0 1 2 3 4 5 6 7 8 9 10 11
[a ] [pad] [pad] [pad] [ b (4字节) ] [c ] [pad] [pad] [pad]
↑ char ↑ int 要 4 对齐 ↑ char ↑ 总大小要 4 的倍数
a在地址 0,满足 1 字节对齐b需要放到地址 4,中间补了 3 字节 paddingc放在地址 8- 总大小还要补齐到 4 的倍数,因此最终是 12 字节
6.2.2 优化的顺序
struct Good {
int b; // 4 字节
char a; // 1 字节
char c; // 1 字节
};
// sizeof = ?
内存布局:
地址: 0 1 2 3 4 5 6 7
[ b (4字节) ] [a ] [c ] [pad] [pad]
↑ int ↑ char ↑ char ↑ 补到 4 的倍数
b在地址 0a在地址 4c在地址 5- 总大小补到 4 的倍数,因此最终是 8 字节
6.3 用代码验证偏移量
#include <stdio.h>
#include <stddef.h> // offsetof
struct Bad {
char a;
int b;
char c;
};
struct Good {
int b;
char a;
char c;
};
int main() {
printf("===== struct Bad =====\n");
printf("sizeof = %zu\n", sizeof(struct Bad)); // 12
printf("a 的偏移量 = %zu\n", offsetof(struct Bad, a)); // 0
printf("b 的偏移量 = %zu\n", offsetof(struct Bad, b)); // 4
printf("c 的偏移量 = %zu\n", offsetof(struct Bad, c)); // 8
printf("\n===== struct Good =====\n");
printf("sizeof = %zu\n", sizeof(struct Good)); // 8
printf("b 的偏移量 = %zu\n", offsetof(struct Good, b)); // 0
printf("a 的偏移量 = %zu\n", offsetof(struct Good, a)); // 4
printf("c 的偏移量 = %zu\n", offsetof(struct Good, c)); // 5
return 0;
}
💡 优化技巧
如果你希望结构体更紧凑,通常可以按照成员大小从大到小排列,这样往往能减少 padding 的浪费。
如果你希望结构体更紧凑,通常可以按照成员大小从大到小排列,这样往往能减少 padding 的浪费。
// 推荐写法:大的放前面
struct Optimized {
double d; // 8 字节
int i; // 4 字节
short s; // 2 字节
char c; // 1 字节
};
// sizeof = 16(紧凑)
6.4 手动控制对齐
// 方法一:#pragma pack(MSVC / GCC 都支持)
#pragma pack(push, 1) // 设置 1 字节对齐(取消 padding)
struct Packed {
char a;
int b;
char c;
};
#pragma pack(pop) // 恢复默认
// sizeof = 6(无 padding,但访问可能变慢)
// 方法二:__attribute__(GCC / Clang)
struct __attribute__((packed)) Packed2 {
char a;
int b;
char c;
};
// sizeof = 6
⚠️ 注意
- 取消对齐虽然节省内存,但可能导致某些平台出现未对齐访问异常
- 同时也可能让 CPU 访问效率下降
- 这种写法通常只在通信协议、文件格式、硬件寄存器映射等必须精确控制布局的场景下使用
七、总结对比
| 概念 | 作用 | 关键符号 | 典型场景 |
|---|---|---|---|
| 结构体 | 组合不同类型数据 | . |
描述复杂对象、寄存器组映射 |
| 结构体指针 | 指向结构体的指针 | -> |
函数传参、动态分配、链表 |
| 嵌套结构体 | 结构体套结构体 | 外层.内层.成员 |
描述层级数据(地址、日期等) |
| 内存对齐 | 优化访问效率与布局规则 | sizeof / offsetof |
底层优化、嵌入式开发、协议设计 |
📋 速记
- 结构体负责“把不同类型数据打包成一个整体”
- 变量访问成员用
.,指针访问成员用-> p->name等价于(*p).name- 嵌套结构体访问时要层层展开,例如
p.addr.city - 成员顺序不同,
sizeof的结果可能不同
相关阅读
- [[98-博客/C语言指针和结构体]]:深入理解指针与结构体的配合使用
- [[98-博客/C语言内存管理]]:动态内存分配与结构体对象的结合
- [[98-博客/嵌入式开发中 void 的用法总结]]:通用指针与结构体参数传递
- [[98-博客/FreeRTOS任务管理-基础架构与生命周期]]:结构体在任务控制块中的实际应用