首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再被字符串坑了:strlen/strcpy/strcat/strcmp/strstr/strtok/strerror 深入解析

别再被字符串坑了:strlen/strcpy/strcat/strcmp/strstr/strtok/strerror 深入解析

作者头像
Extreme35
发布2025-12-23 18:08:13
发布2025-12-23 18:08:13
100
举报
文章被收录于专栏:DLDL

一、字符分类函数

函数

参数符合下列条件就返回真

iscntrl

任意控制字符

isspace

空白字符:空格' ',换页'\f',换行'\n',回车'\r',制表符'\t',垂直制表符'\v'

isdigit

十进制数字0~9

isxdigit

十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F

islower

小写字母a~z

isupper

大写字母A~Z

isalpha

字母a~z或者字母A~Z

isalnum

字母或数字,a~z,A~Z或0~9

ispunct

标点符号,任何不属于字母或者数字的图形字符(可打印字符)

isgraph

任何图形字符

isprint

任何可打印字符,包括图形字符和空白字符

在这些函数中,常用的也就只有那么三四个,判断大小写,判断是否为数字或者字母,其他的都不是很常见,故这里直接使用例子一次带过。 以下给出各个函数的用法例子,参考cplusplus.com中对函数的记载来看,以上所有函数均只需要给他们传字符即可:

代码语言:javascript
复制
int main()
{
    // 构造一个“大乱炖”字符串,覆盖控制符、空白、数字、大小写、标点
    char test[] = "\n\t !#09Azaz@[]";
    // 指针遍历字符串,写法更加简洁
    char* p = test;

    puts(" char \tiscntrl\tisspace\tisdigit\tisxdig\tislower\tisupper\tisalpha\tisalnum\tispunct\tisgraph\tisprint");
    puts("------\t-------\t-------\t-------\t------\t-------\t-------\t-------\t-------\t-------\t-------\t-------");

    while (*p)
    {
        // 避免负数下标陷阱
        unsigned char ch = (unsigned char)*p;           

        // 逐函数测试,真返回 1,假返回 0 
        printf("%3c\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
            (isprint(ch) ? ch : '?'),              //不可打印显示 '?'
            iscntrl(ch),
            isspace(ch),
            isdigit(ch),
            isxdigit(ch),
            islower(ch),
            isupper(ch),
            isalpha(ch),
            isalnum(ch),
            ispunct(ch),
            isgraph(ch),
            isprint(ch));
        p++;
    }
    return 0;
}

unsigned char ch = (unsigned char)*p;这句代码是非常有必要的!!!原因如下: char 默认为 signed:范围 −128~127。 isxxx() 要求下标 0~255;负值 → 数组越界 → UB(未定义行为)。 强制 unsigned char 范围 0~255,安全。 这个不注意的话会被支配很久。

接下来进行输出可得到:

在这里插入图片描述
在这里插入图片描述
  1. 前面的\t\n已经被正确识别为控制符。

不知道大家有没有这个问题,在刚开始时不是只有\一个字符吗?为什么它会连带后面的字符一起被识别为控制字符?

  1. 答案是\n在 C 源码里看起来是两个字符,但编译器只把它当成一个字符—换行符(ASCII 10)。所以只会被读取 一次,不会拆成 \ + n
  2. 有个简单的验证方法就是求一下字符串长度,一验便知。
  3. 其余字符非零部分是 Visual Studio 调试器在快速查看或内存窗口里的十进制整型视图,不是程序 printf 的结果。

这些非零值只是内部实现用的位标志,逻辑上仍然当真用。 调试器显示:原始位掩码 → 可能出现 2/4/8/16/128 等,只要 非 0 就是真。

模拟实现

在这里只对常用的四个函数进行实现,也就是isdigit(ch)islower(ch)isupper(ch)isalpha(ch)其他的不怎么常见常用,这里就忽略了。

在实现这几个函数时,可以通过ASCII码进行实现,也可以直接进行比较,实际上也是使用ASCII码进行对比,那就直接进行对比吧。

代码语言:javascript
复制
// 判断字符是否是数字字符
int is_digit(char ch) 
{
    return (ch >= '0' && ch <= '9');
}
// 判断字符是否是小写字母
int is_lower(char ch) 
{
    return (ch >= 'a' && ch <= 'z');
}
// 判断字符是否是大写字母
int is_upper(char ch) 
{
    return (ch >= 'A' && ch <= 'Z');
}
// 判断字符是否是字母(大写或小写)
int is_alpha(char ch) 
{
    return (is_lower(ch) || is_upper(ch));
}

