欢迎光临
我们一直在努力

C语言字符串&内存函数保姆级讲解|面试必考,坑点全扒开

引言

写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'一起拷贝过去。

四条铁律:

  • 源字符串必须有\\0结尾;
  • 会把\\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 ); 把源字符串接到目标字符串尾巴上。

    注意:

  • 目标字符串里面必须自带\\0,知道从哪里开始追加;
  • 源字符串要有\\0;
  • 目标空间足够大,可修改。
  • 模拟实现思路:先把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限制最多操作字符数量。

  • strncpy:最多拷贝num个字符。如果源字符串不足num个,后面补\\0;源字符串更长,拷贝num个字符,不会自动补结束符!
  • strncat:最多追加num个字符,追加完成自动补\\0。
  • strncmp:最多比较前num个字符,比到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。

    使用规则记牢:

  • 第一次调用传原始字符串;后面继续切,第一个参数传NULL!
  • 会直接修改原字符串:把分隔符原地替换成'\\0',所以尽量不要直接传字符串常量;想保护原数据,先拷贝一份。
  • 连续多个分隔符会被当成一个;分割完毕返回NULL。
  • 示例:分割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号位置。 如果直接从前往后搬,还没读的书直接被覆盖毁掉。 此时要分两种策略:

  • dest目标在src源的右边,发生重叠:必须从后往前拷贝,防止源数据被提前覆盖;
  • dest在src左边 / 两块内存完全不重叠:正常从前向后拷贝。
  • 核心判断条件二种等价写法:

    //写法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,丧失可移植性。

    多敲代码多调试,纸上得来终觉浅。感谢阅读,我们下次见!886~

    赞(0)
    未经允许不得转载:171主机测评 » C语言字符串&内存函数保姆级讲解|面试必考,坑点全扒开
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址