结构体 + 指针 = C 数据结构的根基:-> 运算符、自引用结构、函数指针成员。
-> 运算符结构体指针访问成员,用 ->(等价于 (*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 实际是「先解引用取结构体,再取成员」,编译器算好成员偏移,一次地址运算完成。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 *。结构体里能放「指向自身类型」的指针(不能直接放自身,会无限嵌套),这是链表的基石:
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 章的二级指针心法。
结构体成员在内存里按声明顺序排布,但会插入**填充(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 的对齐——编译器靠填充保证这一点。把数据 + 操作数据的方法(函数指针)放进同一个结构体,就是 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
p->x 是 [p + offset]——成员偏移在编译期就定死了,所以结构体访问几乎和裸内存访问一样快,没有运行时查表。obj.x 背后是引用解引用 + 字段偏移,与 C 结构体指针访问同构。class 默认就是「结构体 + 成员函数 + 隐式 this」,p->x 语法直接继承自 C。__dict__)+ 类型对象,字段访问是哈希查找,比 C 的结构体偏移慢一两个数量级。
->=(*p).x简写;自引用next长出链表/树;函数指针成员长出「方法」。