前言

C 语言不存在原生字符串类型,日常开发中,我们依靠字符数组 char buf[] 或字符指针 char *str 存储字符串,并且以 \0 作为字符串唯一结束标记。无论是学生管理系统的信息录入、数据分割,还是蓝桥杯嵌入式的串口指令解析、LCD 文本打印,字符串操作都是高频用法。string.h 头文件提供了全套字符串操作函数,但多数新手难以区分 str 系列与 mem 系列函数的区别,且不了解函数潜在问题,极易引发 bug。

所有函数均需引入核心头文件: #include <string.h>

一、strlen —— 计算字符串有效长度

函数原型: size_t strlen(const char *s);

功能: 统计字符串有效字符数量,遇到 \0 立即停止计数。

核心重点: 统计长度不包含结束符,与 sizeof 严格区分!

对比维度 strlen sizeof
功能 计算字符串有效字符长度(不包含 '\0' 计算变量/类型/数组在内存中占用的总字节数
参数 字符串指针(const char* 变量名、类型名或数组名
返回值 size_t(无符号整数) size_t(无符号整数)
计算时机 运行时遍历字符串直到遇到 '\0' 编译时由编译器静态计算
是否包含 '\0' 不包含(遇到 '\0' 停止计数) 包含(计算整个内存占用)
适用场景 获取字符串实际内容长度 获取变量/数组/结构体的内存大小
典型示例 char str[] = "hello";
strlen(str) → 5
char str[] = "hello";
sizeof(str) → 6(包含 '\0'

总结: strlen运行时函数,用于获取字符串有效长度;sizeof编译时运算符,用于获取数据类型或变量的内存占用大小。两者本质不同,切勿混淆。

补充坑点: 返回值为无符号 size_t 类型,禁止与负数直接比较,会出现逻辑错误。

实战代码:

#include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "hello";
printf("strlen 有效长度:%zu\n", strlen(str));  // 输出 5
printf("sizeof 总占用字节:%zu\n", sizeof(str)); // 输出 6,包含 '\0'
return 0;
}

二、strcpy —— 字符串整体拷贝

函数原型: char *strcpy(char *dest, const char *src);

功能: 将 src 字符串完整拷贝到 dest 空间,自动拷贝至 \0 结束符。

致命缺陷: 无长度检测机制,源字符串过长会造成缓冲区溢出,存在安全隐患。

项目故障: 学生管理系统超长姓名录入会篡改相邻变量;蓝桥杯串口缓存溢出,导致全局参数错乱。

实战代码:

char dest[20];
char src[] = "test string";
strcpy(dest, src);

三、strncpy —— 限定长度字符串拷贝

函数原型: char *strncpy(char *dest, const char *src, size_t n);

功能: 限制最多拷贝 n 个字节数据,规避溢出风险。

重要注意事项: 若前 n 个字符无 \0,函数不会自动补结束符,会生成非法字符串,必须手动补位!

实战代码:

char dest[10];
char src[] = "1234567890abc";
strncpy(dest, src, 9);
dest[9] = '\0';  // 手动补充结束符,杜绝乱码

四、strcat —— 字符串拼接

函数原型: char *strcat(char *dest, const char *src);

功能: 将 src 字符串拼接在 dest 字符串尾部。

使用前提: dest 必须是带 \0 的合法字符串,且剩余内存空间充足。

缺点:无长度限制,工程开发、竞赛不推荐优先使用。

char buf[32] = "hello ";
strcat(buf, "world");

五、strcmp / strncmp —— 字符串比较

strcmp原型:int strcmp(const char *s1, const char *s2);

返回规则:s1>s2返回正数;s1==s2返回0;s1<s2返回负数。

功能:逐字符按ASCII码对比字符串,区分大小写。

strncmp原型:int strncmp(const char *s1, const char *s2, size_t n);

功能:仅对比前n个字符,适配串口指令模糊匹配场景。

蓝桥杯串口指令解析标准写法:

if(strcmp(buf,"cmd_on") == 0) {
    // 匹配指定指令,执行对应业务逻辑
}

六、strchr / strrchr —— 字符查找

原型:

  • char *strchr(const char *s, int c); // 查找第一个匹配字符
  • char *strrchr(const char *s, int c); // 查找最后一个匹配字符

返回值:匹配成功返回对应地址,失败返回NULL。

char str[] = "abc123abc";
char *p = strchr(str,'b');
printf("%s\n",p); // 输出bc123abc

七、strstr —— 子串查找

函数原型:char *strstr(const char *s1, const char *s2);

功能:在长字符串s1中查找子串s2,返回子串起始地址。

场景:嵌入式串口协议解析、关键词匹配核心函数。

char str[] = "data:12345";
char *p = strstr(str,"data:");

八、strtok —— 字符串分割(高频难点)

函数原型:char *strtok(char *s, const char *accept);

功能:按照指定分隔符切割字符串。

核心特性:会修改原字符串,将分隔符替换为\0,禁止传入字符串常量!

隐藏坑点:内部依赖静态变量保存分割位置,多线程/多任务嵌入式开发不可重入,尽量规避。

项目场景:学生管理系统分割学号姓名、嵌入式分割多路串口数据。

char buf[] = "111,222,333";
char *p = strtok(buf, ",");
while(p != NULL) {
    printf("%s\n", p);
    p = strtok(NULL, ",");
}

九、memcpy —— 通用内存拷贝(项目首选)

函数原型:void *memcpy(void *dest, const void *src, size_t n);

功能:拷贝n字节内存数据,\0无视,支持字符串、数组、结构体任意数据类型。

核心优势:无溢出风险,替代strcpy,是竞赛和工程开发首选拷贝函数。

补充:若内存区域重叠,改用memmove函数。

十、memset —— 内存初始化清零

函数原型:void *memset(void *ptr, int value, size_t num);

功能:从ptr起始,将连续num字节内存统一赋值。

常用场景:缓冲区清零、结构体初始化。

避坑:按字节赋值,不可直接对int数组赋非0值。

char buf[64];
memset(buf, 0 , sizeof(buf)); // 缓冲区标准清零写法

十一、str系列与mem系列函数核心区别

  1. str系列(strcpy/strcat/strcmp):专属字符串操作,识别\0,遇到结束符立即停止;
  2. mem系列(memcpy/memset):通用内存操作,不识别结束符,严格按照指定字节数操作,适配所有数据类型。

十二、竞赛&项目高频字符串bug汇总

  1. 丢失\0结束符:strncpy未手动补位、串口接收未追加结束符,引发乱码、死循环;
  2. strtok传入常量字符串:只读内存被修改,直接程序崩溃;
  3. 滥用strcpy:无长度限制,缓冲区溢出篡改全局变量;
  4. memcpy长度计算错误:结构体、数组拷贝数据错乱。

十三、蓝桥杯嵌入式竞赛编码规范

  1. 串口缓冲区接收数据后,手动补充\0
  2. 字符串拷贝优先使用strncpy/memcpy,杜绝strcpy;
  3. 指令匹配优先使用strncmp,规避多余字符干扰;
  4. 所有缓冲区初始化统一使用memset清零。

十四、综合实战案例:串口指令解析

本节将结合蓝桥杯嵌入式竞赛实际场景,演示一个完整的串口指令解析流程。假设串口接收到的指令格式为"SET:LED1,ON",我们需要:

  1. 使用strstr定位指令头(如"SET:")
  2. 使用strtok或手动解析分割参数(如"LED1"和"ON")
  3. 使用strcmp/strncmp进行指令匹配
  4. 包含缓冲区安全处理和错误判断的完整流程

完整C语言代码示例:

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
// 指令缓冲区大小定义
#define UART_BUFFER_SIZE 64
#define MAX_PARAM_LEN 16
// 指令类型枚举
typedef enum {
CMD_SET,
CMD_GET,
CMD_UNKNOWN
} CommandType;
// 解析结果结构体
typedef struct {
CommandType type;          // 指令类型
char param1[MAX_PARAM_LEN]; // 参数1
char param2[MAX_PARAM_LEN]; // 参数2
bool valid;                // 解析是否成功
} ParseResult;
/**
@brief 解析串口指令
@param buffer 接收到的指令字符串
@param result 解析结果结构体指针
@return 解析成功返回true,失败返回false
/
bool parseUartCommand(const char buffer, ParseResult* result) {
// 1. 安全检查:指针非空且缓冲区非空
if (buffer == NULL || result == NULL || strlen(buffer) == 0) {
return false;
}
// 2. 初始化结果结构体
memset(result, 0, sizeof(ParseResult));
result->valid = false;
// 3. 使用strstr定位指令头
char* cmd_start = strstr(buffer, "SET:");
if (cmd_start != NULL) {
result->type = CMD_SET;
} else {
cmd_start = strstr(buffer, "GET:");
if (cmd_start != NULL) {
result->type = CMD_GET;
} else {
result->type = CMD_UNKNOWN;
return false; // 未知指令类型
}
}
// 4. 跳过指令头(如"SET:"或"GET:")
char* params_start = cmd_start + 4; // "SET:"或"GET:"长度为4
// 5. 使用strtok分割参数(注意:strtok会修改原字符串,这里使用副本)
char work_buffer[UART_BUFFER_SIZE];
// 安全拷贝:使用strncpy并手动补\0
strncpy(work_buffer, params_start, sizeof(work_buffer) - 1);
work_buffer[sizeof(work_buffer) - 1] = '\0';
// 6. 分割参数(以逗号为分隔符)
char* token = strtok(work_buffer, ",");
if (token != NULL) {
// 安全拷贝第一个参数
strncpy(result->param1, token, MAX_PARAM_LEN - 1);
result->param1[MAX_PARAM_LEN - 1] = '\0';
 // 获取第二个参数
 token = strtok(NULL, ",");
 if (token != NULL) {
     strncpy(result-&gt;param2, token, MAX_PARAM_LEN - 1);
     result-&gt;param2[MAX_PARAM_LEN - 1] = '\0';
     result-&gt;valid = true;
 }
}
return result->valid;
}
/**
@brief 执行指令对应的操作
@param result 解析后的指令结果
/
void executeCommand(const ParseResult result) {
if (!result->valid) {
printf("错误:无效指令\n");
return;
}
switch (result->type) {
case CMD_SET:
// 使用strcmp进行精确匹配
if (strcmp(result->param1, "LED1") == 0) {
if (strcmp(result->param2, "ON") == 0) {
printf("执行:打开LED1\n");
// 实际硬件操作代码
} else if (strcmp(result->param2, "OFF") == 0) {
printf("执行:关闭LED1\n");
// 实际硬件操作代码
} else {
printf("错误:LED1的无效状态参数 '%s'\n", result->param2);
}
} else if (strcmp(result->param1, "LED2") == 0) {
// 使用strncmp进行前缀匹配(更安全)
if (strncmp(result->param2, "ON", 2) == 0) {
printf("执行:打开LED2\n");
} else if (strncmp(result->param2, "OFF", 3) == 0) {
printf("执行:关闭LED2\n");
} else {
printf("错误:LED2的无效状态参数\n");
}
} else {
printf("错误:未知设备 '%s'\n", result->param1);
}
break;
 case CMD_GET:
     printf("查询指令:%s\n", result-&gt;param1);
     // 实际查询操作代码
     break;
default:
printf("错误:未知指令类型\n");
break;
}
}
/**
@brief 模拟串口接收数据(实际项目中从硬件串口读取)
@param buffer 接收缓冲区
@param size 缓冲区大小
@return 接收到的数据长度
/
int simulateUartReceive(char buffer, int size) {
// 模拟接收到的指令(实际项目中从串口硬件读取)
const char* received_cmd = "SET:LED1,ON";
// 安全拷贝:使用memcpy确保不会溢出
int len = strlen(received_cmd);
if (len >= size) {
len = size - 1; // 留出\0的位置
}
memcpy(buffer, received_cmd, len);
buffer[len] = '\0'; // 手动添加结束符
return len;
}
int main(void) {
// 1. 初始化接收缓冲区(使用memset清零)
char uart_buffer[UART_BUFFER_SIZE];
memset(uart_buffer, 0, sizeof(uart_buffer));
// 2. 模拟串口接收数据
int received_len = simulateUartReceive(uart_buffer, sizeof(uart_buffer));
printf("接收到指令:%s(长度:%d)\n", uart_buffer, received_len);
// 3. 解析指令
ParseResult result;
if (parseUartCommand(uart_buffer, &amp;result)) {
printf("解析成功:\n");
printf("  指令类型:%s\n", result.type == CMD_SET ? "SET" : "GET");
printf("  参数1:%s\n", result.param1);
printf("  参数2:%s\n", result.param2);
// 4. 执行指令
executeCommand(&amp;amp;result);
} else {
printf("解析失败:指令格式错误\n");
}
return 0;
}

代码解析要点:

  1. 缓冲区安全处理:所有字符串操作都使用strncpy并手动补\0,避免缓冲区溢出。
  2. 指令头定位:使用strstr查找"SET:"或"GET:"指令头,支持多种指令类型。
  3. 参数分割:使用strtok按逗号分割参数,注意strtok会修改原字符串,因此使用工作缓冲区副本。
  4. 指令匹配:使用strcmp进行精确匹配,使用strncmp进行前缀匹配,提高容错性。
  5. 内存初始化:使用memset清零缓冲区和结构体,避免未初始化内存导致的随机值。
  6. 错误处理:包含指针非空检查、缓冲区长度检查、指令格式验证等完整错误处理。
  7. 实际应用扩展:代码结构清晰,易于扩展支持更多指令类型和参数格式。

竞赛实战建议:

  • 在真实嵌入式项目中,串口接收通常使用中断+DMA方式,接收完成后需要手动添加\0结束符。
  • 对于多任务系统,避免使用strtok(不可重入),可改用strchr+strncpy手动分割。
  • 指令匹配时优先使用strncmp,避免因缓冲区残留数据导致匹配失败。
  • 所有字符串操作前都要检查缓冲区边界,这是避免内存错误的关键。

文末小结

字符串操作的底层本质是字符指针的内存操作。建议大家手动封装实现strlen、strcmp、memcpy函数,用指针复刻底层逻辑。手写一遍后,能彻底打通指针与内存的核心逻辑,不管是学生管理系统课程设计,还是蓝桥杯、西门子杯嵌入式竞赛,都能有效规避90%以上的隐性内存bug。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