C语言笔记8之经验总结 说明以下是出道 two years 的经验笔记持续更新笔记内容tip1当一个结构体变量涉及到flash 读写、通信收发按照特定字节格式的协议等其数据内容不能受到默认结构体对齐填补后的影响时需要加上 __attribute__((packed))让其按照字节的格式对齐tip2解析接收数据帧的常规思路1、先从接收缓冲区找到完整数据帧2、通过强制类型转换获取数据帧3、注意这里指针类型强转后Protal_st *p (Protal_st *)pData假设pData执行buff[0]实际上p-data 的值是 buff[3]、buff[4]、buff[5]、buff[6]组合的一个int值赋给的所以这里需要重新指定p-data (pData offsetof(Protal_st, data))即p-data buff[3]4、注意一般来说单片机都是小端模式的所以报文通信也必须是小端模式即低字节在前否则 这里的 uint16_t len p-len 还需要高低字节互换tip3注意函数的参数副本概念上一篇已经说过了tip4当传递函数名 myFunction 时两种方式传 myFunction 或 myFunction 都行都是指传递函数名的地址tip5注意不要混淆联合体和枚举一样的位置当struct 时上边是类型名下边是变量当typedef struct时下边是类型名其不能直接定义变量以为typedef 是声明作用声明一个别名tip6注意以下两种在链表中定义节点的方式第一种是错误的我的理解方式总的来说就是当第一种typedef 方式时编译器在 编译 ListNote *preNote 这行时它找不到ListNote 这个关键字因为下一行这个 ListNote 关键字才出现当第二种 struct 方式时编译器在 编译 struct ListNote *preNote时它在第一行就已经找到 struct ListNote的关键字了即使此时struct ListNote 依旧是未完全定义但编译是通过的因此再看下面两种方式就很容易理解了tip7结构体成员的访问和最终表示总体来说最终表示的是数组/变量/指针就看最后一个成员变量是什么类型的数据tip8注意 sizeof 宏定义的使用sizeof 变量名、数组名、类型名返回的是占用的字节数sizeof 地址、指针返回的是4个字节即 int 类型tip9注意结构体类型当不加 __attribute__((packed))即按照编译器默认结构体对齐也就是说当你使用这个结构体变量读到内存中时编译器会自动补齐即sizeof(结构体类型/变量) 成员变量占用字节大小的累积和其实结构体类型的对齐方式是编译阶段就已经是确定了其需填充字节数也确定了假如上述结构体类型在编译阶段按照 uint32_t 类型对齐对齐结果如下通常补齐的字节都是0x00我的理解由于结构体成员地址统一对齐后其成员的访问方式通过地址偏移操作大大提升了CPU的访问结构体成员的效率当成员未对齐即按照字节的偏移方式访问成员一个字节一个字节的查询故效率低下tip10取消结构体的默认对齐方式通常两种1、使用__attribute__((__packed__))针对某个结构体且只能按照1个字节对齐2、使用宏针对多个结构体且能按照1、2、4、8等字节对齐tip11inline函数编译阶段在函数调用的地方将函数内容展开后续将不需要执行函数调用的开销缺点是代码量增大所以用法函数内容少一般几行的函数可以使用内联函数较少函数调用的开销提高性能tip12 结构体和联合体1、联合体的初始化2、联合体的赋值3、结构体的初始化4、结构体的赋值tip13、函数指针1、普通声明p是一个函数指针指向的函数类型是 func2、函数指针类型声明cert_handler_op 是一种函数指针类型指向 func 这种类型的函数。op 是函数指针func 函数名本身就代表该函数的地址故可直接赋值。tip14、数组指针tip15、指针数组tip16、void * 通用指针void 表示无类型函数定义时表示无返回值、无参数此时不需要加return如下void *p NULL p 是一个通用指针tip17、回调函数1、概念回调函数本质上就是函数指针那使用回调函数的时机是什么首先理解两个角色以及其负责的工作1、回调函数提供者负责提供回调函数管控具体的回调函数的内部执行内容但不管控回调函数的执行时机、具体的参数传递。2、回调函数执行者负责在适当的时机执行回调函数传递具体的参数不关心回调函数内部的实现接下来跟着应用场景去理解回调函数的使用时机、优势2、应用场景1、事件驱动假设我们在应用层的模块中此时程序执行到某个时机需要执行驱动层的继电器闭合操作通常我们是在代码的执行地方调用驱动层的函数接口如下图那问题来了如果我驱动层代码换了继电器闭合的接口函数改名了那我需要替换上述应用层调用的接口函数。也就是说驱动层改了应用层也需要跟着改没法实现应用层和驱动层的解耦1、应用层头文件假设如图是回调函数类型、应用层上下文的结构体2、应用层上下文实例3、应用层注册函数4、驱动层具体的继电器操作函数驱动层不管控回调函数的执行时机、具体的参数传递只负责具体的回调函数的实现5、应用层运行前先注册好驱动层的继电器操作的回调函数若驱动层的继电器操作函数变了应用层只需替换这里contactorA_Opt —— contactorB_Opt6、应用层执行继电器闭合的地方应用层不关心具体的回调函数的实现只管控回调函数的执行时机、具体的参数传递7、结论因此应用层和驱动层的唯一耦合在 register_event() 函数解耦完成2、异步通知假设有两个状态机A、B主状态机A 负责整机流程B则负责从FTP服务器下载文件。当B下载完成后一般都通过变量标志位Flag 或 事件组标志位。当然也可以通过回调函数如图1、A、B上下文2、A、B上下文实例3、B 提供注册函数4、A 提供具体的通知函数状态机A不管控回调函数的执行时机、具体的参数传递只负责具体的回调函数的实现5、A 在 B 去下载文件前注册下载完成通知函数6、A状态机中等待 B下载完成waiting.....7、B状态机中下载完成执行通知回调函数通知A下载完成状态机B不关心具体的回调函数的实现只管控回调函数的执行时机、具体的参数传递8、结论虽然解耦了但似乎把代码复杂化了若使用事件组标志位一两句就搞定了无需这么多麻烦的封装。因此合理选择使用回调函数解耦的时机很重要3、状态机之间数据传递1、假设有两个状态机A、B上下文如下场景是A可以设置B的valueB可以获取A的status2、A、B的上下文实例3、A提供具体的获取status函数4、B提供具体的设置value函数5、A的注册回调函数6、B的注册回调函数7、A中设置B的value8、B中获取A的status4、读写Buff假设应用层在某个事件下需要从SD/外部Flash/模块收发缓冲区/.....读or写入一串数据。1、上下文如下2、上下文实例3、驱动层提供具体的读写函数4、读写注册函数5、读写的执行过程