指针与结构体:结构体指针、-> 运算符、结构体中的指针成员、链表节点的定义

结构体和指针,这两个东西单独拿出来都不算难。但把它们放在一起,很多人就开始犯迷糊了。我当年刚学C语言时,也是在这个地方卡了很久。说白了,结构体是数据的容器,指针是地址的载体。两者结合,才能玩出真正的数据结构。

结构体指针:指向数据块的箭头

结构体变量在内存中占据一块连续的空间。结构体指针,就是指向这块空间起始地址的指针。定义起来很简单:

struct Student {
    char name[32];
    int age;
    float score;
};

struct Student stu = {"张三", 20, 95.5};
struct Student *p = &stu;  // p指向stu

这里p保存的是stu的地址。通过p,我们可以访问stu的成员。怎么访问?有两种方式。

-> 运算符:专为结构体指针而生

如果你有一个普通结构体变量,用点号访问成员:stu.age。如果你有一个结构体指针,用箭头访问成员:p->age

为什么不用 (*p).age?当然可以。但箭头更直观,写起来也更顺手。我在项目中见过有人坚持用 (*p).age,代码读起来特别累。你想想看,一个链表遍历下来,满屏的括号和星号,谁受得了?

核心记忆点:

  • p->member 等价于 (*p).member
  • 箭头左边必须是结构体指针,不能是结构体变量
  • 点号左边必须是结构体变量,不能是结构体指针

我曾经在代码审查时发现一个bug:有人写了 p.age,结果编译报错,他愣是没看出来p是指针。嗯,这种错误其实很常见,尤其是从Java转过来的朋友。

结构体中的指针成员:灵活但危险

结构体的成员可以是任何类型,当然也包括指针。这给了我们极大的灵活性,但也带来了内存管理的麻烦。

struct Person {
    char *name;    // 指针成员
    int age;
};

注意,这里的 name 只是一个指针,它没有实际的内存空间。你必须手动为它分配内存:

struct Person p;
p.name = (char *)malloc(64 * sizeof(char));
if (p.name != NULL) {
    strcpy(p.name, "李四");
    p.age = 25;
}
// 使用完后记得释放
free(p.name);

警告:结构体中的指针成员,不会随着结构体变量的销毁而自动释放。你必须显式地 free 它。否则,内存泄漏等着你。

我有个项目,就是因为结构体里藏了个指针成员,团队里有人忘了free,结果嵌入式设备跑了三天就内存耗尽重启了。排查起来特别痛苦。

链表节点的定义:结构体与指针的经典组合

链表是数据结构的基础。它的核心就是结构体里包含一个指向同类型结构体的指针。这就是所谓的「自引用结构体」。

struct ListNode {
    int data;               // 数据域
    struct ListNode *next;  // 指针域,指向下一个节点
};

这里 next 指向的是 struct ListNode 类型。注意,定义时不能写成 ListNode *next,因为此时 ListNode 这个类型名还没定义完。必须用 struct ListNode 来声明。

为什么可以这样?因为指针本身的大小是固定的(32位系统4字节,64位系统8字节),编译器不需要知道结构体的完整布局就能确定指针成员的大小。所以,自引用是合法的。

一个简单的单向链表创建:

struct ListNode *head = NULL;
struct ListNode *node1 = (struct ListNode *)malloc(sizeof(struct ListNode));
struct ListNode *node2 = (struct ListNode *)malloc(sizeof(struct ListNode));

node1->data = 10;
node1->next = node2;

node2->data = 20;
node2->next = NULL;

head = node1;

遍历这个链表:

struct ListNode *cur = head;
while (cur != NULL) {
    printf("%d ", cur->data);
    cur = cur->next;
}

你看,cur->next 就是下一个节点的地址。当 cur->next 为 NULL 时,链表结束。这就是链表的精髓——用指针把分散的内存块串起来。

个人习惯:我写链表时,总是先画图。把每个节点画成方框,指针画成箭头。代码写错了,看图就能发现问题。别嫌麻烦,画图真的能省很多调试时间。

结构体指针的常见陷阱

我总结几个容易踩的坑:

  • 野指针:结构体指针没有初始化就使用。声明指针后,要么赋有效地址,要么赋 NULL。
  • 浅拷贝问题:结构体里有指针成员时,直接赋值 struct B = struct A 只会拷贝指针值,不会拷贝指针指向的数据。两个结构体指向同一块内存,改一个另一个也变。
  • 忘记释放:malloc 了结构体,也 malloc 了里面的指针成员,释放时顺序搞反。先释放结构体,里面的指针成员就找不到了。

我曾经在调试一个通信协议栈时,发现数据包总是被莫名篡改。查了两天,最后发现是结构体赋值时做了浅拷贝,两个节点共享了同一个缓冲区。嗯,从那以后,我写结构体赋值都格外小心。

知识体系图

下面这张图展示了本章的核心知识点和它们之间的关系:

结构体与指针 结构体指针 定义:struct Student *p = &stu; 访问成员:p->age 或 (*p).age -> 运算符 专用于结构体指针 等价于 (*p).member 指针成员 char *name; // 需手动分配内存 必须手动 free,否则内存泄漏 链表节点:自引用结构体 struct ListNode { int data; struct ListNode *next; }; ⚠ 注意内存管理

这张图把四个核心概念串在了一起。结构体指针是基础,-> 运算符是工具,指针成员是进阶,链表节点是综合应用。每一步都踩稳了,后面的数据结构学习才会顺畅。

最后说一句:指针和结构体,是C语言里最值得花时间啃透的内容。你想想看,操作系统、驱动、网络协议栈,哪个不是靠这两个东西撑起来的?

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