STM32-深入解析C语言结构体与函数指针
本文探讨了结构体与指针在C语言中的核心应用,主要包括:1. 结构体基础概念:通过typedef定义函数指针类型,构建包含函数指针的结构体实现回调机制,分析嵌套结构体的内存布局;2. 结构体初始化的关键技术:使用memset清零内存,指针赋值与数组关联,强调const指针的安全性;3. 5种结构体赋值方式:包括直接赋值、指针赋值、数组关联、函数参数传递和嵌套指针赋值,指出动态内存赋值的风险;4. 函
·
一、结构体基础概念与内存操作
1. 结构体类型定义原理
1.1 函数指针类型定义
// 定义发送函数指针类型:返回值int,参数为(const char* pATSend, int len)
typedef int (*send_function)(const char* pATSend, int len);
// 定义接收函数指针类型:返回值int,参数为(char* pATRecv, int len)
typedef int (*recv_function)(char* pATRecv, int len);
- 本质:
typedef为函数指针创建别名,格式为typedef 返回值类型 (*别名)(参数列表)。 - 作用:将函数指针抽象为类型,便于在结构体中声明成员(如
sendFunction),实现函数回调机制。 - 对比:与普通函数声明的区别在于
*表示指针,需用()包裹别名。
1.2 包含函数指针的结构体
typedef struct {
send_function sendFunction; // 函数指针成员,指向具体发送函数(如GPRS_Send)
char* pATSend; // AT指令字符串指针(如"AT\r\n")
int timeout; // 超时时间(单位:ms)
// 其他成员:接收缓冲区、状态标志等
} Snet_AT;
- 内存布局:结构体成员按声明顺序在内存中连续存放,函数指针占 4 字节(32 位系统),指针变量占 4 字节。
- 设计模式:类似 C++ 的多态,通过函数指针实现 “接口定义 + 具体实现分离”,如不同模块可复用
Snet_AT结构体但绑定不同发送函数。
1.3 嵌套结构体指针的结构体
typedef struct {
const Snet_AT *s_CON; // 指向Snet_AT结构体的常量指针(不可修改指针指向,但可修改指向的内容)
unsigned int status; // 当前模块状态(如初始化、运行、故障)
} SNet;
- 常量指针意义:
const修饰指针本身,确保s_CON始终指向同一Snet_AT实例,避免误操作改变指针指向。 - 应用场景:用于 “配置模板 - 运行实例” 模型,如
s_CON指向只读的默认配置结构体,SNet实例存储运行时状态。
2. 结构体初始化核心操作
void GPRS_Init(void) {
// 1. 内存清零:将sBC260Y结构体的内存全部置0
memset(&sBC260Y, 0, sizeof(sBC260Y));
// 2. 指针赋值:将s_CON指针指向sGPRS_CON数组的第1个元素
sBC260Y.s_CON = &sGPRS_CON[0];
}
2.1 memset函数详解
- 参数解析:
&sBC260Y:结构体变量的地址(需用&取址)。0:填充值(通常为 0,确保布尔值、指针、数值型成员初始化为安全状态)。sizeof(sBC260Y):自动计算结构体大小,避免手动计算错误(如成员增减时无需修改此处)。
- 注意事项:
- 若结构体包含字符串成员,
memset会将其置为\0,相当于空字符串。 - 不可对包含变长数组或动态内存的结构体直接使用
memset(可能导致内存越界)。
- 若结构体包含字符串成员,
2.2 指针赋值与数组关联
&sGPRS_CON[0]vssGPRS_CON:- 数组名
sGPRS_CON在表达式中会隐式转换为指向首元素的指针,因此&sGPRS_CON[0]和sGPRS_CON等价。 - 示例:若
sGPRS_CON是Snet_AT类型数组,则sGPRS_CON的类型为Snet_AT*,指向数组首元素。
- 数组名
- 指针类型匹配:
sBC260Y.s_CON的类型为const Snet_AT*,&sGPRS_CON[0]的类型为Snet_AT*,若sGPRS_CON为非 const 数组,需通过const_cast转换(需谨慎,避免修改只读数据)。
二、结构体赋值的 5 种核心方式
1. 同类型结构体变量直接赋值
Snet_AT sGPRS_CON; // 源结构体变量
Snet_AT sBC260Y; // 目标结构体变量
sBC260Y = sGPRS_CON; // 直接赋值:逐个成员复制
1.1 赋值规则
- 基本类型成员:如
int、char*(仅复制指针值,不复制指向的字符串)直接按字节复制。 - 数组成员:逐元素复制(与
memcpy效果相同)。 - 函数指针成员:直接复制指针值(即指向的函数地址),无需特殊处理。
1.2 潜在风险
- 若结构体包含动态分配内存的指针(如
char* str = malloc(10)),直接赋值会导致多个结构体指向同一内存,释放时可能引发双重释放或野指针问题。 - 解决方案:自定义赋值函数,对动态内存执行深拷贝(
strdup或重新malloc)。
2. 结构体指针赋值(通过地址)
Snet_AT sGPRS_CON[5]; // 结构体数组
SNet sBC260Y; // 包含指针成员的结构体
sBC260Y.s_CON = sGPRS_CON; // 等价于&sGPRS_CON[0],数组名转换为指针
2.1 指针算术运算
- 若
sBC260Y.s_CON指向sGPRS_CON[0],则sBC260Y.s_CON + 1指向sGPRS_CON[1],偏移量为sizeof(Snet_AT)字节。 - 应用场景:遍历结构体数组,如:
for (int i=0; i<5; i++) { sBC260Y.s_CON = &sGPRS_CON[i]; // 逐个指向数组元素 execute_command(); // 执行当前元素对应的命令 }
3. 结构体数组与指针的关联
// 定义包含5个元素的结构体数组
Snet_AT sGPRS_CON[] = {
{"AT\r\n", GPRS_Send, 500}, // 元素0:AT指令初始化
{"ATE0\r\n", GPRS_Send, 600} // 元素1:关闭回显
};
3.1 数组初始化规则
- 每个元素用
{}包裹,按结构体成员顺序赋值(先pATSend,再sendFunction,最后timeout)。 - 未显式初始化的成员自动填充为 0(与
memset效果一致)。
3.2 指针访问数组元素
sBC260Y.s_CON = &sGPRS_CON[1]; // 指向数组第2个元素(索引从0开始)
printf("Current AT command: %s", sBC260Y.s_CON->pATSend); // 输出"ATE0\r\n"
4. 通过函数参数传递结构体指针
// 函数形参为结构体指针,返回值为void
void update_config(SNet* pConfig, const Snet_AT* pTemplate) {
pConfig->s_CON = pTemplate; // 将配置指针指向模板结构体
pConfig->status = 0x01; // 设置状态为"已配置"
}
// 调用示例
SNet device;
update_config(&device, &sGPRS_CON[0]); // 传递device地址和模板地址
4.1 指针传递的优势
- 效率高:无需复制整个结构体(若结构体大小为 100 字节,仅需传递 4 字节指针)。
- 可修改原数据:通过指针可直接修改实参结构体的成员(如
pConfig->status)。
4.2 指针空值检查
- 防御性编程:函数入口处添加
if (pConfig == NULL || pTemplate == NULL) return;,避免空指针解引用导致程序崩溃。
5. 嵌套结构体指针的赋值
typedef struct {
int id;
Snet_AT* commands; // 指向Snet_AT结构体的指针(数组首地址)
} Device;
Device modem;
modem.commands = sGPRS_CON; // 将commands指针指向结构体数组
modem.id = 0x001;
5.1 多级指针的内存模型
modem结构体占sizeof(int) + sizeof(Snet_AT*)字节(如 4+4=8 字节,32 位系统)。modem.commands存储sGPRS_CON数组的首地址,通过modem.commands[0]访问数组第 1 个元素。
5.2 动态内存分配场景
// 动态分配结构体数组
modem.commands = (Snet_AT*)malloc(3 * sizeof(Snet_AT));
if (modem.commands == NULL) { /* 处理内存分配失败 */ }
// 初始化数组元素
modem.commands[0].pATSend = "AT+CSQ\r\n";
modem.commands[0].sendFunction = GPRS_Send;
三、函数指针与结构体的深度结合
1. 函数指针的本质与声明
1.1 函数指针变量定义
send_function ptr_send = GPRS_Send; // 定义指针并指向函数
int result = ptr_send("AT\r\n", 5); // 通过指针调用函数,等价于GPRS_Send("AT\r\n",5)
- 函数名与指针:函数名
GPRS_Send会隐式转换为函数指针,无需使用&取址。 - 类型匹配:
ptr_send的类型需与GPRS_Send的签名(返回值 + 参数)完全一致,否则编译报错。
1.2 匿名函数指针赋值
sBC260Y.s_CON->sendFunction =
(send_function)0x08001234; // 直接赋值函数地址(危险操作,需确保地址有效)
- 风险提示:直接操作函数地址可能导致程序崩溃,仅用于特定嵌入式场景(如固件升级后跳转新函数)。
2. 结构体中函数指针的调用流程
void GPRS_TimeScan(int t) {
// 1. 获取结构体指针:sBC260Y.s_CON指向sGPRS_CON数组首元素
Snet_AT *pCON = sBC260Y.s_CON;
// 2. 调用函数指针:执行pCON指向的结构体中的sendFunction函数
pCON->sendFunction(pCON->pATSend, strlen(pCON->pATSend));
}
2.1 执行流程解析
pCON = sBC260Y.s_CON:从sBC260Y结构体中取出Snet_AT*类型的指针pCON。pCON->pATSend:通过指针访问结构体成员pATSend(AT 指令字符串)。strlen(pCON->pATSend):计算字符串长度,作为函数参数。pCON->sendFunction(...):调用函数指针,等价于(*pCON->sendFunction)(...)(->优先级高于*)。
2.2 空指针保护
- 若
sBC260Y.s_CON未初始化(为NULL),调用pCON->sendFunction会导致段错误。 - 改进代码:
if (pCON != NULL && pCON->sendFunction != NULL) { pCON->sendFunction(...); }
3. 函数指针形参与实参匹配规则
// 定义函数指针类型
typedef int (*handler)(char*, int);
// 函数接受函数指针作为参数
void execute_command(handler fn, char* data, int len) {
fn(data, len); // 调用传入的函数
}
// 具体函数实现(需与handler签名一致)
int process_data(char* buf, int size) {
// 处理数据逻辑
return 0;
}
// 调用示例
execute_command(process_data, "AT+CGMR", 8); // 实参与形参类型匹配
3.1 类型不匹配的后果
- 若实参函数返回值为
void,形参为int (*)(...),编译时可能警告,运行时返回值未定义。 - 若参数顺序或类型不同(如形参为
const char*,实参函数为char*),可能导致栈溢出或数据损坏。
3.2 回调函数的典型应用
- 场景:串口接收数据后,根据不同协议类型调用不同解析函数。
typedef struct { int protocol_id; handler parse_fn; // 解析函数指针 } ProtocolHandler; ProtocolHandler handlers[] = { {0x01, parse_modbus}, {0x02, parse_lora} }; // 根据协议ID调用对应解析函数 void handle_data(int protocol_id, char* data, int len) { for (int i=0; i<sizeof(handlers)/sizeof(handlers[0]); i++) { if (handlers[i].protocol_id == protocol_id) { handlers[i].parse_fn(data, len); // 回调解析函数 break; } } }
四、指针与数组操作的底层原理
1. 结构体指针作为函数形参
void fun(Snet_AT *p) { // 形参为结构体指针
p->timeout = 1000; // 修改指针指向的结构体成员
// p是实参的副本,但若实参是全局指针,修改会影响全局数据
}
// 调用方式:传递结构体变量地址或指针变量本身
Snet_AT inst;
fun(&inst); // 传递变量地址
Snet_AT* p_inst = &inst;
fun(p_inst); // 传递指针变量(等价于&inst)
1.1 指针传递与值传递对比
| 传递方式 | 内存开销 | 能否修改原数据 | 适用场景 |
|---|---|---|---|
| 值传递 | 结构体大小(如 100 字节) | 否 | 结构体小且无需修改原始数据 |
| 指针传递 | 4 字节(指针大小) | 是 | 结构体大或需修改原始数据 |
1.2 指针解引用操作符->与.
p->member等价于(*p).member,但->优先级更高,书写更简洁。- 示例:
(*p).sendFunction("AT", 2); // 等价于 p->sendFunction("AT", 2);
2. 数组数据的解析与重组
unsigned int ReadReceiveCommandInt16(const unsigned char* buffer) {
// 将buffer[0](高字节)左移8位,与buffer[1](低字节)合并为16位整数
unsigned int data = ((unsigned int)(buffer[0] << 8) | buffer[1]);
return data;
}
2.1 大端与小端模式
- 大端模式:高位字节存低地址(如
buffer[0]是高位,buffer[1]是低位,符合本函数逻辑)。 - 小端模式:低位字节存低地址(需改为
(buffer[1] << 8) | buffer[0])。 - 关键提示:通信协议需提前约定字节序,避免解析错误(如 Modbus 协议通常为大端)。
2.2 扩展应用:解析 32 位整数
unsigned int ReadReceiveCommandInt32(const unsigned char* buffer) {
return ((unsigned int)buffer[0] << 24) |
((unsigned int)buffer[1] << 16) |
((unsigned int)buffer[2] << 8) |
buffer[3];
}
3. 数组指针与指针数组的区别
3.1 数组指针(指向数组的指针)
Snet_AT (*p_array)[5]; // 定义指向包含5个Snet_AT元素的数组的指针
Snet_AT arr[5];
p_array = &arr; // 需取数组地址,因为数组名本身是指针,&arr类型为Snet_AT (*)[5]
- 访问元素:
(*p_array)[i]
更多推荐



所有评论(0)