如果说前十章的指针是「单兵作战」,这一章开始它们有了编制:结构体把多个变量捆成一个对象,指针把对象串成链表、树、图,函数指针把行为挂到对象上。C 的整个数据结构世界、乃至「面向对象」的雏形,都从这三个零件长出来。原文小节编号错成 11.x,一并修正。
struct Point { int x, y; };
struct Point pt = {3, 4};
struct Point *p = &pt;
p->x = 30; // 等价 (*p).x = 30
. 用于结构体变量,-> 用于结构体指针,后者是 (*p).x 的简写。机器层一目了然(计组课第 10 章):p->x 就是 [p + offsetof(Point,x)],成员偏移编译期定死,一次地址运算,没有任何运行时查表。这是 C 结构体访问快过 Python 属性访问一两个数量级的原因:一个查偏移,一个查字典。
第 10 章讲过值传递,对结构体它意味着整块复印:
struct Big { char data[1024 * 1024]; };
void by_value(struct Big b) { ... } // 拷 1MB,惨案
void by_ptr(struct Big *b) { ... } // 8 字节门牌
void by_const(const struct Big *b) { ... } // 8 字节 + 只读承诺
铁律两条:大结构体永远传指针;只读场景永远加 const。例外是小结构体(两三个 int)按值传反而更快(不用二次解引用),struct Point 这类「值语义」的小对象按值传是正常写法。分界线大致在「超过两三个机器字」,拿不准就传 const 指针。
结构体里放「指向自己类型的指针」,数据结构课的世界从这一行代码开始:
struct Node {
int data;
struct Node *next; // 自引用:合法(指针大小已知,不需要完整类型)
};
配套的操作把前面几章的技能全部串联:malloc 造节点(第 9 章)、二级指针改头(第 7 章)、遍历释放(第 6 章的纪律):
void push(struct Node **head, int v) { // 传 &head:要改指针本身
struct Node *n = malloc(sizeof *n);
n->data = v;
n->next = *head;
*head = n;
}
void destroy(struct Node *head) { // 先存 next 再 free(第 9 章的纪律)
while (head) {
struct Node *tmp = head;
head = head->next;
free(tmp);
}
}
「不能直接放自身 struct Node next;」值得想一下为什么:完整类型的大小未定(套娃无限),而指针大小固定 8 字节,所以自引用只能是自引用指针。数据结构课第 2、5 章的链表、树,全是这个模式的直接复用。
结构体成员按声明顺序排,但每个成员要对齐到自身大小(的倍数)的地址,中间会垫字节:
struct S { char c; int i; short s; };
// 期望 1+4+2 = 7,实际 sizeof = 12
// c 占 1,垫 3;i 占 4(对齐 4 的倍数);s 占 2;尾部再垫 2(整个结构体按最大对齐 4 收尾)
用 offsetof(struct S, i) 可查每个成员的偏移。工程结论:成员按大小降序排列能省填充(int i; short s; char c; 只有 8 字节,省了 4);序列化、跨进程共享内存、文件格式这些「按字节抠」的场景,offsetof 和对齐是日常工具。数据结构课第 12 章的「指针税」表里,每个结构的真实体积都是 sizeof 说话,不是成员相加。
把数据和「操作它的函数」捆在一起,就是 C 版的类:
struct Vec {
int *data;
int size;
int (*get)(struct Vec *, int);
void (*set)(struct Vec *, int, int);
};
int vec_get(struct Vec *v, int i) { return v->data[i]; }
void vec_set(struct Vec *v, int i, int x) { v->data[i] = x; }
struct Vec v = { .data = NULL, .size = 0, .get = vec_get, .set = vec_set };
v.set(&v, 0, 42); // 注意:C 没有 this,手动把 &v 传进去
那个手动传的 &v 正是 C++ 里 this 指针的真身(编译器自动帮你传),C++ 的虚函数表(vtable)也就是「把函数指针抽出来集中放一张表」。真实世界的 C 大型工程(Linux 内核的 file_operations、Linux 驱动模型、glib 的 GObject)全是这套写法,你以后读内核代码时看到结构体里成片的函数指针,那就是「方法表」。
. 和 -> 用错。指针用 . 编译不过,值用 -> 同样不过;但typedef 后类型一眼难辨时容易写反,报错信息会救你。typedef struct { struct Node *next; } Node; 里 next 的类型名还没定义,要么写 struct Node *next 保留标签,要么前向声明。这个编译错误的信息经常让人摸不着头脑。__attribute__((packed)) 并承担跨平台风险。void (*print)(struct Vec *) 方法,再做一个 struct VecV2(数据成员不同),验证「同一套函数指针表 + 不同数据结构」的玩法,体会它与 C++ 多态的异同。-> 要设计成显式操作(每次都看得见解引用),而 C++ 的 obj.method() 把 this 的传递藏起来?结合第 10 章「复印件世界」的模型,说说显式传 &v 对理解「对象是谁的」有什么帮助。