3. 结构体指针与函数:结构体指针、结构体作为函数参数、指向结构体的指针数组

好,咱们接着聊结构体。前面几章我们把结构体定义、初始化、内存对齐这些基础打牢了。这一章,我带你看看结构体在实际工程中怎么用——说白了,就是结构体怎么跟指针、函数打交道。

你想想看,一个结构体里可能装着几十个字段,如果每次传参都整个拷贝一份,那效率得多低?我早年做嵌入式项目时,就吃过这个亏。当时一个数据包结构体有 200 多字节,我直接传值,结果栈空间爆了,程序跑飞。嗯,从那以后,我养成了用结构体指针的习惯。

3.1 结构体指针:轻量级操作的核心

结构体指针,说白了就是一个指向结构体变量的指针。它存的是结构体的首地址。通过这个指针,我们可以间接访问结构体的成员。

定义方式很简单:

struct Student {
    char name[32];
    int age;
    float score;
};

struct Student stu = {"张三", 20, 95.5f};
struct Student *pStu = &stu;  // 结构体指针

访问成员有两种写法。我个人习惯用箭头运算符 ->,因为它更直观:

// 方式一:解引用后取成员
(*pStu).age = 21;

// 方式二:箭头运算符(推荐)
pStu->age = 21;
pStu->score = 98.0f;
我的习惯:只要涉及结构体指针,一律用 ->。代码更干净,也不容易漏括号。你想想看,(*pStu).age 这种写法,括号一多就容易乱。

3.2 结构体作为函数参数:传值 vs 传指针

这是面试高频题,也是实际开发中容易踩坑的地方。我们直接看对比:

传参方式 语法 内存开销 能否修改原数据 适用场景
传值 void func(struct Student s) 整个结构体拷贝(大) 否(副本操作) 结构体很小(≤8字节)
传指针 void func(struct Student *ps) 4/8字节(指针大小) 是(直接操作原数据) 结构体较大或需要修改
传 const 指针 void func(const struct Student *ps) 4/8字节 否(只读保护) 大结构体只读访问

为什么我强调用指针?举个例子。假设你有一个传感器数据结构体,里面包含 10 个浮点数和 5 个整型:

struct SensorData {
    float temperature[10];
    float humidity[10];
    int status[5];
    uint32_t timestamp;
};

// 传值 —— 栈上拷贝 100+ 字节
void process_by_value(struct SensorData data) {
    // 操作的是副本,原数据不变
}

// 传指针 —— 只传 4 字节地址
void process_by_pointer(struct SensorData *pdata) {
    // 直接操作原数据,零拷贝开销
}
我曾经踩过的坑:在中断服务函数里传了一个大结构体(约 500 字节)的值。结果栈溢出,系统复位。后来改成传指针,问题解决。记住:嵌入式环境栈空间很宝贵,能传指针就别传值。

3.3 传 const 指针:只读保护的好习惯

如果你只是想读取结构体数据,不想修改它,那就加个 const。这既是文档,也是编译器帮你做的安全检查。

void print_student(const struct Student *ps) {
    // 可以读
    printf("Name: %s, Age: %d\n", ps->name, ps->age);
    
    // 编译器报错:不能修改
    // ps->age = 30;  // ❌ 编译错误
}

我个人写库函数时,所有只读参数都加 const。这能防止调用方不小心改了不该改的数据。你想想看,如果某个模块偷偷改了你的结构体,排查起来多痛苦?

3.4 指向结构体的指针数组:管理多个对象

这个技巧在实际项目中非常常用。比如你要管理多个设备的状态,每个设备是一个结构体,用指针数组来组织它们:

struct Device {
    int id;
    char name[16];
    int status;  // 0: offline, 1: online
};

struct Device dev1 = {1, "Sensor_A", 1};
struct Device dev2 = {2, "Sensor_B", 0};
struct Device dev3 = {3, "Sensor_C", 1};

// 指针数组:每个元素指向一个 Device 结构体
struct Device *devices[] = {&dev1, &dev2, &dev3};
int device_count = sizeof(devices) / sizeof(devices[0]);

