引言
写C语言躲不开字符串处理,strlen、strcpy、memmove这些函数看着简单,实则遍地陷阱。很多人写代码疯狂bug,面试模拟实现直接卡壳。今天用通俗比喻+实战坑点,一次性吃透这一堆库函数!
头文件提醒:
- 字符分类转换函数:#include <ctype.h>
- 字符串/内存操作函数:#include <string.h>
- 错误码相关:#include <errno.h>
一、字符分类&转换函数:给字符做体检、整容
ctype.h提供一批判断字符的工具函数,参数是int类型(传字符即可),返回非0代表真,返回0代表假。
| islower(c) | 判断是不是小写字母a‑z |
| isupper(c) | 判断是不是大写字母A‑Z |
| isdigit(c) | 判断是不是数字字符'0'~'9' |
| isprint(c) | 判断是否为可打印字符 |
转换两个王牌:
- toupper(c):小写转大写
- tolower(c):大写转小写
⚠️注意:函数不会直接修改原字符!返回转换后的字符,必须接收返回值!
//错误示范!白写,c不会改变
if(islower(c))
toupper(c);
//正确写法
if(islower(c))
c = toupper(c);
练习:把字符串全部小写转大写
#include <stdio.h>
#include <ctype.h>
int main ()
{
int i = 0;
char str[] = "Test String.\\n";
char c;
while (str[i])
{
c = str[i];
if (islower(c))
c = toupper(c);
putchar(c);
i++;
}
return 0;
}
二、字符串基础4件套:strlen / strcpy / strcat / strcmp
1. strlen — 求字符串真实长度
原型:size_t strlen ( const char * str );
- 统计'\\0'之前字符个数,不包含结束符\\0
- 返回值是size_t无符号类型!这是高频大坑!
经典bug:
const char* str1 = "abcdef";
const char* str2 = "bbb";
if(strlen(str2) – strlen(str1) > 0)
{
printf("str2 > str1\\n");
}
strlen(str2)-strlen(str1)数学上是负数,但size_t无符号,负数会变成超大正数,条件永远成立!永远不要拿两个strlen结果相减做判断,直接比较:if(strlen(str2) > strlen(str1))。
三种模拟实现:计数器、递归、指针减指针
//方式1:计数器
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
//方式2:递归,不许创建临时变量
int my_strlen(const char * str)
{
assert(str);
if(*str == '\\0')
return 0;
else
return 1 + my_strlen(str+1);
}
//方式3:指针‑指针
int my_strlen(char *s)
{
assert(s);
char *p = s;
while(*p != '\\0')
p++;
return p – s;
}
2. strcpy — 字符串拷贝
原型:char* strcpy(char * dest, const char * src ); 功能:把源字符串拷贝到目标空间,连同'\\0'一起拷贝过去。
四条铁律:
模拟实现(面试手写):
char* my_strcpy(char *dest, const char*src)
{
char *ret = dest;
assert(dest != NULL);
assert(src != NULL);
//表达式结果赋值,遇到'\\0'赋值后循环终止
while((*dest++ = *src++))
{
;
}
return ret;
}
3. strcat — 字符串追加/拼接
原型:char * strcat ( char * dest, const char * src ); 把源字符串接到目标字符串尾巴上。
注意:
模拟实现思路:先把dest指针走到\\0位置,再执行类似strcpy拷贝。
char* my_strcat(char *dest, const char*src)
{
char *ret = dest;
assert(dest != NULL);
assert(src != NULL);
//找到目标字符串末尾'\\0'
while(*dest)
{
dest++;
}
//追加拷贝
while((*dest++ = *src++))
{
;
}
return ret;
}
4. strcmp — 字符串比较
原型:int strcmp ( const char * str1, const char * str2 ); 不是比较长度!逐字符对比ASCII码!
- str1 > str2 → 返回大于0数字
- str1 == str2 → 返回0
- str1 < str2 → 返回小于0数字
模拟实现:
int my_strcmp (const char * str1, const char * str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
while(*str1 == *str2)
{
if(*str1 == '\\0')
return 0;
str1++;
str2++;
}
return *str1 – *str2;
}
三、长度受限制版本 strncpy / strncat / strncpy
上面strcpy/strcat是“裸奔”,不受长度控制,很容易越界崩溃。于是C提供带n的安全版本,多一个参数size_t num限制最多操作字符数量。
开发中优先使用带n版本,降低缓冲区溢出风险。
四、strstr:查找子字符串
原型:char * strstr ( const char * str1, const char * str2);
在一大段字符串str1里面找str2子串第一次出现的位置。
- 找到:返回子串起始地址;
- 找不到:返回NULL;
- 如果str2是空字符串,直接返回str1的起始地址。
示例:
char str[] ="This is a simple string";
char * pch;
pch = strstr (str,"simple");
if (pch != NULL)
printf("%s\\n", pch);
暴力模拟实现(面试手写):
char * my_strstr (const char * str1, const char * str2)
{
char *cp = (char *) str1;
char *s1, *s2;
if ( !*str2 )
return((char *)str1);
while (*cp)
{
s1 = cp;
s2 = (char *) str2;
while ( *s1 && *s2 && !(*s1-*s2) )
s1++, s2++;
if (!*s2)
return(cp);
cp++;
}
return(NULL);
}
进阶优化可以学习KMP算法,处理超长文本匹配。
五、strtok — C语言简陋版split分割
原型:char * strtok(char * str, const char *delim); 作用:按照分隔符集合delim切割字符串,类似Python的split。
使用规则记牢:
示例:分割IP地址192.168.6.111
char arr[] = "192.168.6.111";
const char* sep = ".";
char buf[30] = {0};
strcpy(buf, arr); //复制一份,保护原始数据
char *str = NULL;
for (str = strtok(buf, sep); str != NULL; str = strtok(NULL, sep))
{
printf("%s\\n", str);
}
缺点:内部保存静态指针,线程不安全,多线程环境不建议使用。
六、strerror / perror:看懂看不懂的错误码
库函数出错,不会直接打印文字,只会给一个数字错误码,存在全局变量errno。
- strerror(errno):输入错误码,返回对应错误信息字符串的地址,不会打印。
- perror("提示文字"):直接打印:提示文字: 错误信息,等价于printf+strerror组合。
示例:打开一个不存在的文件
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
FILE * pFile = NULL;
pFile = fopen ("unexist.ent", "r");
if (pFile == NULL)
{
perror("打开文件失败");
}
return 0;
}
七、内存函数!memcpy、memmove、memset、memcmp
上面字符串函数只针对char*;mem系列直接操作整块内存,任意类型数组都能用,单位全部是字节! 头文件<string.h>。
1. memcpy:内存拷贝(不处理重叠)
原型:void * memcpy ( void * dest, const void * src, size_t num );
- 拷贝num字节,不在乎里面存int、char、结构体;遇到\\0不会停下。
- 如果src和dest内存重叠,结果属于未定义行为,不要用!重叠拷贝请用memmove。
模拟实现:
void * my_memcpy ( void * dst, const void * src, size_t count)
{
void * ret = dst;
assert(dst);
assert(src);
while (count–) {
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
return(ret);
}
2. memmove — 处理内存重叠的拷贝(小米面试高频)
原型:void * memmove ( void * dest, const void * src, size_t num );
memcpy的升级版,专门解决内存重叠场景。
举个生活化比喻:
书架一排书,我要把第3‑6本书挪到第1‑4号位置。 如果直接从前往后搬,还没读的书直接被覆盖毁掉。 此时要分两种策略:
核心判断条件二种等价写法:
//写法1:如果dest>src并且重叠,后向拷贝
if (dest > src && dest <= (char*)src + count)
{
//从后往前复制
}
else
{
//从前向后复制
}
//写法2:不重叠或者dest在左边,正向拷贝
if (dest <= src || (char*)dest >= (char*)src + count)
完整模拟实现:
void * my_memmove ( void * dst, const void * src, size_t count)
{
void * ret = dst;
assert(dst);
assert(src);
if (dst <= src || (char *)dst >= ((char *)src + count))
{
//不重叠,从前向后拷贝
while (count–) {
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
}
else
{
//重叠!目标在源后方,从末尾倒着拷贝
dst = (char *)dst + count – 1;
src = (char *)src + count – 1;
while (count–) {
*(char *)dst = *(char *)src;
dst = (char *)dst – 1;
src = (char *)src – 1;
}
}
return(ret);
}
3. memset:内存按字节设置值
原型:void * memset ( void * ptr, int value, size_t num ); 把内存的num个字节全部设置为value。
重点!按字节设置!不要拿来直接初始化int数组非0值!
char str[] = "hello world";
memset (str,'x',6);
printf(str); //输出 xxxxxxworld
###4. memcmp:内存按字节比较 原型:int memcmp ( const void * ptr1, const void * ptr2, size_t num ); 一块内存拿num个字节逐字节对比,返回规则和strcmp一样。区别:strcmp碰到\\0就结束;memcmp不管内容,硬性对比num个字节。
面试总结速记表
| strcpy / strcat | 遇到\\0停止 | 目标空间要足够,不能重叠拷贝 |
| strncpy / strncat | 限制字符数,更安全 | strncpy不会自动补\\0 |
| strcmp | 逐字符ASCII比较 | 不是比较字符串长度 |
| strstr | 暴力找子串 | 子串为空返回原串 |
| strtok | 分割字符串 | 修改原字符串,后续传NULL,线程不安全 |
| strerror/perror | 解析错误码 | 需要errno.h头文件 |
| memcpy | 内存拷贝,任意类型 | 内存重叠结果未定义 |
| memmove | 可处理内存重叠拷贝 | 第三个参数单位是字节,不是元素个数! |
| memset | 按字节填充 | 不能直接初始化int数组为1/2这类非0值 |
血泪提醒:mem系列第三个参数永远是字节数!拷贝4个int,要写4*sizeof(int),不要硬写死16,丧失可移植性。



