传给函数的结构体会被复制。对于两个 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)。