遍历和操作就非常灵活了:

void scan_devices(struct Device *devs[], int count) {
    for (int i = 0; i < count; i++) {
        if (devs[i]->status == 1) {
            printf("Device %d (%s) is online\n", 
                   devs[i]->id, devs[i]->name);
        }
    }
}

// 调用
scan_devices(devices, device_count);

为什么用指针数组而不是结构体数组?两个原因:

  • 节省内存:指针数组只存地址,结构体数组存完整数据。如果结构体很大,指针数组优势明显。
  • 灵活排序:你可以只交换指针,不用搬动整个结构体。排序效率高得多。
核心要点:指针数组 + 结构体 = 高效管理多个对象的黄金组合。我在做物联网网关项目时,用这个模式管理了 200 多个传感器节点,内存占用比结构体数组少了 60%。

3.5 动态分配结构体指针数组

如果设备数量在运行时才能确定,那就需要动态分配:

#include <stdlib.h>

struct Device **create_device_array(int count) {
    // 分配指针数组
    struct Device **arr = (struct Device **)malloc(
        count * sizeof(struct Device *));
    
    if (arr == NULL) {
        return NULL;  // 分配失败
    }
    
    // 为每个指针分配结构体
    for (int i = 0; i < count; i++) {
        arr[i] = (struct Device *)malloc(sizeof(struct Device));
        if (arr[i] == NULL) {
            // 清理已分配的内存
            for (int j = 0; j < i; j++) {
                free(arr[j]);
            }
            free(arr);
            return NULL;
        }
        arr[i]->id = i + 1;
        snprintf(arr[i]->name, 16, "Device_%d", i + 1);
        arr[i]->status = 0;
    }
    return arr;
}

// 记得释放
void free_device_array(struct Device **arr, int count) {
    for (int i = 0; i < count; i++) {
        free(arr[i]);
    }
    free(arr);
}
注意:动态分配一定要成对释放。我曾经在项目中漏了 free,结果内存泄漏,系统跑了三天后挂了。排查了好久才发现是这里的问题。建议写代码时就把 free 写好,再写 malloc。

3.6 结构体指针与函数指针结合

这个稍微进阶一点,但非常实用。结构体里可以放函数指针,实现类似面向对象的多态效果:

struct Animal {
    char name[16];
    void (*speak)(void);  // 函数指针
};

void dog_speak(void) {
    printf("汪汪!\n");
}

void cat_speak(void) {
    printf("喵喵~\n");
}

int main() {
    struct Animal dog = {"旺财", dog_speak};
    struct Animal cat = {"咪咪", cat_speak};
    
    struct Animal *p = &dog;
    p->speak();  // 输出:汪汪!
    
    p = &cat;
    p->speak();  // 输出:喵喵~
    
    return 0;
}

这种写法在嵌入式驱动层很常见。比如不同型号的传感器,初始化、读取数据的函数不同,但接口一致。用结构体指针 + 函数指针,就能统一管理。

3.7 本章知识体系

下面这张图帮你梳理结构体指针与函数的核心脉络:

结构体指针与函数 · 知识体系 结构体指针 函数传参方式 传值:拷贝整个结构体 传指针:只传4/8字节地址 传const指针:只读保护 指针数组管理 静态数组:固定数量 动态分配:运行时确定 排序高效:只交换指针 函数指针结合 实现多态效果 驱动层统一接口 回调函数机制 核心原则:能传指针不传值,能加const就加const

这张图把本章的核心内容串起来了。中心是结构体指针,三个分支分别对应函数传参、指针数组管理、函数指针结合。底部是核心原则——记住它,能帮你避开 90% 的坑。

我的建议:刚开始学的时候,先把传值和传指针的区别搞清楚。动手写几个小例子,感受一下内存变化。等你熟练了,再玩指针数组和函数指针。一步一步来,别急。

好了,这一章就到这里。结构体指针是 C 语言里非常实用的工具,用好了能让你的代码既高效又优雅。下一章我们聊聊结构体嵌套和自引用,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321