实现也是比较简单的,只需要直接跟边界字符进行比较,判断字符是否在界内即可。


二、字符转换函数

2.1 tolower

函数也比较简单,通过名称可以得出,就是将参数传进去的⼤写字⺟转⼩写。

2.2 toupper

toupper则与tolower刚好相反,将参数传进去的⼩写字⺟转⼤写。

2.3 模拟实现

从ASCII码表可以看出,每一对大小写字母之间的ASCII码差值均为32,故实现时可以直接使用ASCII码加减32得到。

这里Aa的ASCII码应该记住,大写在前,65,小写在后,97,差值32

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
// 将字符转换为大写字母
char to_upper(char ch) 
{
    // 如果字符是小写字母,则将其转换为大写
    if (ch >= 'a' && ch <= 'z')
        return ch - 32;  // 小写字母转换为大写字母
    return ch;  // 其他字符保持不变
}
// 将字符转换为小写字母
char to_lower(char ch) 
{
    // 如果字符是大写字母,则将其转换为小写
    if (ch >= 'A' && ch <= 'Z')
        return ch + 32;  // 大写字母转换为小写字母
    return ch;  // 其他字符保持不变
}

三、strlen的使⽤和模拟实现

代码语言:javascript
复制
size_t strlen ( const char * str );
  • 字符串以 '\0'作为结束标志,strlen函数返回的是在字符串中 '\0' 前⾯出现的字符个数(不包含 '\0' )。
  • 参数指向的字符串必须要以 '\0' 结束。
  • 注意函数的返回值为size_t,是⽆符号的( 易错 )
  • strlen的使⽤需要包含头⽂件 <string.h>

模拟实现

代码语言:javascript
复制
// 遍历⽅式
int my_strlen(const char* str)
{
    int count = 0;
    while (*str)
    {
        count++;
        str++;
    }
    return count;
}
// 递归方式
int my_strlen(const char* str)
{
    if (*str == '\0')
        return 0;
    else
        return 1 + my_strlen(str + 1);
}
//指针-指针的⽅式
int my_strlen(char* s)
{
    char* p = s;
    while (*p != '\0')
        p++;
    return (p - s);
}
  • 遍历实现时只需要注意比末尾的标识符\0即可。
  • 当要求不能创建临时变量时,递归就很好用了,只要没到末尾,就一直递归,没到末尾且可以进入下一次递归时,则必有一个字符。
  • 最后给出的是使用指针里的地址空间进行相减计数,找到头尾字符存储的地址进行相减,即可得到元素数量,注意这里是元素数量,不是字节数,如果你想计算字节数,需要乘上相应指针类型所占的字节数,也就是sizeof(指针类型)

四、strcpystrncpy的使⽤和模拟实现

strcpy函数功能:将源指向的 C 字符串复制到目标指向的数组中,包括终止的空字符(并在该位置停止)。

  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷⻉到⽬标空间。
  • ⽬标空间必须⾜够⼤,以确保能存放源字符串。
  • ⽬标空间必须可修改。
代码语言:javascript
复制
// 模拟 strcpy 函数
char* my_strcpy(char* destination, const char* source) 
{
    char* dest = destination;  // 保存目标指针的起始位置

    // 循环遍历源字符串,直到遇到空字符 '\0'
    while (*source != '\0') 
        *destination++ = *source++;  // 将源字符复制到目标位置 并同时移动

    *destination = '\0';  // 在目标字符串末尾添加 '\0',标志结束

    return dest;  // 返回目标字符串的起始位置
}

这里的复制就相当于直接覆盖,且是全部复制,而strncpy会有长度这个参数

strncpy函数详解:

代码语言:javascript
复制
char * strncpy ( char * destination, const char * source, size_t num );
  1. 复制前 num 个字符:
    • strncpy 会从源字符串 source 中复制最多 num 个字符到目标字符串 destination 中。
    • 如果源字符串的长度小于 num,即源字符串在遇到 '\0' 后就结束了,那么目标字符串将用零字符 '\0' 填充,直到目标字符串的长度达到 num。
    • 如果源字符串的长度大于或等于 num,它只会复制 num 个字符,而不会填充零字符'\0'
  2. 遇到空字符 '\0' 终止符的情况:
    • 如果在复制过程中遇到源字符串中的空字符 '\0',表示源字符串的结束,目标字符串会被填充为零字符,直到目标字符串的长度达到 num。
    • 这意味着目标字符串将包含源字符串的内容(包括空字符 '\0')以及填充的零字符。
  3. 目标字符串的填充:
    • 如果源字符串在复制过程中提前结束了(即遇到 '\0'),目标数组的剩余部分将用 '\0' 填充,直到达到 num 字符。
    • 这是为了保证无论源字符串多长,目标数组都将始终写满 num 个字符。

