一、结构体基础概念与内存操作

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] vs sGPRS_CON
    • 数组名sGPRS_CON在表达式中会隐式转换为指向首元素的指针,因此&sGPRS_CON[0]sGPRS_CON等价。
    • 示例:若sGPRS_CONSnet_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 赋值规则
  • 基本类型成员:如intchar*(仅复制指针值,不复制指向的字符串)直接按字节复制。
  • 数组成员:逐元素复制(与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 执行流程解析
  1. pCON = sBC260Y.s_CON:从sBC260Y结构体中取出Snet_AT*类型的指针pCON
  2. pCON->pATSend:通过指针访问结构体成员pATSend(AT 指令字符串)。
  3. strlen(pCON->pATSend):计算字符串长度,作为函数参数。
  4. 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]
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