指针与结构体:结构体指针、通过指针访问结构体成员(->运算符)

好,咱们今天聊一个非常实用的组合——指针和结构体。说实话,我刚学C语言那会儿,觉得结构体就是个打包数据的工具,指针就是个地址。直到我在一个嵌入式项目里,需要把一个包含几十个字段的设备配置结构体传给函数……嗯,那时候我才真正体会到,结构体指针这东西,用好了是神器,用不好就是噩梦。

为什么需要结构体指针?

你想想看,一个结构体里可能装着好几个int、float,甚至数组。如果你直接把这个结构体传给函数,C语言会怎么做?它会复制一份!没错,整个结构体所有字段全部拷贝一遍。如果结构体很大,比如几百个字节,那每次函数调用都要在栈上复制这么一大块数据,效率低不说,还容易把栈空间撑爆。

我在一个物联网终端项目里就遇到过这个问题。当时定义了一个传感器数据结构体,里面包含时间戳、温度、湿度、气压、GPS坐标……总共大概200多字节。一开始我图省事,直接传结构体变量,结果在中断服务函数里调用时,栈溢出导致系统复位。查了两天才发现是结构体拷贝惹的祸。从那以后,我养成了一个习惯:只要结构体超过16字节,一律用指针传递。

核心原则:结构体较大时,用指针传递避免拷贝;需要修改结构体内容时,用指针传递才能生效。

结构体指针的定义与使用

结构体指针的定义其实很简单,就是在普通结构体变量名前加个星号:

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

struct Student stu1;        // 普通结构体变量
struct Student *pStu;       // 结构体指针变量

让指针指向结构体变量,用取地址符&:

pStu = &stu1;   // pStu 指向 stu1

到这里都还比较常规,对吧?接下来才是重点——怎么通过指针访问结构体成员。

两种访问方式:(*p).member 与 p->member

假设我们有一个结构体指针 pStu 指向 stu1,要访问 stu1 的 age 成员,有两种写法:

方式一:先解引用,再取成员

(*pStu).age = 20;   // 注意括号不能少

这里有个坑——点运算符 . 的优先级高于解引用运算符 *。如果不加括号,写成 *pStu.age,编译器会理解为 *(pStu.age),而 pStu 是个指针,它没有 age 这个成员,编译直接报错。我刚开始学的时候就被这个坑过,调试了半天才发现是优先级的问题。

方式二:用箭头运算符 ->(推荐)

pStu->age = 20;   // 简洁明了,推荐使用

箭头运算符 -> 是C语言专门为结构体指针设计的语法糖。它把「解引用」和「取成员」两个操作合并成一个,写起来顺手,读起来也直观。我个人习惯,只要看到 ->,脑子里立刻就知道这是个结构体指针在操作。

我的建议:在实际项目中,统一使用 -> 运算符访问结构体指针的成员。这样代码风格一致,可读性也更好。除非你是在写一些非常底层的、需要明确区分解引用步骤的代码,否则没必要用 (*p).member 这种写法。

完整示例:学生信息管理

来看一个完整的例子,把上面说的串起来:

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

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

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

void updateScore(struct Student *p, float newScore) {
    p->score = newScore;  // 通过指针修改成员
}

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

    // 初始化
    strcpy(pStu->name, "张三");
    pStu->age = 20;
    pStu->score = 85.5;

    printf("修改前:\n");
    printStudent(pStu);

    updateScore(pStu, 92.0);

    printf("\n修改后:\n");
    printStudent(pStu);

    return 0;
}

运行结果:

修改前:
姓名: 张三
年龄: 20
成绩: 85.5

修改后:
姓名: 张三
年龄: 20
成绩: 92.0

注意看,updateScore 函数接收的是结构体指针,它直接修改了 stu1 的 score 成员。如果传的是结构体本身,修改的只是副本,原数据不会变。这就是指针传递的核心价值——让函数拥有修改原结构体的能力

结构体指针与动态内存分配

在实际的嵌入式开发中,结构体指针经常和 malloc/free 配合使用。比如你需要创建一个链表节点,或者动态管理设备配置:

#include <stdlib.h>

struct DeviceConfig {
    int deviceId;
    char deviceName[32];
    int baudRate;
    int dataBits;
};

// 动态创建配置
struct DeviceConfig* createConfig(int id, const char *name) {
    struct DeviceConfig *pConfig = 
        (struct DeviceConfig*)malloc(sizeof(struct DeviceConfig));
    
    if (pConfig == NULL) {
        // 内存分配失败处理
        return NULL;
    }
    
    pConfig->deviceId = id;
    strcpy(pConfig->deviceName, name);
    pConfig->baudRate = 115200;
    pConfig->dataBits = 8;
    
    return pConfig;
}

// 使用完后释放
void destroyConfig(struct DeviceConfig *p) {
    if (p != NULL) {
        free(p);
    }
}

我曾经踩过的坑:用 malloc 分配结构体内存后,一定要检查返回值是否为 NULL。在资源受限的嵌入式系统里,内存分配失败是常有的事。另外,记得用 free 释放,否则内存泄漏会让你追查得焦头烂额。我有个同事,就是因为忘了释放一个设备配置结构体,导致系统运行72小时后内存耗尽死机……排查过程极其痛苦。

结构体指针的常见应用场景

  • 函数参数传递:避免大结构体拷贝,提高效率
  • 链表、树等数据结构:节点之间通过指针连接
  • 回调函数:传递上下文信息
  • 硬件寄存器映射:用结构体指针直接操作内存地址(嵌入式常用)

说到硬件寄存器映射,这是嵌入式开发的经典用法。比如某个外设的寄存器基地址是 0x40020000,你可以这样定义:

// 定义寄存器结构体
typedef struct {
    volatile uint32_t CR;    // 控制寄存器,偏移0x00
    volatile uint32_t SR;    // 状态寄存器,偏移0x04
    volatile uint32_t DR;    // 数据寄存器,偏移0x08
} UART_Registers;

// 将指针指向基地址
#define UART1 ((UART_Registers*)0x40020000)

// 直接操作寄存器
UART1->CR = 0x01;   // 使能UART
uint8_t data = UART1->DR;  // 读取数据

你看,通过结构体指针,我们可以用非常直观的方式操作硬件寄存器。每个寄存器有名字、有类型,代码可读性比直接写地址偏移量好太多了。

结构体指针的SVG知识图谱

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

结构体指针 定义:struct Student *p; 赋值:p = &stu; 访问成员(两种方式) (*p).age = 20 先解引用,再取成员 p->age = 20 箭头运算符(推荐) 应用场景 函数参数传递 链表/树结构 硬件寄存器映射 动态内存管理

避坑指南

最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路:

我曾经犯过的错:

  • 未初始化指针就使用:声明了 struct Student *p; 就直接 p->age = 20;,结果p指向的是随机地址,程序直接崩溃。记住:指针在使用前必须指向有效的内存。
  • 混淆结构体变量和指针:看到 stu.namep->name 两种写法,心里要清楚 stu 是变量,p 是指针。写代码时手一滑,把 p->name 写成 p.name,编译器会报错。
  • 结构体指针作为函数参数时忘记判空:尤其是在嵌入式环境,指针可能因为各种原因变成NULL。函数入口处加一句 if (p == NULL) return; 能省去很多调试时间。

结构体指针这东西,说白了就是「用地址操作数据」。刚开始用可能觉得多此一举,但等你真正遇到性能瓶颈或者需要动态管理数据时,就会明白它的威力。多写几个例子,多在实际项目里用用,慢慢就熟练了。


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