虽然函数会在源字符串结束后用 '\0' 填充目标数组,但是在复制操作中,如果目标数组的大小不足以容纳 num 个字符,且没有正确保证终止符的位置,则可能会导致目标数组溢出。这时候需要手动添加终止符。

代码语言:javascript
复制
void my_strncpy(char* destination, const char* source, size_t num) 、
{
    size_t i = 0;

    // 复制源字符串中的字符
    while (i < num && source[i] != '\0') {
        destination[i] = source[i];
        i++;
    }

    // 如果源字符串长度小于 num,填充剩余部分为 '\0'
    while (i < num) {
        destination[i] = '\0';
        i++;
    }
}

这里只实现了简单版本,需要具体情况具体分析。


五、strcatstrncat的使⽤和模拟实现

  • strcat功能:把 source 指向的 C 字符串复制到 destination 现有内容的后面。
  • 流程:
    1. 在 destination 里先找到结尾的 '\0'
    2. 从这里开始,把 source 的字符逐个拷贝过来;
    3. 连同 source 的结尾 '\0' 一起拷贝,保证新串也以 '\0' 结束。
  • 因为 destination 的结尾 '\0' 被第一 个 source 字符覆盖,所以新串是destination + source

使用前提

  • 源字符串必须以 '\0' 结束:否则不知道在哪里停,拷到越界为止,未定义行为(UB)。
  • 目标字符串中也得有 '\0':否则不知道从哪儿开始追加(找不到“尾巴”),同样 UB。
  • 目标空间必须足够大:容量需 ≥ strlen(dest) + strlen(src) + 1,否则溢出(UB)。
  • 目标空间必须可修改:比如指向字面量常量区的 char *dest = “abc”; 不能作为 destination。
  • 字符串“自己给自己追加”:strcat(dest, dest) 或任意重叠内存(destination 与 source 有交叠)——未定义行为。标准明确要求两区域不得重叠。
    • 为什么?strcat 是前向拷贝,没有像 memmove 那样处理重叠的保障;一旦写入覆盖了还没读的源数据,逻辑就混乱了:可能死循环、无限增长、崩溃,结果不可预期。
代码语言:javascript
复制
// 模拟 strcat 函数
char* my_strcat(char* dest, const char* src) 
{
    char* d = dest;
    // 找到dest的结尾 \0
    while (*d != '\0')
        d++;
    // 逐字节拷贝src(含终止符)
    while (*src != '\0')
        *d++ = *src++;
    // 手动添加终止符
    *d = '\0';
    return dest;
}

这段实现假定:destsrc 都是以 '\0' 结束的有效C串、两者不重叠,且 dest 有充足空间。

strncatchar* strncat(char* destination, const char* source, size_t num);

  • 主要功能:
    1. 复制前 num 个字符:strncat 会从源字符串 source 中复制最多 num 个字符到目标字符串 destination 的末尾。
    2. 追加'\0' 终止符:strncat 会在目标字符串 destination 的末尾追加一个 '\0' 终止符,确保结果是一个有效的 C 字符串。
  • 特点:
    • 目标字符串的结束符被覆盖:strncat 会覆盖目标字符串 destination 末尾的 '\0',并把复制的字符放在目标字符串的末尾。
    • 如果 source 长度小于 num:strncat 只会复制源字符串的全部内容(直到遇到 '\0'),并不会超出源字符串的结束符。此时,source 字符串的结束符 '\0' 会追加到目标字符串。

strncat 使用时的注意事项:

  1. 目标数组大小:
    • 目标数组 destination 必须有足够的空间来容纳原有的内容和追加的字符。如果目标数组的空间不足,可能导致缓冲区溢出,产生未定义行为。
    • 在使用 strncat 时,需要确保目标数组能容纳 destination 原本的内容以及额外添加的字符。例如:destination 的大小应为 strlen(destination) + num + 1
  2. 目标字符串的结束符:
    • 目标字符串的末尾 '\0' 会被覆盖,故调用 strncat 之前,目标字符串必须以 '\0' 结束。
    • 如果目标字符串的末尾没有 '\0',调用 strncat 时会造成未定义行为。
