13、指针与结构体:结构体指针、->运算符、结构体指针作为函数参数、结构体数组的指针

结构体这东西,说白了就是把一堆不同类型的数据打包成一个新类型。但真正让结构体发挥威力的,是它和指针的结合。我刚开始学C语言时,总觉得结构体指针多此一举——直接用结构体变量不就行了?后来在嵌入式项目里被内存打脸,才明白指针才是结构体的灵魂。

13.1 结构体指针:为什么需要它?

先看一个最简单的例子。假设我们定义一个学生结构体:

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

如果你直接定义一个结构体变量:

struct Student stu;
stu.age = 18;

这没问题。但当你需要把这个结构体传给函数时,问题就来了。如果你传的是整个结构体,C语言会做一次完整的拷贝——所有成员都复制一遍。如果结构体很大(比如里面有数组),这个拷贝的开销就很可观了。

核心区别:

  • 传结构体变量:拷贝整个结构体,占用栈空间大,效率低
  • 传结构体指针:只拷贝一个地址(4或8字节),效率高

我在一个数据采集项目里遇到过这种情况。当时定义了一个包含128个采样点的结构体,每个采样点又是浮点数。如果每次调用函数都传整个结构体,栈空间直接爆掉。换成指针后,问题迎刃而解。

13.2 -> 运算符:箭头指向成员

结构体指针访问成员,用的是箭头运算符 ->。这个符号很形象——箭头指向结构体内部的某个成员。

struct Student stu = {"张三", 20, 89.5};
struct Student *p = &stu;

// 通过指针访问成员
printf("姓名: %s\n", p->name);
printf("年龄: %d\n", p->age);
printf("成绩: %.1f\n", p->score);

你可能会问:为什么不用 *p.name?嗯,这里有个运算符优先级的问题。. 的优先级比 * 高,所以 *p.name 会被解析成 *(p.name),这显然不是我们想要的。正确的写法是 (*p).name,但每次加括号太麻烦。所以C语言干脆提供了 -> 运算符,既直观又安全。

个人习惯:我写代码时,只要是通过指针访问结构体成员,一律用 ->。这样代码读起来一目了然——看到箭头就知道是在操作指针指向的结构体。

13.3 结构体指针作为函数参数

这是实际项目中最常见的用法。我们写一个函数来打印学生信息:

void printStudent(const struct Student *p) {
    if (p == NULL) {
        printf("无效指针\n");
        return;
    }
    printf("姓名: %s, 年龄: %d, 成绩: %.1f\n",
           p->name, p->age, p->score);
}

注意这里用了 const 修饰。为什么?因为打印函数不应该修改结构体的内容。加上 const 后,编译器会帮你检查——如果你不小心写了 p->age = 21,编译直接报错。这是防御性编程的好习惯。

再看一个修改结构体内容的例子:

void updateScore(struct Student *p, float newScore) {
    if (p == NULL) return;
    p->score = newScore;
}

这里就不能加 const 了,因为我们确实要修改数据。

我曾经踩过的坑:在函数里直接修改了结构体指针指向的内容,但调用方传进来的是 const 指针。编译器报了一堆警告,我当时没在意,结果运行时数据被意外篡改。从那以后,我养成了一个习惯:函数参数能加 const 就加,不能加就明确写清楚。

13.4 结构体数组的指针

结构体数组在实际项目中太常见了。比如一个班级有50个学生,我们通常会定义:

struct Student class[50];

那么,指向这个数组的指针怎么定义?

struct Student (*pClass)[50];  // 指向包含50个Student的数组
pClass = &class;

这种写法看着有点绕。其实你想想看,class 本身就是一个数组名,它代表数组首元素的地址。所以:

struct Student *p = class;  // p指向class[0]
p->age = 18;               // 等价于 class[0].age = 18
(p+1)->age = 19;           // 等价于 class[1].age = 19

遍历结构体数组的典型写法:

void printAllStudents(const struct Student *arr, int count) {
    for (int i = 0; i < count; i++) {
        printf("第%d个学生: %s, %d岁, 成绩%.1f\n",
               i+1, arr[i].name, arr[i].age, arr[i].score);
    }
}

// 调用
printAllStudents(class, 50);

这里 arr[i] 等价于 *(arr + i),访问的是第i个结构体变量。然后用 . 访问其成员。

关键理解:

  • struct Student *p 可以指向单个结构体,也可以指向结构体数组的首元素
  • 通过 p + i 可以访问数组中的第i个元素
  • p->member 访问当前指向的结构体成员
  • (p + i)->member 访问第i个结构体的成员

13.5 综合示例:学生成绩管理系统

把上面的知识点串起来,写一个简单的成绩管理函数:

#include <stdio.h>
#include <string.h>

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

// 初始化学生信息
void initStudent(struct Student *p, const char *name,
                 int age, float score) {
    strcpy(p->name, name);
    p->age = age;
    p->score = score;
}

// 计算平均分
float calcAverage(const struct Student *arr, int count) {
    float sum = 0;
    for (int i = 0; i < count; i++) {
        sum += arr[i].score;
    }
    return count > 0 ? sum / count : 0;
}

// 查找最高分
const struct Student* findTop(const struct Student *arr, int count) {
    if (count <= 0) return NULL;
    const struct Student *top = &arr[0];
    for (int i = 1; i < count; i++) {
        if (arr[i].score > top->score) {
            top = &arr[i];
        }
    }
    return top;
}

int main() {
    struct Student class[3];
    initStudent(&class[0], "张三", 20, 85.5);
    initStudent(&class[1], "李四", 21, 92.0);
    initStudent(&class[2], "王五", 19, 78.5);

    printf("平均分: %.2f\n", calcAverage(class, 3));

    const struct Student *top = findTop(class, 3);
    if (top != NULL) {
        printf("最高分: %s, %.1f\n", top->name, top->score);
    }
    return 0;
}

这个例子展示了结构体指针的几种典型用法:初始化时传指针修改内容、计算时传 const 指针只读访问、查找时返回指针指向找到的元素。

避坑指南:我曾经在返回结构体指针时,返回了局部变量的地址。比如:

struct Student* getStudent() {
    struct Student s = {"临时", 0, 0};
    return &s;  // 危险!s是局部变量,函数返回后内存被回收
}

正确的做法是:要么返回传入的指针参数,要么用 malloc 动态分配内存。

13.6 知识体系图

下面这张图帮你理清结构体指针的核心脉络:

结构体指针 定义与初始化 struct Student *p = &stu; p = (struct Student*)malloc(...); -> 运算符 p->name, p->age 等价于 (*p).name 函数参数 void func(const struct S *p) 传指针,避免拷贝 结构体数组指针 struct Student arr[50]; struct Student *p = arr; // 指向首元素 核心:指针操作结构体,高效、灵活、节省内存 注意:const保护、NULL检查、避免返回局部变量地址

13.7 总结

结构体指针是C语言中非常实用的工具。我个人总结了三个要点:

  • 用指针传参:避免结构体拷贝,提高效率。能加 const 就加,保护数据安全。
  • -> 访问成员:比 (*p).member 更直观,代码可读性更好。
  • 数组名就是指针:结构体数组名就是首元素地址,配合指针运算可以灵活遍历。

嗯,这些知识点看起来简单,但真正用好需要多写多练。下次你在项目中遇到需要传递大量结构体数据时,不妨想想今天讲的这些——用指针,别用拷贝。


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