7、指针与结构体:结构体指针、-> 运算符、结构体成员访问、结构体数组与指针、结构体中的指针成员
结构体这东西,说白了就是C语言里用来“打包数据”的。你想想看,一个学生有学号、姓名、成绩,如果每个都单独定义变量,代码会乱成一锅粥。结构体就是把这些零散的数据捆在一起,变成一个整体。
但真正让结构体发挥威力的,是它和指针的结合。我在项目中见过太多人,结构体用得挺好,一加上指针就懵了。今天咱们就把这块彻底讲透。
7.1 结构体指针:指向数据的“遥控器”
结构体变量本身占一块内存,结构体指针就是存这块内存的地址。你可以通过指针来操作结构体,而不是直接操作变量本身。
为什么要用指针?两个原因:一是传参效率高,你传一个8字节的指针,比传一个几百字节的结构体快得多;二是可以动态分配结构体,这个后面会讲。
struct Student {
int id;
char name[20];
float score;
};
struct Student stu = {1001, "张三", 89.5};
struct Student *p = &stu; // p 指向 stu
嗯,这里要注意:p 存的是 stu 的地址,不是 stu 本身。你可以通过 p 来修改 stu 的内容。
7.2 -> 运算符:指针访问成员的“快捷方式”
如果你有一个结构体变量 stu,访问成员用 stu.id。但如果有一个指针 p 指向 stu,你怎么访问成员?
最直接的方式是 (*p).id。先解引用拿到结构体,再取成员。但这样写太啰嗦了,而且括号容易漏。所以C语言提供了一个语法糖——-> 运算符。
// 两种写法等价
printf("%d\n", (*p).id); // 先解引用,再取成员
printf("%d\n", p->id); // 直接用 -> 一步到位
我个人习惯用 ->,因为它更直观,而且不容易出错。你想想看,p->id 一眼就能看出 p 是指针,id 是成员。而 (*p).id 还得在脑子里过一遍优先级。
p->member,心里可以默念“p指向的结构体的member成员”。多念几遍就顺了。
7.3 结构体成员访问:点号 vs 箭头
很多初学者搞不清什么时候用 .,什么时候用 ->。其实规则很简单:
- 如果你手里是一个结构体变量,用
. - 如果你手里是一个结构体指针,用
->
就这么简单。但实际工程中,经常出现“指针的指针”或者“结构体数组的指针”,这时候就要小心了。
struct Student students[3] = {
{1001, "张三", 89.5},
{1002, "李四", 92.0},
{1003, "王五", 78.5}
};
struct Student *p = students; // 数组名就是首元素地址
// 访问第一个学生的 id
printf("%d\n", p->id); // 1001
printf("%d\n", students[0].id); // 1001
// 访问第二个学生的 name
printf("%s\n", (p+1)->name); // 李四
printf("%s\n", students[1].name); // 李四
我在项目中遇到过一个问题:有人用 p++ 来遍历结构体数组,但忘了 p 的类型是 struct Student *,p++ 实际上跳过了整个结构体的大小(而不是1个字节)。这个坑很多人踩过。
7.4 结构体数组与指针:遍历的多种姿势
结构体数组本质上就是连续存放的一组结构体。数组名就是首地址,指针运算和普通数组完全一样。
遍历结构体数组,常见的有三种写法:
// 写法1:下标法
for (int i = 0; i < 3; i++) {
printf("%d %s %.1f\n", students[i].id, students[i].name, students[i].score);
}
// 写法2:指针偏移法
for (int i = 0; i < 3; i++) {
printf("%d %s %.1f\n", (p+i)->id, (p+i)->name, (p+i)->score);
}
// 写法3:指针递增法
for (struct Student *q = students; q < students + 3; q++) {
printf("%d %s %.1f\n", q->id, q->name, q->score);
}
我个人推荐写法3,因为它最接近“迭代器”的思想,而且代码更简洁。但要注意,q 是临时指针,不会影响原数组。
students++!数组名是常量指针,不能自增。如果你需要移动指针,请另外定义一个指针变量。
7.5 结构体中的指针成员:灵活但危险
结构体里可以放指针成员,这是C语言灵活性的体现。但也是bug的高发区。
最常见的场景是:结构体里放一个 char * 来指向字符串,而不是用固定大小的数组。这样可以节省内存,但你必须自己管理内存的生命周期。
struct Person {
int age;
char *name; // 指针成员,指向动态分配的字符串
};
struct Person p;
p.age = 25;
p.name = (char *)malloc(20 * sizeof(char));
strcpy(p.name, "张三");
// 使用完后,必须释放
free(p.name);
我曾经在一个项目中,结构体里放了三个指针成员,分别指向不同的动态内存。结果有一个分支忘记释放了,内存泄漏查了两天才找到。嗯,从那以后我养成了一个习惯:谁分配,谁释放;分配和释放成对出现。
还有一种情况是结构体里的指针指向另一个结构体,形成链表或树结构。这时候指针的指向关系一定要画清楚,不然很容易写出野指针。
struct Node {
int data;
struct Node *next; // 指向下一个节点
};
这种链表结构在嵌入式系统中很常见,比如任务队列、缓冲区管理等。但要注意,链表的插入和删除操作,一定要先画图再写代码,别凭感觉来。
7.6 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,结构体指针是中心,它连接了结构体变量、结构体数组、动态分配和链表结构。
7.7 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 结构体指针未初始化就使用:这是最常见的段错误来源。声明指针后,要么让它指向一个已有的变量,要么用
malloc分配内存。 - 结构体中的指针成员指向栈内存:如果结构体里的指针指向了一个局部变量,函数返回后这个指针就变成了野指针。
- 结构体赋值时浅拷贝:如果结构体里有指针成员,直接用
=赋值只是拷贝了指针的值,两个结构体指向同一块内存。修改一个会影响另一个。 - 忘记释放结构体中的指针成员:释放结构体本身不会自动释放它内部的指针成员指向的内存。需要手动
free。
我曾经在一个通信协议解析的项目中,结构体里嵌套了三级指针,每一级都指向动态分配的内存。释放的时候少写了一行代码,结果内存泄漏导致设备运行三天后死机。从那以后,我写结构体时都会画一张内存布局图,把谁指向谁、谁负责释放都标清楚。
好了,指针与结构体的内容就讲到这里。记住一句话:结构体是数据的容器,指针是操作的钥匙。两者结合,才能写出高效、灵活的C代码。
公众号:蓝海资料掘金营,微信deep3321