【嵌入式 C 语言实战】手动实现字符串四大核心函数(strcpy/strcat/strlen/strcmp)
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,字符串处理是高频核心需求,而 C 语言标准库中的strcpy、strcat、strlen、strcmp是字符串处理的四大基石。今天我们不依赖标准库,手动实现这四个函数,深入理解其底层逻辑,同时修正代码中的小问题,掌握嵌入式场景下字符串处理的核心技巧。
一、代码整体概述
这份代码的核心目标是模拟 C 语言标准库的字符串四大核心函数 ,不依赖<string.h>头文件,完全通过指针操作实现字符串的复制、拼接、长度统计和比较功能,是理解 C 语言字符串本质(以'\0'结尾的字符数组)的绝佳案例。
先明确四个函数的核心功能:
Strcpy:字符串复制,将源字符串复制到目标字符数组中;Strlen:字符串长度统计,返回字符串有效字符个数(不包含'\0');Strcat:字符串拼接,将源字符串追加到目标字符串末尾;Strmpy(对应标准库strcmp):字符串比较,按 ASCII 码值比较两个字符串的大小。
二、先修正代码中的关键问题(保证正常运行)
在解析代码之前,先修正两处影响运行的小问题,避免编译警告和运行错误:
1. 函数名拼写修正:Strmpy → 统一为Strcmp(贴合标准库,提升可读性)
原代码中函数声明和实现为Strmpy,属于拼写笔误,统一修正为Strcmp(与标准库strcmp对应),避免混淆。
2. 修正main函数中只读字符串指针赋值问题
c
运行
arduino
// 原代码问题:d、e是指向只读字符串常量的指针,不能直接用于修改(虽此处仅用于比较,无修改,但会有编译警告)
// 修正为:使用字符数组存储,符合嵌入式字符串处理规范
char d[]="HELLO3";
char e[]="HELLO4";
问题说明 :char *d="HELLO3"中,"HELLO3"是存储在只读存储区的字符串常量,指针d仅指向其首地址,无法修改该字符串;而字符数组char d[]="HELLO3"是将字符串常量复制到栈上的字符数组中,可读取可修改(此处用于比较,虽无需修改,但更符合规范,避免编译警告)。
3. 修正函数声明与实现的一致性
原代码中Strmpy声明参数为char *d,char *s,修正后Strcmp保持参数一致性,同时建议添加const修饰只读参数(源字符串不被修改),提升代码健壮性。
三、逐函数详细解析(核心实现与底层逻辑)
1. 字符串复制:Strcpy(对应标准库strcpy)
核心功能
将源字符串(const char *s)的内容完整复制到目标字符数组(char *d)中,包括末尾的'\0',返回目标字符数组的首地址。
代码实现(修正后)
c
运行
arduino
char *Strcpy(char *d,const char *s)
{
// 保存目标数组首地址(后续d指针会移动,需返回原始首地址)
char *p=d;
// 遍历源字符串,直到遇到'\0'(*s为'\0'时,循环条件为假,终止循环)
while(*s)
{
// 逐字符复制:将源字符串当前字符赋值给目标数组
*d=*s;
// 指针同时后移,遍历下一个字符
d++;
s++;
}
// 关键:在目标字符串末尾添加'\0',标识字符串结束
*d='\0';
// 返回目标数组原始首地址,支持链式调用
return p;
}
关键细节解析
const char *s:使用const修饰源字符串指针,表明源字符串仅用于读取,不被修改,是 C 语言良好编码习惯,防止函数内部误修改源字符串;- 保存首地址
char *p=d:由于后续d指针会不断后移指向目标数组的后续位置,若不保存原始首地址,无法返回目标数组的起始位置,影响链式调用(如b=Strcpy(a,b)); - 循环条件
while(*s):等价于while(*s != '\0'),利用 C 语言 "0 为假,非 0 为真" 的特性,简洁高效; - 手动添加
*d='\0':这是字符串复制的核心要点,源字符串的'\0'不会被带入循环(循环终止于*s='\0'),必须手动在目标数组末尾添加'\0',否则目标数组无法被识别为合法字符串。
嵌入式注意点
- 目标字符数组
d必须有足够的容量,能够容纳源字符串的所有字符(包括'\0'),否则会导致数组越界,覆盖其他内存区域,引发系统崩溃; - 避免目标指针
d为NULL,嵌入式开发中需添加判空逻辑(如if(d==NULL || s==NULL) return NULL;),提升代码健壮性。
2. 字符串长度统计:Strlen(对应标准库strlen)
核心功能
统计字符串的有效字符个数(不包含末尾的'\0'),返回整数长度。
代码实现
c
运行
arduino
int Strlen(char *d)
{
// 初始化长度计数器为0
int len=0;
// 遍历字符串,直到遇到'\0'
while(*d)
{
// 每遍历一个有效字符,计数器+1
len++;
// 指针后移,遍历下一个字符
d++;
}
// 返回有效字符长度
return len;
}
关键细节解析
- 计数规则:仅统计
'\0'之前的有效字符,例如"HELLO1"的长度为 6,不包含末尾隐藏的'\0'; - 指针遍历:无需知道字符数组的总容量,仅通过指针遍历到
'\0'即可终止,符合 C 语言字符串的特性; - 时间复杂度:O (n)(n 为字符串有效长度),仅需遍历一次字符串,效率高效。
嵌入式注意点
- 若传入的字符串未以
'\0'结尾,会导致while循环无限执行,引发栈溢出或程序崩溃,嵌入式开发中必须确保字符串以'\0'结尾; - 避免传入
NULL指针,可添加判空逻辑(如if(d==NULL) return 0;)。
3. 字符串拼接:Strcat(对应标准库strcat)
核心功能
将源字符串(const char *s)追加到目标字符串(char *d)的末尾,覆盖目标字符串原来的'\0',并在拼接后的新字符串末尾添加'\0',返回目标字符串的首地址。
代码实现
c
运行
arduino
char *Strcat(char *d,const char *s)
{
// 保存目标数组首地址
char *p=d;
// 第一步:遍历目标字符串,找到其末尾的'\0'
while(*d)
{
d++;
}
// 第二步:与Strcpy逻辑一致,将源字符串逐字符复制到目标字符串末尾
while(*s)
{
*d=*s;
d++;
s++;
}
// 关键:在拼接后的新字符串末尾添加'\0'
*d='\0';
// 返回目标数组原始首地址
return p;
}
关键细节解析
- 两步遍历逻辑:第一步找到目标字符串的末尾(
'\0'位置),第二步从该位置开始复制源字符串,实现 "追加" 效果,而非覆盖; - 与
Strcpy的区别:Strcpy从目标数组的起始位置开始复制,Strcat从目标字符串的'\0'位置开始复制; - 手动添加
*d='\0':确保拼接后的字符串是合法的 C 语言字符串,否则后续遍历(如Strlen、printf)会出现异常。
嵌入式注意点
- 目标字符数组
d的剩余容量必须足够容纳源字符串的所有字符(包括'\0'),例如目标数组a[1024]已存储"HELLO1"(长度 6),则剩余容量需大于源字符串"HELLO2"的长度(6),否则会导致数组越界; - 不支持自拼接(如
Strcat(a,a)),会导致源字符串被覆盖,引发逻辑错误。
4. 字符串比较:Strcmp(对应标准库strcmp,原代码Strmpy)
核心功能
按 ASCII 码值逐字符比较两个字符串的大小,返回比较结果:
- 返回值 > 0:第一个字符串大于第二个字符串;
- 返回值 = 0:两个字符串完全相等;
- 返回值 < 0:第一个字符串小于第二个字符串。
代码实现(修正后)
c
运行
arduino
int Strcmp(char *d,char *s)
{
// 第一步:逐字符比较,直到其中一个字符串遇到'\0',或出现不相等字符
while(*d&&*s)
{
// 若当前字符不相等,直接返回两个字符的ASCII码差值
if(*d!=*s)
{
return *d-*s;
}
// 字符相等,指针同时后移,继续比较下一个字符
d++;
s++;
}
// 第二步:若其中一个字符串已遍历完毕,返回剩余字符的ASCII码差值
// (若两个字符串完全相等,此处* d和*s均为'\0',差值为0)
return *d-*s;
}
关键细节解析
- 比较规则:按字符的 ASCII 码值比较,例如
'A'(65)<'a'(97),'3'(51)<'H'(72); - 循环条件
while(*d&&*s):仅当两个字符串当前字符都不为'\0'时,才进行比较,任意一个字符串遍历完毕则终止循环; - 返回值逻辑:直接返回字符的 ASCII 码差值,符合标准库
strcmp的返回值规范,无需额外判断正负; - 完全相等的情况:当两个字符串长度相同且所有字符都相等时,循环结束后
*d和*s均为'\0'(ASCII 码值为 0),返回值为 0。
嵌入式注意点
- 支持长度不同的字符串比较,例如
"HELLO3"和"HELLO345",会先比较到"HELLO3"的'\0',返回0 - '4'(即 - 52); - 无需修改字符串内容,建议添加
const修饰参数(int Strcmp(const char *d, const char *s)),提升代码安全性。
四、完善后的完整代码(可直接编译运行)
c
运行
arduino
#include<stdio.h>
// 函数声明(修正命名,保持一致性)
char *Strcpy(char *d,const char *s);
char *Strcat(char *d,const char *s);
int Strcmp(const char *d,const char *s); // 添加const修饰,提升健壮性
int Strlen(char *d);
int main()
{
char a[1024] = {0}; // 目标字符数组,初始化全0,确保有足够容量
const char *b="HELLO1";
// 字符串复制
Strcpy(a,b);
printf("字符串复制结果:%s\n",a);
// 字符串长度统计
int len=Strlen(a);
printf("字符串长度:%d\n",len);
// 字符串拼接
const char *c="HELLO2";
Strcat(a,c);
printf("字符串拼接结果:%s\n",a);
// 字符串比较
char d[]="HELLO3"; // 修正为字符数组,避免只读警告
char e[]="HELLO4";
int s=Strcmp(d,e);
printf("字符串比较结果:%d\n",s);
return 0;
}
// 字符串比较实现(修正命名,添加const修饰)
int Strcmp(const char *d,const char *s)
{
while(*d&&*s)
{
if(*d!=*s)
{
return *d-*s;
}
d++;
s++;
}
return *d-*s;
}
// 字符串复制实现
char *Strcpy(char *d,const char *s)
{
// 判空逻辑(嵌入式开发推荐添加,避免NULL指针)
if(d==NULL || s==NULL)
{
printf("指针为空,复制失败\n");
return NULL;
}
char *p=d;
while(*s)
{
*d=*s;
d++;
s++;
}
*d='\0';
return p;
}
// 字符串拼接实现
char *Strcat(char *d,const char *s)
{
// 判空逻辑(嵌入式开发推荐添加,避免NULL指针)
if(d==NULL || s==NULL)
{
printf("指针为空,拼接失败\n");
return NULL;
}
char *p=d;
while(*d)
{
d++;
}
while(*s)
{
*d=*s;
d++;
s++;
}
*d='\0';
return p;
}
// 字符串长度统计实现
int Strlen(char *d)
{
// 判空逻辑(嵌入式开发推荐添加,避免NULL指针)
if(d==NULL)
{
printf("指针为空,长度统计失败\n");
return 0;
}
int len=0;
while(*d)
{
len++;
d++;
}
return len;
}
五、编译运行与预期结果
1. 编译命令(Linux/macOS)
将代码保存为string_functions.c,在终端执行以下命令:
bash
运行
bash
gcc string_functions.c -o string_functions
./string_functions
2. 预期运行结果
plaintext
字符串复制结果:HELLO1
字符串长度:6
字符串拼接结果:HELLO1HELLO2
字符串比较结果:-1
结果解析:
HELLO3与HELLO4比较,最后一个字符'3'(51)比'4'(52)小 1,返回 - 1,符合预期。
六、嵌入式开发优化建议
- 添加严格的指针判空逻辑 :嵌入式设备中内存资源有限,指针容易出现
NULL,必须在函数入口处判断d和s是否为NULL,避免野指针导致系统崩溃; - 限制字符串长度,避免数组越界 :可在函数中添加长度参数(如
Strcpy(char *d, const char *s, int max_len)),限制复制 / 拼接的字符个数,防止目标数组溢出; - 使用静态内存替代动态分配 :嵌入式开发中尽量避免
malloc,优先使用固定大小的字符数组或静态内存池,减少内存碎片化问题; - 避免栈溢出,优先使用全局 / 静态字符数组:栈空间通常较小(几 KB 到几十 KB),局部字符数组过大容易导致栈溢出,可将大字符数组定义为全局或静态(存储在全局存储区);
- 兼容多编码格式:若处理中文等多字节字符,需注意编码格式(如 UTF-8),避免按单字节遍历导致逻辑错误。
七、总结
- 手动实现四大字符串函数的核心是指针操作 +
'\0'结束标记,所有逻辑都围绕 "逐字符遍历" 和 "合法字符串收尾" 展开; Strcpy和Strcat的核心区别是遍历起始位置不同,且都必须手动添加'\0';Strlen仅统计'\0'前的有效字符,Strcmp按 ASCII 码值逐字符比较,返回差值;- 嵌入式开发中使用字符串函数,需重点关注指针判空、数组容量、栈溢出三大问题,兼顾功能实现和系统稳定性。
掌握这四个函数的底层实现,不仅能加深对 C 语言字符串的理解,更能为嵌入式开发中的字符串解析、配置项处理等场景打下坚实基础。我是学嵌入式的小杨同学,关注我,解锁更多嵌入式 C 语言实战技巧!