做耳机开发,需要掌握哪些 C 语言知识?

TWS 应用层的大部分工作,最后还是要落到 C 代码上。这篇整理开发中经常用到、也容易混淆的内容,方便需要时快速回顾。

一、数据类型与类型转换

嵌入式代码经常使用 uint8_tuint16_tuint32_t,因为它们的位宽是明确的。

int 占多少字节,由芯片架构和编译器决定。在常见的 32 位平台上,int 通常占 4 字节;在部分嵌入式平台上,也可能只占 2 字节。可以使用 sizeof 查看当前平台上的实际大小:

c 复制代码
printf("int size: %u\n", (unsigned int)sizeof(int));

sizeof 返回的是 size_t 类型。这里先把结果转换成 unsigned int,再使用 %u 输出,适合常见的嵌入式 printf

普通计算和循环计数可以使用 int。通信协议、Flash 数据、文件格式和寄存器字段需要明确长度时,优先使用固定位宽类型。

类型转换主要注意下面几种情况:

情况 需要注意什么
大类型转小类型 高位可能被截断
有符号转无符号 负数会变成很大的正数
无符号与有符号比较 有符号数可能先被转成无符号数
指针类型转换 只改变读取内存的方式,不会改变原来的数据

注意:有符号数和无符号数比较前,尽量把两边统一成合适的类型。进行指针类型转换时,要确认原内存长度不小于目标类型的大小。例如原数据只有 1 字节,转换成 uint32_t * 后却按照 4 字节读取,就会读到不属于它的内存。

二、指针、数组与生命周期

指针保存的是地址。使用前主要检查四件事:

  • 指针是否为空。
  • 指向的内存是否仍然有效。
  • 访问长度是否越界。
  • 指针指向的数据类型是否正确。
c 复制代码
void update_state(uint8_t *state)
{
    if (state == NULL) {
        return;
    }

    *state = 1;
}

数组传入函数后通常会退化成指针,函数内部无法再通过 sizeof 得到原数组长度。因此传递缓冲区时,最好同时传入长度。

c 复制代码
void parse_data(const uint8_t *data, uint16_t len);

也不要返回局部变量的地址。函数退出后,局部变量的生命周期已经结束,原来的地址不能继续使用。

三、几个常见关键字

关键字 主要作用
static 限制跨文件可见性,或让局部变量保留上次的值
const 限制通过当前对象或指针修改数据
volatile 要求编译器每次实际访问内存,但不保证原子性和线程安全
extern 声明符号定义在其他文件中,由链接器查找

const 放在不同位置,含义也不同:

写法 能否通过指针修改数据 指针能否改指向 常见叫法
const uint8_t *p1 不能 可以 常量指针
uint8_t * const p2 可以 不能 指针常量
const uint8_t * const p3 不能 不能 指向常量的指针常量

简单记忆:const* 前面,限制指向的数据;const* 后面。

volatile 常用于可能被中断、硬件或其他执行上下文改变的数据。它解决的是编译器优化问题,不能代替锁、临界区或原子操作。

四、内存区域、结构体与大小端

区域 常见内容 主要特点
局部变量、函数调用信息 自动分配和释放,空间有限
动态申请的内存 使用灵活,但要防止泄漏和碎片
全局区 全局变量、静态变量 生命周期贯穿程序运行过程
只读区 常量、只读数据 通常不允许修改

结构体大小不能只把各成员占用的字节数直接相加,这个问题我之前踩过坑。例如下面的成员看起来一共只有 5 字节:

c 复制代码
typedef struct {
    uint8_t type;     // 1 字节
    uint32_t value;   // 4 字节
} packet_t;

printf("size: %u\n", (unsigned int)sizeof(packet_t));

但在部分平台上,sizeof(packet_t) 得到的可能是 8,而不是 5。编译器可能会在 type 后面自动填充 3 个字节,使 value 从合适的地址开始存放。

因此,通信数据不能想当然地直接转换成结构体。原始数据可能只有 5 字节,但程序却按照 8 字节的结构体读取,字段位置也可能对不上。处理协议数据时,按字段的位置和长度分别解析会更稳妥。

例如,协议规定一包数据固定为 5 字节:

字节位置 含义
第 0 字节 类型 type
第 1~4 字节 数值 value,小端格式

收到的数据如下:

c 复制代码
uint8_t data[5] = {
    0x01,                   // type
    0x78, 0x56, 0x34, 0x12 // value
};

不建议直接把它强制转换成结构体:

c 复制代码
packet_t *packet = (packet_t *)data; // 不推荐

可以先检查长度,再按照字段位置分别读取:

c 复制代码
uint8_t type;
uint32_t value;

if (sizeof(data) >= 5) {
    type = data[0];

    value =  (uint32_t)data[1]
          | ((uint32_t)data[2] << 8)
          | ((uint32_t)data[3] << 16)
          | ((uint32_t)data[4] << 24);
}

解析后,type 等于 0x01value 等于 0x12345678。这种写法不受结构体填充影响,也明确处理了协议规定的小端顺序。

大小端决定多字节数据在内存中的排列顺序。发送和接收 uint16_tuint32_t 时,要按照协议规定转换,不能直接假设双方顺序相同。

写在最后

这篇主要整理了 TWS 应用开发中经常接触,也比较容易混淆的几块 C 语言知识。

实际开发中,很多问题并不是语法不会写,而是忽略了数据类型、长度、地址、内存对齐和生命周期等细节。把这些基础问题弄清楚,在阅读代码和排查问题时会更有帮助。后续遇到值得记录的内容,我也会继续整理。

相关推荐
隐积3 小时前
3.1.7.Qt容器大家族(3):QVariant——万能盒子
开发语言·qt
名字还没想好☜6 小时前
Next.js ‘use client‘ 到底加在哪:Server/Client Components 边界与常见报错
开发语言·前端·javascript·react·next.js
初级代码游戏7 小时前
iOS开发 Swift 速记1:变量和基本数据类型
开发语言·ios·swift
酷在前行8 小时前
【R生态】PERMANOVA 进阶实战:距离选择、PERMDISP、两两比较与受限置换(保姆级教程)
开发语言·r语言
cxr8288 小时前
Graphify vs GitNexus vs CodeGraph — 三工具架构深度对比
开发语言·架构·知识图谱
2601_955518189 小时前
C语言标准演化史:从K&R到GNU,谁才是正统?
c语言·gnu·标准演化·ansic·性能与可移植性
废弃的小码农9 小时前
功能测试--Day07--Python编程基础
开发语言·python
fengci.9 小时前
Microweber CMS 未授权路径穿越漏洞(CVE-2026-65694)
android·开发语言·前端·学习·php
程序员zgh10 小时前
C++ 拷贝赋值运算符 详解
c语言·开发语言·c++
念何架构之路10 小时前
restartmanager-重启管理子系统
java·开发语言