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,比如 pDevpStu。这样一眼就能看出这是个指针,避免误用 . 运算符。

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 本章知识体系

下面这张图总结了结构体指针的核心知识点和它们之间的关系:

结构体指针 定义与初始化 struct Student *p = &stu; -> 运算符 p->member 等价于 (*p).member 作为函数参数 传指针 vs 传值:效率高、可修改原数据 动态内存分配 malloc + free,记得释放后置 NULL 核心优势 节省内存 传递高效 可修改原数据 支持动态分配 适合链表等结构 常见陷阱 野指针 内存泄漏 忘记置 NULL 数组赋值错误 sizeof 误用

结构体指针是 C 语言中连接数据和逻辑的桥梁。掌握了它,你就能写出更高效、更优雅的代码。我个人觉得,结构体指针用得好不好,是区分初级和高级 C 程序员的一个重要标志。

最后分享一个习惯:我每次定义结构体时,都会顺手写一个对应的指针类型别名:typedef struct Student *PStudent;。这样在函数参数中直接写 PStudent p,代码更简洁。但要注意,别滥用 typedef,否则别人看你的代码会一头雾水。


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