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 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到,结构体指针是中心,它连接了结构体变量、结构体数组、动态分配和链表结构。

结构体指针 结构体变量 结构体数组 动态分配结构体 链表 / 树结构 访问方式:p->member 或 (*p).member 核心:结构体指针是连接数据和操作的桥梁 注意内存管理:谁分配谁释放

7.7 避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 结构体指针未初始化就使用:这是最常见的段错误来源。声明指针后,要么让它指向一个已有的变量,要么用 malloc 分配内存。
  • 结构体中的指针成员指向栈内存:如果结构体里的指针指向了一个局部变量,函数返回后这个指针就变成了野指针。
  • 结构体赋值时浅拷贝:如果结构体里有指针成员,直接用 = 赋值只是拷贝了指针的值,两个结构体指向同一块内存。修改一个会影响另一个。
  • 忘记释放结构体中的指针成员:释放结构体本身不会自动释放它内部的指针成员指向的内存。需要手动 free

我曾经在一个通信协议解析的项目中,结构体里嵌套了三级指针,每一级都指向动态分配的内存。释放的时候少写了一行代码,结果内存泄漏导致设备运行三天后死机。从那以后,我写结构体时都会画一张内存布局图,把谁指向谁、谁负责释放都标清楚。

好了,指针与结构体的内容就讲到这里。记住一句话:结构体是数据的容器,指针是操作的钥匙。两者结合,才能写出高效、灵活的C代码。


公众号:蓝海资料掘金营,微信deep3321