C语言字符串操作完全指南:从strlen到memcpy,避开所有坑点
前言
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系列函数核心区别
- str系列(strcpy/strcat/strcmp):专属字符串操作,识别
\0,遇到结束符立即停止; - mem系列(memcpy/memset):通用内存操作,不识别结束符,严格按照指定字节数操作,适配所有数据类型。
十二、竞赛&项目高频字符串bug汇总
- 丢失\0结束符:strncpy未手动补位、串口接收未追加结束符,引发乱码、死循环;
- strtok传入常量字符串:只读内存被修改,直接程序崩溃;
- 滥用strcpy:无长度限制,缓冲区溢出篡改全局变量;
- memcpy长度计算错误:结构体、数组拷贝数据错乱。
十三、蓝桥杯嵌入式竞赛编码规范
- 串口缓冲区接收数据后,手动补充
\0; - 字符串拷贝优先使用strncpy/memcpy,杜绝strcpy;
- 指令匹配优先使用strncmp,规避多余字符干扰;
- 所有缓冲区初始化统一使用memset清零。
十四、综合实战案例:串口指令解析
本节将结合蓝桥杯嵌入式竞赛实际场景,演示一个完整的串口指令解析流程。假设串口接收到的指令格式为"SET:LED1,ON",我们需要:
- 使用
strstr定位指令头(如"SET:") - 使用
strtok或手动解析分割参数(如"LED1"和"ON") - 使用
strcmp/strncmp进行指令匹配 - 包含缓冲区安全处理和错误判断的完整流程
完整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->param2, token, MAX_PARAM_LEN - 1);
result->param2[MAX_PARAM_LEN - 1] = '\0';
result->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->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, &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;result);
} else {
printf("解析失败:指令格式错误\n");
}
return 0;
}
代码解析要点:
- 缓冲区安全处理:所有字符串操作都使用
strncpy并手动补\0,避免缓冲区溢出。 - 指令头定位:使用
strstr查找"SET:"或"GET:"指令头,支持多种指令类型。 - 参数分割:使用
strtok按逗号分割参数,注意strtok会修改原字符串,因此使用工作缓冲区副本。 - 指令匹配:使用
strcmp进行精确匹配,使用strncmp进行前缀匹配,提高容错性。 - 内存初始化:使用
memset清零缓冲区和结构体,避免未初始化内存导致的随机值。 - 错误处理:包含指针非空检查、缓冲区长度检查、指令格式验证等完整错误处理。
- 实际应用扩展:代码结构清晰,易于扩展支持更多指令类型和参数格式。
竞赛实战建议:
- 在真实嵌入式项目中,串口接收通常使用中断+DMA方式,接收完成后需要手动添加
\0结束符。 - 对于多任务系统,避免使用
strtok(不可重入),可改用strchr+strncpy手动分割。 - 指令匹配时优先使用
strncmp,避免因缓冲区残留数据导致匹配失败。 - 所有字符串操作前都要检查缓冲区边界,这是避免内存错误的关键。
文末小结
字符串操作的底层本质是字符指针的内存操作。建议大家手动封装实现strlen、strcmp、memcpy函数,用指针复刻底层逻辑。手写一遍后,能彻底打通指针与内存的核心逻辑,不管是学生管理系统课程设计,还是蓝桥杯、西门子杯嵌入式竞赛,都能有效规避90%以上的隐性内存bug。
更多推荐

所有评论(0)