代码语言:javascript
复制
// 模拟 strncat 函数
char* my_strncat(char* destination, const char* source, size_t num) 
{
    char* d = destination;
    // 找到destination的末尾(\0)
    while (*d != '\0')
        d++;
    // 复制source的前num个字符到destination末尾
    while (num-- > 0 && *source != '\0')
        *d++ = *source++;
    // 在目标字符串末尾添加\0
    *d = '\0';
    return destination;
}

六、strcmpstrncmp的使⽤和模拟实现

  1. 功能: strcmp 比较两个 C 字符串 str1 和 str2 的字符。它通过逐个比较两个字符串中的字符,直到遇到字符不相等或者遇到字符串的结束符 '\0' 为止。
  2. 返回值:strcmp 的返回值根据字符串的比较结果可以是三种情况之一:
    • 大于零:如果 str1 字符串大于 str2 字符串,返回一个大于零的值。
    • 等于零:如果 str1 和 str2 相等,返回 0。
    • 小于零:如果 str1 字符串小于 str2 字符串,返回一个小于零的值。
  3. 具体比较过程: 从第一个字符开始比较:strcmp 会逐个比较两个字符串对应位置的字符,直到遇到不同的字符或者遇到字符串的终止符 '\0'。比较字符的 ASCII 值:
    • 如果字符不同,返回的是两个字符的 ASCII 值的差。例如,如果 str1 中的字符是 ‘A’,str2 中的字符是 ‘B’,则比较结果是 ‘A’ - ‘B’,即 65 - 66 = -1,返回负值。
    • 如果字符相同,则继续比较下一个字符,直到比较完所有字符或遇到 '\0'
  • strtcmp模拟实现
代码语言:javascript
复制
int my_strcmp(const char* str1, const char* str2)
{
    while (*str1 == *str2)      // 1、比较当前字符是否相等
    {
        if (*str1 == '\0')      // 2、如果正好两边都到 '\0',说明完全相等
            return 0;
        str1++;                 // 3、同时推进到下一个字符
        str2++;
    }
    return *str1 - *str2;       // 4、第一个不等字符的差值作为结果
}
  • strncmp函数int strncmp ( const char * str1, const char * str2, size_t num );
  • 功能:比较字符串 str1 与 str2 的前 num 个字符(逐字符比较,遇到不同或到达 '\0' 时停止)。 返回值含义与strcmp返回值一致。
  • 易踩的坑
    • 忽略 n 的语义:前 n 个相同就返回 0,不代表两个完整字符串相同。
    • 带符号 char 比较:自己实现务必转为 unsigned char。
    • 缓冲区未以 '\0' 结尾:用 strncmp 可以,但确保 至少有 n 个可读字节,避免越界。
    • 大小写/本地化:strncmp 是逐字节比较,不做大小写或地区规则转换;要不区分大小写请用自写逻辑或平台特定函数(如 strncasecmp,非标准)。
代码语言:javascript
复制
// 模拟 strncmp 函数
int my_strncmp(const char* s1, const char* s2, size_t n)
{
    while (n--) // 只比较前 n 个字符
    {  
        unsigned char c1 = (unsigned char)*s1++;
        unsigned char c2 = (unsigned char)*s2++;

        if (c1 != c2)         // 一旦不相等,直接返回差值
            return c1 - c2;
        if (c1 == '\0')       // 若遇到字符串结尾,提前退出
            return 0;
    }
    return 0;  // 前 n 个字符都相同
}

七、strstr的使⽤和模拟实现

代码语言:javascript
复制
char* strstr(const char* haystack, const char* needle);
  • 在 haystack(主串)中查找 子串 needle 的首次出现位置。
  • 返回:指向主串中匹配起始处的指针;若找不到则返回 NULL
  • 不比较 终止符 '\0',但遇到 '\0' 即停止。

约定:若 needle 是空串(“”),必定返回 haystack(主串开头)。

  • 行为细节 & 边界:
    • 首次出现:一旦找到第一处完整匹配就返回;不再继续查找。
    • 空子串:needle[0] == '\0' → 直接返回 haystack
    • 空主串:若 haystack 为空串,则仅当 needle 也为空时返回 haystack,否则 NULL。
    • 返回指针指向主串内部:不分配内存、不拷贝数据;只是返回一个指针视图。
    • 大小写敏感:逐字节匹配。要忽略大小写可用 GNU 扩展 strcasestr 或自行转换。
