第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,用完记得清空。

结构体与指针知识体系 结构体指针 定义与声明 -> 箭头运算符 函数参数传递 指针变量 & 取地址 p->member (*p).member 传值(复制开销大) 传指针(推荐) const指针(只读) 应用场景:链表 · 回调函数 · 动态内存 · 硬件映射

我的建议:刚开始学结构体指针时,多画内存布局图。把变量、指针、成员在内存中的位置画出来,理解起来会快很多。我在带新人时,都是让他们先画图,再写代码。

专注资料整理