Menu

C 语言结构体指针:箭头运算符与堆上的结构体

C 语言中如何用指针指向结构体:箭头运算符,为什么 (*p).x 必须加括号,通过指针传递结构体来修改它或避免复制,用 malloc 分配结构体,以及构建链表节点。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

传给函数的结构体会被复制。对于两个 int 组成的坐标点,这没问题;对于 200 字节的记录,就是浪费。而且这样无法修改结构体,因为函数看到的永远只是自己的副本。用指针指向结构体能同时解决这两个问题,它也是构建可增长结构(链表、树、图)的唯一途径。

获取结构体的指针

不需要什么特别的操作。& 给出地址,类型是“指向结构体的指针”:

三行都打印 3。后两行是同一个操作的两种写法。

为什么 (*ptr).x 必须加括号

你可能想去掉括号写成 *ptr.x。但这样编译出来的意思完全不同,更多时候干脆无法编译,因为成员运算符 . 的优先级高于解引用运算符 *:

*ptr.x        // parses as *(ptr.x)  -- wrong
(*ptr).x      // dereference first, then take the member -- correct
ptr->x        // the same thing, said shortly

ptr.x 是在取指针的成员,而指针没有成员。编译器的报错(“request for member 'x' in something not a structure or union”)其实是一个伪装起来的优先级 bug。

由于带括号的写法既难看又容易写错,C 提供了 ->。在实际代码中你几乎看不到 (*p).x;直接用 p->x,忘掉长写法吧,只在解释箭头含义时才需要它。

通过指针传递结构体

指针参数让函数拿到调用者真正的结构体,所以修改会保留下来:

这里有两个值得养成的习惯:

  • 把只读指针标记为 const。const struct Point *p 承诺函数不会修改 p 指向的内容。如果以后有人在函数里加了 p->x = 0,编译就会失败,而不是让 bug 上线。它也在调用处说明了意图。
  • 大结构体即使只读也用指针传递。一个带 64 字节名字缓冲区的结构体,按值传递每次调用要复制 64 字节;传指针只要 8 字节。

小结构体按值传递仍然没问题:8 字节的 struct Point 复制起来和指针一样便宜,而且按值传递的版本不可能是 NULL,这就消除了一整类出错的可能。

指向结构体数组的指针

指针运算在结构体数组上的效果和在 int 数组上完全一样:p + 1 前进一个完整的结构体,包括填充字节。

数组名会退化为指向首元素的指针,所以在调用处 staff 已经是 struct Employee * 了。这也是函数需要单独传入 n 的原因:在函数内部用 sizeof 测量的是指针,而不是数组。

在堆上分配结构体

栈上的结构体在作用域结束时就消亡了。要创建一个比创建它的函数活得更久的结构体,或者按输入的需要创建任意多个,就用 malloc 分配:

这个小函数里的四个细节都至关重要:

  • 用 sizeof *e,而不是 sizeof(struct Employee)。它的意思是“e 所指向的东西的大小”,所以类型一旦改变,分配大小会自动跟上,两者不可能对不上。
  • 检查 NULL。malloc 无法满足请求时返回 NULL。通过空指针写入 e->id 会导致段错误。
  • 初始化每个成员。malloc 不会把内存清零,拿到的结构体里全是垃圾值。(calloc(1, sizeof *e) 会帮你清零。)
  • 必须有人调用 free。返回一个已分配的指针,就把释放的责任交给了调用者。要在注释里写明,所有权规则不清楚正是内存泄漏的开始。

回报:链表节点

这是结构体离开指针就做不到的事。结构体不能包含自身,那样会无限大,但它可以包含一个指向同类型的指针,C 中所有链式数据结构都建立在这一个技巧之上。

注意,struct Node *next; 在 struct Node 还在声明的过程中就引用了它。这之所以合法,正是因为无论指针指向什么,它的大小都是已知的,编译器此时还不需要完整的定义。这也是唯一一个即使你写了 typedef,也必须使用 struct Node 标签的地方:在 typedef 自己的定义内部,这个类型名还不存在。

free_list 循环在调用 free(head) 之前先保存了 head->next。在 free 之后读取 head->next 属于释放后使用:那块内存已经不归你了,读到的值是分配器放在那里的任意内容。

常见错误

  • 对未初始化或为 NULL 的指针使用 p->x。这是大量使用结构体的代码中最常见的崩溃原因。把指针初始化为 NULL,解引用之前先检查。
  • 返回指向局部结构体的指针。struct Point *bad(void) { struct Point p = {1,2}; return &p; } 返回的地址在函数返回的那一刻就不再有效。应该按值返回结构体,或者动态分配它。
  • 释放两次,或者忘记释放。每个 malloc 恰好对应一个 free。释放之后,把指针设为 NULL,这样以后不小心使用时会明显地崩溃,而不是悄悄地破坏内存。
  • 复制包含指针的结构体。结构体赋值复制的是指针的值,而不是它指向的内容。两个结构体现在引用同一个缓冲区,无论哪个先释放它,另一个都会变成悬空指针。

常见问题

C 语言中 -> 运算符的作用是什么?

p->x 通过指针 p 访问成员 x。它和 (*p).x 完全等价:先解引用指针,再取成员。之所以有箭头,是因为结构体指针太常用了,较长的写法会让代码显得杂乱。

为什么 *p.x 对结构体指针不起作用?

因为 . 的优先级比 * 高。*p.x 会被解析为 *(p.x):取 p(它是指针,不是结构体)的成员 x,再解引用。你需要写 (*p).x,或者直接写 p->x。

C 语言中结构体应该按值传递还是按指针传递?

当函数必须修改调用者的结构体,或者结构体很大、复制太浪费时,按指针传递。对于只读的小结构体,按值传递,复制开销小,代码也更简单。把只读的指针参数标记为 const struct T *,让编译器来保证这个承诺。

C 语言中如何用 malloc 分配结构体?

struct Node *n = malloc(sizeof *n);。sizeof *n 的写法取的是 n 所指向类型的大小,所以以后修改类型时它依然正确。检查结果是否为 NULL,初始化每个成员,用完后调用 free(n)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始