第24章:结构体与指针:结构体指针,->运算符,结构体作为函数参数
结构体这东西,说白了就是C语言里用来打包数据的工具。但光会定义结构体还不够,真正干活的时候,你得跟指针配合着用。这一章我带你把这几个关键点啃透。
24.1 结构体指针:为什么要用它?
先问个问题:你有一个结构体变量,里面装了100个成员。你想把它传给一个函数去处理。直接传值?那意味着在栈上复制一份完整的副本——100个成员全拷一遍。浪费内存,浪费时间。
我在项目中遇到过这种情况:一个结构体里存了传感器的校准参数,总共512字节。一开始团队里有人直接传值,结果每次调用函数都要在栈上压入512字节,堆栈很快就爆了。后来改成传指针,问题瞬间解决。
结构体指针,就是一个指向结构体变量的指针。它只占4字节(32位系统)或8字节(64位系统),传起来轻快得多。
核心要点:结构体指针存储的是结构体变量的地址,通过它可以间接访问结构体的成员。
// 定义结构体
struct SensorData {
int id;
float temperature;
float humidity;
char status;
};
// 声明变量和指针
struct SensorData sensor1;
struct SensorData *pSensor;
// 让指针指向结构体变量
pSensor = &sensor1;
24.2 ->运算符:箭头指向成员
有了结构体指针,怎么访问里面的成员?用点号?不行。点号是给结构体变量用的。指针得用箭头——->。
我个人习惯把->读作「箭头」,它其实就是(*指针).成员的简写。你想想看,每次都要写(*pSensor).temperature多麻烦,而且括号还不能省。箭头运算符让代码干净多了。
// 通过指针访问成员
pSensor->id = 1001;
pSensor->temperature = 25.6;
pSensor->humidity = 68.2;
pSensor->status = 'A';
// 等价写法(不推荐,太啰嗦)
(*pSensor).id = 1001;
小技巧:如果你看到p->member这种写法,记住p一定是指针。如果p是普通变量,编译器会报错。
24.3 结构体作为函数参数:传值 vs 传指针
这是面试常考的点,也是实际开发中容易踩坑的地方。
| 传参方式 | 语法 | 优点 | 缺点 |
|---|---|---|---|
| 传值 | void func(struct SensorData s) |
不影响原数据 | 复制开销大,占用栈空间 |
| 传指针 | void func(struct SensorData *p) |
效率高,可修改原数据 | 需注意指针有效性 |
| 传const指针 | void func(const struct SensorData *p) |
效率高,且保护数据不被修改 | 不能修改原数据 |
我曾经在维护一个嵌入式项目时,看到有人把整个结构体传值进了中断处理函数。中断里栈空间本来就紧张,结果一压栈就溢出了,系统直接复位。查了两天才找到原因。从那以后,我定了个规矩:结构体超过8字节,一律传指针。
// 传值方式——不推荐用于大结构体
void printSensorData(struct SensorData s) {
printf("ID: %d\n", s.id);
printf("Temp: %.1f\n", s.temperature);
}
// 传指针方式——推荐
void printSensorDataPtr(const struct SensorData *p) {
if (p == NULL) return; // 防御性编程
printf("ID: %d\n", p->id);
printf("Temp: %.1f\n", p->temperature);
}
// 传指针并修改——需要改原数据时用
void updateSensorData(struct SensorData *p, float newTemp) {
if (p == NULL) return;
p->temperature = newTemp;
}
注意:传指针时一定要检查是否为NULL。我曾经在代码里忘了做空指针检查,结果传入NULL指针,程序直接段错误。这个bug查了我一下午。
24.4 结构体指针的典型应用场景
实际项目中,结构体指针最常见的用法有这么几种:
- 链表操作:每个节点包含数据和指向下一个节点的指针,遍历时全靠指针跳转。
- 函数回调:把结构体指针作为参数传给回调函数,实现松耦合设计。
- 动态内存分配:用malloc分配结构体内存,返回的就是指针。
- 硬件寄存器映射:把结构体指针强制指向某个内存地址,直接操作硬件寄存器。
// 动态分配结构体内存
struct SensorData *pSensor = (struct SensorData *)malloc(sizeof(struct SensorData));
if (pSensor != NULL) {
pSensor->id = 2001;
// ... 使用完后释放
free(pSensor);
pSensor = NULL; // 避免野指针
}
24.5 结构体指针与数组
结构体数组和指针配合起来,能实现很多灵活的操作。比如你有一批传感器数据,用数组存着,然后用指针遍历。
struct SensorData sensors[3] = {
{1, 23.5, 60.1, 'A'},
{2, 24.0, 65.3, 'B'},
{3, 22.8, 70.0, 'A'}
};
struct SensorData *p = sensors; // 数组名就是首元素地址
// 遍历数组
for (int i = 0; i < 3; i++) {
printf("Sensor %d: %.1f°C\n", (p + i)->id, (p + i)->temperature);
// 等价写法:p[i].temperature
}
嗯,这里要注意:p + i不是简单地在地址上加i,而是加上i * sizeof(struct SensorData)。指针的加减运算,是以它所指向的类型大小为步长的。
24.6 避坑指南
我总结几个常见的坑,都是我在项目里踩过的:
- 野指针:指针没有初始化就使用。声明指针后要么赋有效地址,要么置为NULL。
- 悬空指针:free掉内存后没有置NULL,后续又去访问。我吃过这个亏,程序跑着跑着就崩了。
- 类型不匹配:结构体指针强制转换成其他类型指针,然后访问成员。除非你清楚知道内存布局,否则别这么干。
- 忘记取地址:
p = &sensor写成了p = sensor,编译器可能不报错,但逻辑全错。
总结一下:结构体指针是C语言里连接数据和代码的桥梁。用好->运算符,掌握传指针的技巧,你就能写出高效、可维护的代码。记住:大结构体传指针,小结构体随意;传指针必检NULL,用完记得清空。
我的建议:刚开始学结构体指针时,多画内存布局图。把变量、指针、成员在内存中的位置画出来,理解起来会快很多。我在带新人时,都是让他们先画图,再写代码。