代码语言:javascript
复制
// 模拟 strstr 函数
char* strstr(const char* str1, const char* str2)
{
    char* cp = (char*)str1;
    char* s1, * s2;
    if (!*str2)                // ① 子串为空:按标准直接返回主串开头
        return (char*)str1;
    while (*cp) 
    {              // ② 从主串的每个位置 cp 开始尝试匹配
        s1 = cp;
        s2 = (char*)str2;
        while (*s1 && *s2 && !(*s1 - *s2))
            s1++, s2++;        // ③ 两指针前进:逐字符比较
        if (!*s2)              // ④ 当 s2 走到 '\0',说明子串已完整匹配
            return cp;
        cp++;                  // ⑤ 否则从主串下一位置继续
    }
    return NULL;               // ⑥ 扫描完整个主串也没有匹配
}

八、strtok 函数的使⽤

代码语言:javascript
复制
char *strtok(char *str, const char *sep);
  • 把 C 字符串按分隔符集合 sep 拆成一个个标记(token):
    • 第一次调用:str 传入待分割的可写字符串;
    • 后续调用:str 传 NULL,继续在同一字符串里找下一个标记。
    • 返回值:指向当前标记起始处的指针;没有更多标记时返回 NULL。

工作方式:

  1. sep 是字符集合,集合内任何一个字符出现都算分隔符(不是子串匹配)。
  2. strtok 会在原字符串上修改:把每个分隔符处改成 ‘\0’,因此每次返回的 token 都是原串中的一个子串视图。
  3. 它在库内部保存一个静态游标(指向“下次从哪里开始”),所以:
    • 不是线程安全(不可跨线程共享);
    • 也不能并行处理两条字符串(一个在处理中又调用 strtok 会打断另一个)。
  4. 规则:
    • 跳过起始处的所有分隔符(因此不会产生空 token)。
    • 读到分隔符或 '\0' 停止;若是分隔符就改成 '\0',返回当前 token。
    • 下一次从被改写分隔符后继续。

边界与易错点 1. 不能传字符串字面量:strtok("a,b", ",") → 修改常量区,未定义行为;必须用可写数组或堆内存。 2. 多分隔符连在一起:中间会被跳过,不返回空 token。若需要空 token,请用 strsep 或手写解析。 3. 空分隔符集:sep="" → 没有字符可当分隔符,第一次返回整个字符串,后续都返回 NULL。 4. 空字符串:str="" 或第一次调用后就是 '\0' → 直接返回 NULL。

strtok 用于“就地切分、逐个取 token”的简单场景;会修改原串、跳过连续分隔符且非可重入。

代码语言:javascript
复制
// strtok函数示例
static void test2()
{
    char arr[] = "192.168.6.111";
    char* sep = ".";
    char* str = NULL;
    for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep))
        printf("%s\n", str);
}

九、strerror 函数的使⽤

  • 是什么
代码语言:javascript
复制
char *strerror(int errnum);
  • 把错误码 errnum 映射为可读的英文错误信息字符串,并返回其指针。
    • 错误码常来自全局/线程局部变量 errno(定义在 <errno.h>),如 EINTR、EINVAL、ENOENT 等。
    • 返回的是指向静态内部缓冲区的指针;不要修改、不要 free,且下一次调用可能会被覆盖。
  • 典型用法
代码语言:javascript
复制
// strerror函数使用示例
void test3()
{
    int i = 0;
    for (i = 0; i <= 10; i++)
        printf("%s\n", strerror(i));
}

简单理解就是将错误信息打印出来:

在这里插入图片描述
在这里插入图片描述
  • 上述示例是在手动打印:
    • strerror(errno) 把错误码映射为可读字符串(例如 “No such file or directory”)。
    • printf 打印:“Error opening file unexist.ent: <错误文本>”。
  • 也可以直接一步打印到位:perror函数
代码语言:javascript
复制
// perror函数
void test4()
{
    FILE * pFile;
    pFile = fopen("unexist.ent", "r");
    if (pFile == NULL)
        perror("Error opening file unexist.ent");
}

那他们两者的区别也可以简单了解一下:

方式

优点

缺点

strerror(errno) + printf

可自定义格式/重定向

需要自己保存errno;非可重入

perror("prefix")

简单、一行搞定;自动读errno

输出格式固定、总是到 stderr

这些简单了解了解,需要用的时候再深入学习即可。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、字符分类函数
    • 模拟实现
  • 二、字符转换函数
    • 2.1 tolower
    • 2.2 toupper
    • 2.3 模拟实现
  • 三、strlen的使⽤和模拟实现
  • 四、strcpy 和strncpy的使⽤和模拟实现
  • 五、strcat和strncat的使⽤和模拟实现
  • 六、strcmp和strncmp的使⽤和模拟实现
  • 七、strstr的使⽤和模拟实现
  • 八、strtok 函数的使⽤
  • 九、strerror 函数的使⽤
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档