指针与结构体:结构体指针、通过指针访问结构体成员(->运算符)
好,咱们今天聊一个非常实用的组合——指针和结构体。说实话,我刚学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->age = 20;,结果p指向的是随机地址,程序直接崩溃。记住:指针在使用前必须指向有效的内存。 - 混淆结构体变量和指针:看到
stu.name和p->name两种写法,心里要清楚 stu 是变量,p 是指针。写代码时手一滑,把p->name写成p.name,编译器会报错。 - 结构体指针作为函数参数时忘记判空:尤其是在嵌入式环境,指针可能因为各种原因变成NULL。函数入口处加一句
if (p == NULL) return;能省去很多调试时间。
结构体指针这东西,说白了就是「用地址操作数据」。刚开始用可能觉得多此一举,但等你真正遇到性能瓶颈或者需要动态管理数据时,就会明白它的威力。多写几个例子,多在实际项目里用用,慢慢就熟练了。
公众号:蓝海资料掘金营,微信deep3321