16、指针与结构体:结构体指针的定义与使用、->运算符、结构体指针作为函数参数
结构体这东西,说白了就是把一堆不同类型的数据打包成一个新类型。你想想看,一个学生有学号、姓名、成绩,用三个独立变量去管理,多累啊。但结构体指针,才是真正让结构体发挥威力的关键。
我个人习惯是:只要结构体数据量超过几十个字节,我几乎不用结构体变量本身,而是用指针。为什么?因为传指针比传整个结构体副本快太多了。我在项目中遇到过一位同事,传了一个 1KB 的结构体给函数,每次调用都复制一遍,结果整个系统响应慢得像蜗牛。换成指针后,问题瞬间解决。
16.1 结构体指针的定义与初始化
结构体指针,就是一个指向结构体变量的指针。定义方式很简单:
struct Student {
int id;
char name[32];
float score;
};
// 定义结构体变量
struct Student stu1 = {1001, "张三", 89.5};
// 定义结构体指针,指向 stu1
struct Student *pStu = &stu1;
这里要注意,pStu 存储的是 stu1 的地址。通过指针访问成员,有两种方式:
(*pStu).id—— 先解引用,再取成员。括号不能少,因为.优先级高于*。pStu->id—— 直接用->运算符,更直观。
嗯,这里要注意:-> 是专门为结构体指针设计的。你写 pStu->id,编译器就知道你要访问指针指向的结构体的 id 成员。
核心要点:结构体指针本身只占 4 或 8 个字节(取决于平台),无论它指向的结构体有多大。这就是指针的高效之处。
16.2 -> 运算符的深入理解
-> 运算符,其实就是 (*ptr).member 的语法糖。但为什么 C 语言要专门设计这个运算符?
我曾经在调试一个嵌入式系统时,代码里到处都是 (*pDev)->status 这种写法,看得我头皮发麻。后来全部改成 pDev->status,代码清晰多了。说白了,-> 就是让代码更可读。
来看一个实际例子:
struct Device {
uint8_t id;
uint8_t status;
uint16_t value;
};
void process_device(struct Device *dev) {
// 这两种写法完全等价
uint8_t id1 = (*dev).id; // 方式一:解引用后取成员
uint8_t id2 = dev->id; // 方式二:直接使用 ->
// 但方式二明显更简洁
dev->status = 0x01;
dev->value = 1024;
}
个人技巧:我习惯在结构体指针变量命名时加前缀 p,比如 pDev、pStu。这样一眼就能看出这是个指针,避免误用 . 运算符。
16.3 结构体指针作为函数参数
这是结构体指针最常用的场景。为什么不用结构体变量本身?我给你算笔账:
| 传递方式 | 内存开销 | 速度 | 能否修改原数据 |
|---|---|---|---|
| 传结构体变量(值传递) | 整个结构体大小(如 40 字节) | 慢(需要复制) | 不能 |
| 传结构体指针 | 4 或 8 字节 | 快(只传地址) | 能 |
你看,传指针几乎在所有方面都胜出。我在项目中处理过 200 多个字段的结构体,如果传值,每次调用都要在栈上分配几百字节,栈空间很快就爆了。传指针就完全没这个问题。
来看一个完整的例子:
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[32];
float score;
};
// 用结构体指针作为参数
void print_student(const struct Student *p) {
// 加 const 表示只读,防止意外修改
printf("ID: %d, Name: %s, Score: %.1f\n",
p->id, p->name, p->score);
}
// 修改结构体内容
void update_score(struct Student *p, float new_score) {
p->score = new_score; // 直接修改原数据
}
int main() {
struct Student stu = {1001, "李四", 78.5};
print_student(&stu); // 传地址,只读访问
update_score(&stu, 92.0); // 传地址,修改原数据
print_student(&stu);
return 0;
}
我曾经踩过的坑:在函数内部用 p->name = "王五" 想修改字符串,结果编译报错。因为 name 是数组,不能直接赋值。必须用 strcpy(p->name, "王五")。记住:数组名不是左值,不能放在赋值号左边。
16.4 结构体指针与动态内存分配
在实际工程中,结构体经常和 malloc 配合使用。比如你不知道要处理多少个学生,那就动态分配:
struct Student *create_student(int id, const char *name, float score) {
struct Student *p = (struct Student *)malloc(sizeof(struct Student));
if (p == NULL) {
printf("内存分配失败!\n");
return NULL;
}
p->id = id;
strcpy(p->name, name);
p->score = score;
return p;
}
// 使用完后一定要释放
void destroy_student(struct Student *p) {
if (p != NULL) {
free(p);
p = NULL; // 防止野指针
}
}
嗯,这里有个细节:sizeof(struct Student) 计算的是整个结构体的大小,包括可能的对齐填充字节。用 malloc 分配时,一定要用 sizeof,不要自己算字节数,不同平台可能不一样。
16.5 结构体指针数组
有时候你需要管理多个结构体指针,那就用指针数组:
struct Student *class[50]; // 最多管理 50 个学生
// 初始化
for (int i = 0; i < 3; i++) {
class[i] = create_student(2000 + i, "学生", 60.0 + i * 10);
}
// 遍历
for (int i = 0; i < 3; i++) {
printf("%d: %s, %.1f\n", class[i]->id, class[i]->name, class[i]->score);
}
// 释放
for (int i = 0; i < 3; i++) {
destroy_student(class[i]);
}
这种模式在嵌入式系统中很常见。比如管理多个传感器设备,每个设备有自己的配置参数,用结构体指针数组来管理,清晰又高效。
避坑指南:我曾经在释放结构体指针数组时,忘记把已释放的指针置为 NULL。结果后续代码不小心又访问了那个指针,导致野指针崩溃。记住:free 之后一定要置 NULL,这是保命习惯。
16.6 本章知识体系
下面这张图总结了结构体指针的核心知识点和它们之间的关系:
结构体指针是 C 语言中连接数据和逻辑的桥梁。掌握了它,你就能写出更高效、更优雅的代码。我个人觉得,结构体指针用得好不好,是区分初级和高级 C 程序员的一个重要标志。
最后分享一个习惯:我每次定义结构体时,都会顺手写一个对应的指针类型别名:typedef struct Student *PStudent;。这样在函数参数中直接写 PStudent p,代码更简洁。但要注意,别滥用 typedef,否则别人看你的代码会一头雾水。