22、结构体(二):结构体指针、指向结构体的指针、结构体作为函数参数(传值与传址)

上一节我们把结构体定义、声明和初始化聊透了。这一节咱们来点硬核的——指针和结构体怎么配合。说实话,指针这东西,刚学的时候觉得它玄乎,用多了你会发现,它其实就是C语言的“遥控器”。

结构体本身可以很大,比如一个学生结构体,里面塞了姓名、学号、成绩数组……你要是每次都用值传递,那效率可就太感人了。我当年刚入行时,就因为这个被老同事调侃过:“你这是在拷贝整个数据库啊?”

好,咱们直接进入正题。

结构体指针:指向结构体的指针

结构体指针,说白了就是一个指针变量,它存的是结构体变量的地址。定义方式很简单:

struct Student {
    char name[20];
    int id;
    float score;
};

struct Student stu1;
struct Student *pStu = &stu1;  // 指向结构体的指针

这里 pStu 就是结构体指针。它指向 stu1 的起始地址。你可能会问:“我直接用 stu1 不就行了,干嘛多此一举?”

嗯,问得好。我举个例子你就明白了。

假设你写了一个函数,要修改学生成绩。如果你传的是结构体本身,函数里改的是副本,原数据纹丝不动。但如果你传的是指针,函数就能直接操作原数据。这就是“传址”的魅力。

访问结构体成员:两种方式

通过结构体指针访问成员,有两种写法:

  1. * 解引用(*pStu).score = 95.5;
  2. -> 箭头运算符pStu->score = 95.5;

我个人习惯用箭头运算符。为什么?因为简洁、直观。你看 (*pStu).score 这写法,括号不能少,点号优先级比星号高,写起来容易出错。箭头运算符就是专门为结构体指针设计的,读起来也顺口:“pStu 指向的 score”。

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

struct Student {
    char name[20];
    int id;
    float score;
};

int main() {
    struct Student stu1;
    struct Student *p = &stu1;

    // 用箭头赋值
    strcpy(p->name, "张三");
    p->id = 1001;
    p->score = 88.5;

    // 用箭头输出
    printf("姓名:%s\n", p->name);
    printf("学号:%d\n", p->id);
    printf("成绩:%.1f\n", p->score);

    return 0;
}

输出结果:

姓名:张三
学号:1001
成绩:88.5
小技巧: 如果你看到 p->name 这种写法,心里默念“p指向的name”,就不会搞混了。

结构体作为函数参数:传值 vs 传址

这是面试高频考点。咱们直接对比着看。

传值(值传递)

传值就是把整个结构体拷贝一份给函数。函数里改的是副本,不影响原数据。

void printStudent(struct Student s) {
    printf("姓名:%s,成绩:%.1f\n", s.name, s.score);
    s.score = 0;  // 只改了副本,原数据不变
}

int main() {
    struct Student stu = {"李四", 1002, 92.0};
    printStudent(stu);
    printf("原成绩:%.1f\n", stu.score);  // 还是 92.0
    return 0;
}

输出:

姓名:李四,成绩:92.0
原成绩:92.0

看到了吧?函数里把成绩改成0,原数据纹丝不动。这就是传值的特点——安全,但效率低。如果结构体很大(比如里面有个大数组),每次传值都要拷贝一整块内存,性能开销不小。

传址(指针传递)

传址就是传结构体的地址,函数通过指针直接操作原数据。

void updateScore(struct Student *p, float newScore) {
    p->score = newScore;  // 直接修改原数据
}

int main() {
    struct Student stu = {"王五", 1003, 78.0};
    updateScore(&stu, 95.0);
    printf("更新后成绩:%.1f\n", stu.score);  // 变成 95.0
    return 0;
}

输出:

更新后成绩:95.0

传址的好处很明显:效率高、能改原数据。但缺点也有——如果你不小心在函数里改了不该改的数据,那就麻烦了。所以,如果你只是想读取数据,不想修改,可以加 const 修饰:

void printStudent(const struct Student *p) {
    printf("姓名:%s,成绩:%.1f\n", p->name, p->score);
    // p->score = 0;  // 编译报错,const 保护了数据
}
注意: 传址时如果忘了加 const,函数内部可能会意外修改原数据。我曾经在项目里遇到过,一个同事写的打印函数里顺手改了成绩,排查了半天才发现。所以,只读函数一定要加 const

什么时候用传值,什么时候用传址?

我总结了一个简单的原则:

场景 推荐方式 原因
结构体很小(如只含几个int) 传值 拷贝开销小,代码简单
结构体较大(含数组、嵌套结构体) 传址 避免拷贝,提升性能
需要修改原数据 传址 传值无法修改原数据
只读访问,不修改 传址 + const 高效且安全

你想想看,如果你写一个链表操作函数,节点里存了上千个字符,传值的话每次调用都要拷贝一遍,那程序跑起来得多慢。所以实际项目中,传址几乎是标配

避坑指南:结构体指针的常见错误

我见过不少新手在结构体指针上栽跟头。这里列几个典型问题:

  • 未初始化指针就使用struct Student *p; p->score = 90; —— 这会导致野指针,程序直接崩溃。记住:指针必须先指向一个有效的结构体变量,或者用 malloc 分配内存。
  • 混淆 .->:结构体变量用 .,结构体指针用 ->。混用的话编译器会报错。
  • 传址时忘记取地址updateScore(stu, 95.0); 而不是 updateScore(&stu, 95.0); —— 这会把结构体内容当成地址,编译可能不报错,但运行结果完全不对。

核心总结:

  • 结构体指针用 -> 访问成员
  • 传值安全但低效,传址高效但需小心
  • 只读函数用 const 保护数据
  • 指针必须初始化后再使用

知识体系图

下面这张图帮你理清结构体指针和传值传址的关系:

结构体操作 结构体指针 函数参数 定义:struct S *p 访问:p->成员 取地址:&变量 传值:副本拷贝 传址:指针传递 const 保护 核心:传址高效,传值安全,const 保平安

这张图把结构体指针和函数参数的关系梳理得很清楚。左边是指针的定义和访问方式,右边是传值和传址的对比。你多看几遍,脑子里就有框架了。


好了,这一节的内容就到这儿。结构体指针和传值传址,说白了就是“怎么高效、安全地操作结构体数据”。我个人建议你多写几个小例子,比如写一个学生成绩管理系统,用结构体指针和传址来实现增删改查。代码写多了,这些概念自然就刻在脑子里了。