聚焦 STM32 嵌入式开发场景,整理
<string.h>及<stdio.h>中常用字符串操作函数的分类、原型、使用场景与安全注意事项。适用平台 :STM32 全系列
标准库 :Newlib / Newlib-nano
核心原则 :安全 · 轻量 · 可重入
规范标准:C99 / POSIX
一、长度查询类
获取字符串长度是 STM32 数据处理最频繁的操作,注意空指针与缓冲区边界。
1.1 strlen
c
size_t strlen(const char *s);
返回字符串长度(不含 \0)。
⚠️ STM32 注意 :空指针、无限读取风险。如果
s指向的内存没有\0,会导致越界读取。
示例:
c
char buf[] = "Hello STM32";
size_t len = strlen(buf); // len = 11
1.2 strnlen
c
size_t strnlen(const char *s, size_t maxlen);
限制最大长度的 strlen,最多读取 maxlen 个字符。
✅ STM32 推荐:首选函数,防止无限读取导致 HardFault。
示例:
c
char buf[64];
// 从 UART 接收数据后,安全获取长度
size_t len = strnlen(buf, sizeof(buf) - 1);
二、比较类
字符串比较广泛应用于指令解析(如 AT 命令)、协议帧校验等场景。
2.1 strcmp
c
int strcmp(const char *s1, const char *s2);
逐字符比较(区分大小写),返回 0 表示相等。
⚠️ STM32 注意 :无边界检查,注意输入长度。两个字符串都必须以
\0结尾。
示例:
c
if (strcmp(cmd, "RESET") == 0) {
system_reset();
}
2.2 strncmp
c
int strncmp(const char *s1, const char *s2, size_t n);
仅比较前 n 个字符。
✅ STM32 推荐:首选函数,防溢出。比较 AT 命令前缀时尤其有用。
示例:
c
// 安全比较 AT 命令前缀
if (strncmp(cmd, "AT+", 3) == 0) {
process_at_command(cmd + 3);
}
// 比较响应结果(固定长度)
if (strncmp(rx_buf, "OK\r\n", 4) == 0) {
// 命令执行成功
}
2.3 strcasecmp
c
int strcasecmp(const char *s1, const char *s2);
不区分大小写比较(POSIX 扩展,Newlib 支持)。
⚠️ STM32 注意:部分库支持(Newlib 有),注意重入问题。非标准 C99 函数。
示例:
c
if (strcasecmp(method, "GET") == 0) {
handle_get_request();
}
三、复制 / 填充类
内存操作函数在 STM32 中极其高频,用于数据缓冲、协议帧构造、外设 DMA 传输等。
3.1 strcpy
c
char *strcpy(char *dest, const char *src);
复制字符串(含 \0)。
⛔ 绝对禁止:无边界检查,在 STM32 中极易导致缓冲区溢出,引发 HardFault 或内存越界。
反面教材(不要用):
c
// ❌ 危险!dest 溢出无感知
char dest[8];
strcpy(dest, "Hello World!"); // 溢出!
3.2 strncpy
c
char *strncpy(char *dest, const char *src, size_t n);
复制最多 n 个字符到 dest。
⚠️ 谨慎使用 :不保证
\0终止。如果src长度 >=n,dest将不会以\0结尾。
示例:
c
char buf[32];
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // 必须手动补终止符!
3.3 memcpy
c
void *memcpy(void *dest, const void *src, size_t n);
内存块复制(不关心 \0),效率最高。
✅ STM32 推荐:高频使用,效率最高。适用于已知长度的二进制数据复制。
示例:
c
// 复制已知长度的数据
uint8_t rx_data[16];
memcpy(rx_data, dma_buffer, 16);
// 复制字符串(已知长度时比 strncpy 更安全)
memcpy(buf, src, len);
if (len < sizeof(buf)) buf[len] = '\0';
3.4 memset
c
void *memset(void *s, int c, size_t n);
内存块填充(常用于清空缓冲区)。
✅ STM32 推荐:DMA 清空、结构体初始化必备。
示例:
c
// 清空结构体
SensorData_t sensor;
memset(&sensor, 0, sizeof(sensor));
// 清空 UART 接收缓冲区
memset(rx_buf, 0, sizeof(rx_buf));
四、连接 / 拼接类
字符串拼接在构建协议报文(如 MQTT、HTTP、Modbus)中非常常见。
4.1 strcat
c
char *strcat(char *dest, const char *src);
拼接字符串。
⛔ 绝对禁止 :极易溢出。
dest必须有足够的剩余空间,但无法检查。
反面教材(不要用):
c
// ❌ 危险!
char buf[16] = "Temp:";
strcat(buf, "123.456789"); // 可能溢出
4.2 strncat
c
char *strncat(char *dest, const char *src, size_t n);
最多拼接 n 个字符。
⚠️ 谨慎使用 :需确保
dest有剩余空间,且本身已\0终止。
示例:
c
char buf[32] = "Sensor";
strncat(buf, "_Data", sizeof(buf) - strlen(buf) - 1);
4.3 snprintf
c
int snprintf(char *s, size_t n, const char *fmt, ...);
格式化拼接(强烈推荐)。
✅ STM32 必备 :C99 标准,安全灵活。替代所有
strcat/strcpy/sprintf。
示例:
c
// 构建传感器数据上报报文(JSON)
char msg[64];
int len = snprintf(msg, sizeof(msg),
"{\"temp\":%.1f,\"hum\":%.1f,\"id\":\"%s\"}",
temperature, humidity, device_id
);
// 构建 MQTT Topic
char topic[48];
snprintf(topic, sizeof(topic), "sensor/%s/data", device_id);
// 构建 AT 命令
char at_cmd[32];
snprintf(at_cmd, sizeof(at_cmd), "AT+SEND=%d,%s\r\n", data_len, payload);
五、查找 / 搜索类
解析协议帧、提取关键字段时经常使用查找函数。
5.1 strchr
c
char *strchr(const char *s, int c);
查找字符首次出现。
示例:
c
char *colon = strchr(http_header, ':');
if (colon) {
*colon = '\0'; // 分隔键和值
char *value = colon + 1;
}
5.2 strrchr
c
char *strrchr(const char *s, int c);
查找字符最后一次出现。
示例:
c
// 取文件扩展名(最后一次出现的 '.')
char *ext = strrchr(filename, '.');
if (ext && strcmp(ext, ".bin") == 0) {
// 扩展名为 .bin,执行固件升级
}
5.3 strstr
c
char *strstr(const char *haystack, const char *needle);
查找子串。
示例:
c
// 定位帧结束符 \r\n
char *p = strstr(rx_buf, "\r\n");
if (p) {
size_t frame_len = p - rx_buf;
// 处理完整帧...
}
// 检查响应中是否包含 "ERROR"
if (strstr(response, "ERROR") != NULL) {
handle_error();
}
5.4 strpbrk
c
char *strpbrk(const char *s, const char *accept);
查找第一个出现在 accept 集合中的字符。
示例:
c
// 查找第一个逗号或分号(分隔符)
char *delim = strpbrk(line, ",;");
if (delim) *delim = '\0'; // 截断字符串
5.5 memchr
c
void *memchr(const void *s, int c, size_t n);
内存中查找字符(有限长度)。
示例:
c
// 在二进制数据中查找分隔符
uint8_t *p = memchr(rx_buffer, 0xAA, rx_len);
if (p) {
size_t offset = p - rx_buffer;
}
// 安全获取字符串长度(二进制+文本混合数据)
size_t blen = (char*)memchr(buf, '\0', sizeof(buf)) - buf;
六、转换类
数值与字符串之间的转换是嵌入式开发中最容易出错的环节之一。
6.1 atoi
c
int atoi(const char *nptr);
字符串转 int。
⛔ 不推荐:无错误处理。非法输入时行为未定义,返回 0 无法区分"输入为0"和"转换失败"。
反面教材(不要用):
c
// ❌ 无法判断 "abc" 和 "0"
int val = atoi("abc"); // val = 0,但不知道是错误
6.2 atol
c
long atol(const char *nptr);
字符串转 long。
⛔ 不推荐 :同
atoi,无错误处理。
6.3 strtol
c
long strtol(const char *nptr, char **endptr, int base);
字符串转 long,支持进制转换 + 错误检测(强烈推荐)。
✅ STM32 推荐 :通过
endptr精确判断转换位置,检测非法字符,支持 2/8/10/16 进制。
示例:
c
// 带错误检查的字符串转整数
char *end;
long val = strtol(str, &end, 10);
if (*end != '\0') {
// 转换不完整,str 中包含非数字字符
printf("Invalid character: %c\n", *end);
}
// 16 进制字符串转整数(如解析地址)
uint32_t addr = strtol("0x08000000", NULL, 16); // addr = 0x08000000
// 2 进制字符串转整数
int flags = strtol("1010", NULL, 2); // flags = 10
6.4 sprintf
c
int sprintf(char *s, const char *fmt, ...);
格式化输出到字符串。
⛔ 危险:无边界检查,缓冲区溢出风险。嵌入式中禁止使用。
反面教材(不要用):
c
// ❌ 危险!
char buf[8];
int x = 12345;
sprintf(buf, "%d", x); // 写入 "12345"(5字符),但如果 x 很大就溢出
6.5 snprintf
c
int snprintf(char *s, size_t n, const char *fmt, ...);
安全版 sprintf(必备)。
✅ STM32 必备:始终限制写入长度,返回值可判断是否截断。
示例:
c
// 浮点数格式化(STM32 传感器常用)
char volt_str[16];
snprintf(volt_str, sizeof(volt_str), "%.2fV", 3.14159); // "3.14V"
// 带截断检测的格式化
char buf[10];
int n = snprintf(buf, sizeof(buf), "Temperature: %d", 250);
if (n >= sizeof(buf)) {
// 输出被截断了
}
七、实用工具类
这些函数在日常嵌入式开发中虽然不是最常用,但特定场景下非常高效。
7.1 strtok
c
char *strtok(char *str, const char *delim);
按分隔符切分字符串(非线程安全)。
⚠️ STM32 注意 :会修改原字符串(将分隔符替换为
\0),且非线程安全。FreeRTOS 下请使用strtok_r。
示例:
c
// 解析 CSV 数据
char line[] = "12.5,68.3,1013.2";
char *token = strtok(line, ",");
while (token) {
float val = atof(token);
process(val);
token = strtok(NULL, ",");
}
7.2 strtok_r
c
char *strtok_r(char *str, const char *delim, char **saveptr);
线程安全的 strtok(GNU 扩展 / POSIX)。
✅ STM32 推荐 :FreeRTOS 环境下使用。通过
saveptr保存状态,可重入。
示例:
c
// FreeRTOS 下安全解析
char line[] = "cmd,arg1,arg2";
char *saveptr;
char *cmd = strtok_r(line, ",", &saveptr);
char *arg1 = strtok_r(NULL, ",", &saveptr);
char *arg2 = strtok_r(NULL, ",", &saveptr);
7.3 strspn
c
size_t strspn(const char *s, const char *accept);
返回 s 开头 accept 字符集的连续长度。
示例:
c
// 跳过前导空白字符
const char *s = " \t\nHello";
size_t offset = strspn(s, " \t\n");
const char *content = s + offset; // 指向 "Hello"
// 验证字符串是否全为数字
if (strspn(input, "0123456789") == strlen(input)) {
// 是纯数字字符串
}
7.4 strcspn
c
size_t strcspn(const char *s, const char *reject);
返回 s 开头直到出现 reject 字符集的长度。
示例:
c
// 查找第一个非数字字符的位置
const char *num_str = "12345abc";
size_t num_len = strcspn(num_str, "abcdefghijklmnopqrstuvwxyz");
// num_len = 5
// 获取一行文本(直到换行符)
size_t line_len = strcspn(buffer, "\r\n");
八、陷阱与实战技巧
STM32 资源受限,字符串处理不当极易引发难以调试的 Crash。
| 类别 | 问题 | 解决方案 |
|---|---|---|
| ⛔ 危险 | strcpy / strcat / sprintf 无边界检查 |
全部替换为 strncpy / snprintf |
| ⚠️ 注意 | strncpy 不保证 \0 终止 |
手动 buf[sizeof(buf)-1] = '\0' |
| ⛔ 危险 | strtok 修改原字符串且非线程安全 |
FreeRTOS 用 strtok_r,或自实现 |
| ⚠️ 注意 | Flash 字符串字面量不可修改 | 不要对字符串字面量调用 strtok / strcpy(目标不能是字面量) |
| 🔵 性能 | Newlib 标准库体积大 | 使用 Newlib-nano 或链接 --specs=nano.specs |
安全宏定义模板
c
/* stm32_string_utils.h */
// 安全的字符串拷贝宏(dst 必须是数组,不能是指针)
#define SAFE_STRCPY(dst, src) do { \
strncpy((dst), (src), sizeof(dst) - 1); \
(dst)[sizeof(dst) - 1] = '\0'; \
} while (0)
// 安全的字符串格式化宏(dst 必须是数组,不能是指针)
#define SAFE_SPRINTF(dst, fmt, ...) \
snprintf((dst), sizeof(dst), fmt, ##__VA_ARGS__)
// 线程安全的 strtok 封装(FreeRTOS)
static inline char *safe_strtok(char *str, const char *delim, char **saveptr) {
return strtok_r(str, delim, saveptr);
}
九、速查表
推荐函数(日常使用首选)
| 函数 | 用途 | 替代 |
|---|---|---|
strnlen |
安全获取长度 | strlen |
strncmp |
安全比较 | strcmp |
memcpy |
内存块复制 | strcpy |
memset |
清空/初始化 | 手动循环 |
snprintf |
安全格式化 | sprintf / strcat |
strstr |
子串搜索 | 手动查找 |
strchr |
字符查找 | 手动查找 |
strtok_r |
线程安全切分 | strtok |
strtol |
带错误检查的转换 | atoi / atol |
strpbrk |
多分隔符查找 | 多次 strchr |
memchr |
二进制数据查找 | 手动查找 |
memmove |
重叠内存复制 | memcpy(有重叠时) |
绝对禁止
strcpy · strcat · sprintf
谨慎使用
strncpy(需手动补 \0) · strtok(非 RTOS) · atoi(无错误处理)
总结 :STM32 下字符串处理的核心原则是 ------ 优先
strn*族、摒弃str*无边界函数、强制用snprintf、配合 RTOS 时注意重入性。