第 12 章 · 指针与结构体

结构体 + 指针 = C 数据结构的根基:-> 运算符、自引用结构、函数指针成员。

11.1 -> 运算符

结构体指针访问成员,用 ->(等价于 (*p).x 的简写):

struct Point { int x, y; };
struct Point pt = {3, 4};
struct Point *p = &pt;

p->x;              // 等价 (*p).x = 3
p->x = 30;         // 通过指针修改成员
  • . 用于结构体-> 用于结构体指针
  • p->x 实际是「先解引用取结构体,再取成员」,编译器算好成员偏移,一次地址运算完成。

11.2 传结构体指针:避免大拷贝

C 按值传递,传结构体会整体拷贝(大结构体拷贝开销大,且函数内改动不生效):

struct Big { char data[1024 * 1024]; };

void by_value(struct Big b) { ... }   // 拷贝 1MB,慢
void by_ptr(const struct Big *b) { ... } // 只传 8 字节地址,快
  • 只读用 const struct Big *,想改用 struct Big *
  • 大型 C 项目里结构体几乎总以指针流转。

11.3 自引用结构:链表节点

结构体里能放「指向自身类型」的指针(不能直接放自身,会无限嵌套),这是链表的基石:

struct Node {
    int data;
    struct Node *next;   // 指向下一个节点,自引用
};

链表的基本操作:

// 头插(O(1))
void push(struct Node **head, int v) {
    struct Node *n = malloc(sizeof *n);
    n->data = v;
    n->next = *head;      // 新节点指到旧头
    *head = n;            // 头指针指向新节点(二级指针改外部指针)
}

// 遍历
for (struct Node *cur = head; cur; cur = cur->next)
    printf("%d ", cur->data);

// 释放整个链表
while (head) {
    struct Node *tmp = head;
    head = head->next;
    free(tmp);
}

注意 push 用了 struct Node **head——因为要改调用方的 head 指针本身,这正是第 7 章的二级指针心法。

11.4 结构体的内存布局与对齐

结构体成员在内存里按声明顺序排布,但会插入**填充(padding)**以满足每个成员的对齐要求:

struct S {
    char  c;     // 1 字节
    int   i;     // 4 字节,需 4 对齐 → 前面垫 3 字节
    short s;     // 2 字节
};
// sizeof(struct S) 通常是 12,而不是 1+4+2=7
  • offsetof(struct S, i) 查成员偏移;sizeof 看总大小(含尾部填充)。
  • 成员顺序会影响总大小:把大对齐的成员放前面,通常能省填充。
  • 取成员地址 &s.i 得到的指针必须满足 int 的对齐——编译器靠填充保证这一点。

11.5 函数指针成员:C 版「对象」

把数据 + 操作数据的方法(函数指针)放进同一个结构体,就是 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 = { .get = vec_get, .set = vec_set };
v.set(&v, 0, 42);   // 模拟 v.set(0, 42),只是 C 没 this,得手动传 &v
  • 这就是 C++ 虚函数表的雏形:C++ 编译器在背后维护一张函数指针表(vtable),多态调用就是查表 + 间接调用。
  • C 里这种写法在大型 C 工程(内核、glib、curl)里很常见。

11.6 与汇编 / 其他语言对照

  • 汇编p->x[p + offset]——成员偏移在编译期就定死了,所以结构体访问几乎和裸内存访问一样快,没有运行时查表。
  • Java:对象就是「结构体 + 方法表(vtable)+ 对象头(锁/GC 标记)」,obj.x 背后是引用解引用 + 字段偏移,与 C 结构体指针访问同构。
  • C++class 默认就是「结构体 + 成员函数 + 隐式 this」,p->x 语法直接继承自 C。
  • Python:对象是字典(__dict__)+ 类型对象,字段访问是哈希查找,比 C 的结构体偏移慢一两个数量级。

-> = (*p).x 简写;自引用 next 长出链表/树;函数指针成员长出「方法」。