字符函数和字符串函数

1. 字符分类函数

C语言中提供了一系列用于字符分类的函数,用于判断一个字符属于哪种类型。使用这些函数需要包含头文件 <ctype.h>

函数功能总览

下表列出了常用的字符分类函数及其功能:

函数名 功能说明 判断条件
isalnum 检查字符是否为字母或数字 a-z, A-Z, 0-9
isalpha 检查字符是否为字母 a-z, A-Z
isdigit 检查字符是否为十进制数字 0~9
isxdigit 检查字符是否为十六进制数字 0-9, a-f, A-F
islower 检查字符是否为小写字母 a~z
isupper 检查字符是否为大写字母 A~Z
isspace 检查字符是否为空白字符 空格' '、换页'\f'、换行'\n'、回车'\r'、制表符'\t'、垂直制表符'\v'
ispunct 检查字符是否为标点符号 任何不属于数字或字母的可打印图形字符
isgraph 检查字符是否为任何图形字符(可打印,不包括空格) 除空格外的所有可打印字符
isprint 检查字符是否为任何可打印字符 包括图形字符和空白字符
iscntrl 检查字符是否为任何控制字符 ASCII码 0x00~0x1F0x7F (DEL)

代码示例

c 复制代码
#include <stdio.h>
#include <ctype.h>

int main() {
    char ch = 'A';

    // 使用 isalpha 判断是否为字母
    if (isalpha(ch)) {
        printf("'%c' 是一个字母\n", ch);
    }

    // 使用 isdigit 判断是否为数字
    if (isdigit(ch)) {
        printf("'%c' 是一个数字\n", ch);
    } else {
        printf("'%c' 不是一个数字\n", ch);
    }

    // 使用 islower 和 isupper 判断大小写
    if (islower(ch)) {
        printf("'%c' 是小写字母\n", ch);
    } else if (isupper(ch)) {
        printf("'%c' 是大写字母\n", ch);
    }

    // 使用 isspace 判断空白字符
    char space = ' ';
    if (isspace(space)) {
        printf("空格字符被识别为空白字符\n");
    }

    // 使用 ispunct 判断标点符号
    char punct = '!';
    if (ispunct(punct)) {
        printf("'%c' 是一个标点符号\n", punct);
    }

    // 使用 isalnum 判断字母或数字
    char num = '5';
    if (isalnum(num)) {
        printf("'%c' 是字母或数字\n", num);
    }

    // 使用 isxdigit 判断十六进制数字
    char hex = 'F';
    if (isxdigit(hex)) {
        printf("'%c' 是十六进制数字\n", hex);
    }

    // 使用 isgraph 和 isprint 的区别
    char printable = 'a';
    printf("'%c' 是可打印字符: %d\n", printable, isprint(printable));
    printf("'%c' 是图形字符: %d\n", printable, isgraph(printable));

    // 控制字符(如换行符)不是可打印字符
    char newline = '\n';
    printf("换行符是可打印字符: %d\n", isprint(newline));
    printf("换行符是控制字符: %d\n", iscntrl(newline));

    return 0;
}

运行结果

复制代码
'A' 是一个字母
'A' 不是一个数字
'A' 是大写字母
空格字符被识别为空白字符
'!' 是一个标点符号
'5' 是字母或数字
'F' 是十六进制数字
'a' 是可打印字符: 1
'a' 是图形字符: 1
换行符是可打印字符: 0
换行符是控制字符: 1

重点说明

  1. 返回值 :这些函数在判断条件成立时返回非零值(真) ,不成立时返回 0(假)
  2. 头文件 :使用前必须包含 <ctype.h>
  3. 参数类型 :函数接收 int 类型的参数,实际传入 char 类型时会自动提升为 int
  4. isgraph vs isprintisprint 包含空格字符,而 isgraph 不包含空格字符,这是两者唯一的区别。
  5. iscntrl :用于判断控制字符(如 \n\t\0 等),这些字符不可打印。

2. 字符转换函数

C语言中提供了两个字符转换函数,用于实现字母大小写的转换。使用这些函数同样需要包含头文件 <ctype.h>

函数原型

c 复制代码
int tolower(int c);  // 将大写字母转换为小写字母
int toupper(int c);  // 将小写字母转换为大写字母

函数说明

函数名 功能 参数 返回值
tolower 将大写字母转换为小写字母 要转换的字符(int类型) 如果参数是大写字母,返回对应的小写字母;否则返回原字符
toupper 将小写字母转换为大写字母 要转换的字符(int类型) 如果参数是小写字母,返回对应的大写字母;否则返回原字符

代码示例

c 复制代码
#include <stdio.h>
#include <ctype.h>

int main() {
    char upper = 'A';
    char lower = 'b';
    char digit = '3';

    // 大写转小写
    printf("tolower('%c') = '%c'\n", upper, tolower(upper));

    // 小写转大写
    printf("toupper('%c') = '%c'\n", lower, toupper(lower));

    // 非字母字符转换(返回原字符)
    printf("tolower('%c') = '%c'\n", digit, tolower(digit));
    printf("toupper('%c') = '%c'\n", digit, toupper(digit));

    // 实际应用:忽略大小写比较
    char input = 'Y';
    if (toupper(input) == 'Y') {
        printf("用户确认操作\n");
    }

    return 0;
}

运行结果

复制代码
tolower('A') = 'a'
toupper('b') = 'B'
tolower('3') = '3'
toupper('3') = '3'
用户确认操作

重点说明

  1. 非字母字符 :如果传入的参数不是字母,函数会原样返回该字符。
  2. 参数类型 :虽然参数是 int 类型,但实际传入 char 类型时会自动提升。
  3. 头文件 :使用前必须包含 <ctype.h>
  4. 常见用途:常用于忽略大小写的字符串比较、用户输入规范化等场景。

3. strlen的使用和模拟实现

3.1 函数介绍

strlen 是C语言中用于计算字符串长度的函数,定义在 <string.h> 头文件中。

c 复制代码
size_t strlen(const char *str);

函数说明

项目 说明
功能 统计参数 str 指向的字符串的长度,统计的是字符串中 '\0' 之前的字符个数
参数 str:指向要统计长度的字符串的指针
返回值 返回字符串的长度,类型为 size_t(无符号整数)
头文件 <string.h>

使用注意事项

  1. 结束标志 :字符串以 '\0' 作为结束标志,strlen 返回的是 '\0' 前面出现的字符个数(不包含 '\0')。
  2. 必须包含 '\0' :参数指向的字符串必须'\0' 结束,否则函数会继续向后访问内存,导致越界访问
  3. 返回值类型 :返回类型为 size_t,是无符号整数,这在比较时容易出错(见下方易错点)。

3.2 strlen返回值详解

strlen 的返回值类型 size_t 在大多数编译器中是 unsigned intunsigned long 的别名。由于是无符号类型,在比较时需特别注意:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    // 正确用法
    const char *str1 = "Hello";
    size_t len = strlen(str1);
    printf("字符串 \"%s\" 的长度为: %zu\n", str1, len);  // 输出 5

    // 易错点:无符号类型比较
    const char *str2 = "abc";
    const char *str3 = "abcdef";

    // 错误比较方式
    if (strlen(str2) - strlen(str3) > 0) {
        printf("str2 比 str3 长(错误判断)\n");
    } else {
        printf("str2 比 str3 短(正确结果)\n");
    }
    // 解释:strlen("abc") - strlen("abcdef") = 3 - 6 = -3
    // 但由于 size_t 是无符号类型,-3 会被解释为一个很大的正数

    // 正确比较方式
    if (strlen(str2) > strlen(str3)) {
        printf("str2 比 str3 长\n");
    } else {
        printf("str2 比 str3 短\n");
    }

    return 0;
}

运行结果

复制代码
字符串 "Hello" 的长度为: 5
str2 比 str3 长(错误判断)
str2 比 str3 短

3.3 strlen的模拟实现

方式一:计数器方式

c 复制代码
#include <stdio.h>
#include <assert.h>

size_t my_strlen_counter(const char *str) {
    assert(str != NULL);  // 断言,防止传入空指针
    size_t count = 0;
    while (*str != '\0') {
        count++;
        str++;
    }
    return count;
}

方式二:指针减指针方式

c 复制代码
#include <stdio.h>
#include <assert.h>

size_t my_strlen_pointer(const char *str) {
    assert(str != NULL);
    const char *start = str;
    while (*str != '\0') {
        str++;
    }
    return (size_t)(str - start);  // 两个指针相减得到元素个数
}

方式三:递归方式

c 复制代码
#include <stdio.h>
#include <assert.h>

size_t my_strlen_recursive(const char *str) {
    assert(str != NULL);
    if (*str == '\0') {
        return 0;
    }
    return 1 + my_strlen_recursive(str + 1);
}
完整测试代码
c 复制代码
#include <stdio.h>
#include <string.h>

size_t my_strlen_counter(const char *str) {
    size_t count = 0;
    while (*str != '\0') {
        count++;
        str++;
    }
    return count;
}

size_t my_strlen_pointer(const char *str) {
    const char *start = str;
    while (*str != '\0') {
        str++;
    }
    return (size_t)(str - start);
}

size_t my_strlen_recursive(const char *str) {
    if (*str == '\0') {
        return 0;
    }
    return 1 + my_strlen_recursive(str + 1);
}

int main() {
    const char *test = "Hello World!";

    printf("原始字符串: \"%s\"\n", test);
    printf("库函数 strlen:     %zu\n", strlen(test));
    printf("计数器方式:        %zu\n", my_strlen_counter(test));
    printf("指针减指针方式:    %zu\n", my_strlen_pointer(test));
    printf("递归方式:          %zu\n", my_strlen_recursive(test));

    // 测试空字符串
    const char *empty = "";
    printf("\n空字符串测试:\n");
    printf("库函数 strlen:     %zu\n", strlen(empty));
    printf("计数器方式:        %zu\n", my_strlen_counter(empty));
    printf("指针减指针方式:    %zu\n", my_strlen_pointer(empty));
    printf("递归方式:          %zu\n", my_strlen_recursive(empty));

    return 0;
}

运行结果

复制代码
原始字符串: "Hello World!"
库函数 strlen:     12
计数器方式:        12
指针减指针方式:    12
递归方式:          12

空字符串测试:
库函数 strlen:     0
计数器方式:        0
指针减指针方式:    0
递归方式:          0

三种实现方式对比

实现方式 原理 优点 缺点
计数器方式 遍历字符串,每遇到一个非'\0'字符就计数+1 逻辑简单直观,易于理解 需要额外变量记录计数
指针减指针方式 用结束指针减去起始指针,得到元素个数 代码简洁,效率高 需要理解指针运算
递归方式 递归调用,每次递归计数+1 代码优雅,体现递归思想 效率低,字符串过长可能导致栈溢出

重点总结

  1. strlen 不包含 '\0' :返回的是 '\0' 之前的字符个数。
  2. 字符串必须以 '\0' 结尾:否则会导致越界访问。
  3. 返回值是 size_t:无符号类型,比较时注意不要直接相减。
  4. 模拟实现:计数器方式最常用,指针减指针方式最简洁,递归方式适合理解递归思想。

4. strcpy的使用和模拟实现

strcpy 是C语言中用于字符串拷贝的函数,定义在 <string.h> 头文件中。

c 复制代码
char* strcpy(char *destination, const char *source);

函数说明

项目 说明
功能 将源字符串(包括 '\0')拷贝到目标空间
参数 destination:指向目标空间的指针;source:指向源字符串的指针
返回值 返回目标空间的起始地址(即 destination 的值)
头文件 <string.h>

4.1 代码演示

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Hello World";
    char dest[20] = {0};  // 初始化为全0

    printf("拷贝前: dest = \"%s\"\n", dest);

    // 使用 strcpy 进行字符串拷贝
    char *ret = strcpy(dest, src);

    printf("拷贝后: dest = \"%s\"\n", dest);
    printf("返回值: ret = \"%s\"\n", ret);

    // 验证返回值就是目标空间的起始地址
    if (ret == dest) {
        printf("返回值与目标地址相同\n");
    }

    return 0;
}

运行结果

复制代码
拷贝前: dest = ""
拷贝后: dest = "Hello World"
返回值: ret = "Hello World"
返回值与目标地址相同

使用注意事项

  1. 源字符串必须以 '\0' 结束strcpy 会一直拷贝直到遇到源字符串中的 '\0',如果源字符串没有 '\0',会导致越界访问。
  2. 会将源字符串中的 '\0' 拷贝到目标空间 :拷贝完成后,目标字符串也会以 '\0' 结尾。
  3. 目标空间必须足够大 :目标空间的大小必须能够容纳源字符串的全部内容(包括 '\0'),否则会导致缓冲区溢出。
  4. 目标空间必须可修改:目标空间不能是字符串常量或只读内存区域。

4.2 模拟实现

下面展示三种逐步优化的 strcpy 模拟实现方式:

方式一:基础版本(使用临时变量)

c 复制代码
#include <assert.h>

char* my_strcpy(char* dest, const char* src) {
    char* ret = dest;          // 保存目标起始地址用于返回
    assert(dest && src);       // 断言,保证指针有效性

    while (*src != '\0') {     // 遍历源字符串,直到遇到 '\0'
        *dest = *src;          // 将源字符拷贝到目标空间
        dest++;                // 目标指针后移
        src++;                 // 源指针后移
    }
    *dest = *src;              // 拷贝源字符串末尾的 '\0'

    return ret;                // 返回目标空间的起始地址
}

int main() {
    char arr1[] = "abcdef";
    char arr2[20] = "-------------";

    char* p = my_strcpy(arr2, arr1);

    printf("%s\n", arr2);      // 输出: abcdef
    printf("%s\n", p);         // 输出: abcdef

    return 0;
}

代码分析

  • 使用 assert 确保 destsrc 都不是空指针。
  • while 循环逐个字符拷贝,直到遇到 '\0'
  • 循环结束后,单独拷贝 '\0' 到目标空间。
  • 返回保存的目标起始地址。

方式二:简化版本(指针自增合并)

c 复制代码
#include <assert.h>

char* my_strcpy(char* dest, const char* src) {
    char* ret = dest;          // 保存目标起始地址用于返回
    assert(dest && src);       // 断言,保证指针有效性

    while (*src != '\0') {
        *dest++ = *src++;      // 将拷贝和指针自增合并为一条语句
    }
    *dest = *src;              // 拷贝源字符串末尾的 '\0'

    return ret;                // 返回目标空间的起始地址
}

int main() {
    char arr1[] = "abcdef";
    char arr2[20] = "-------------";

    char* p = my_strcpy(arr2, arr1);

    printf("%s\n", arr2);      // 输出: abcdef
    printf("%s\n", p);         // 输出: abcdef

    return 0;
}

代码分析

  • *dest++ = *src++ 等价于 *dest = *src; dest++; src++;,将三条语句合并为一条,代码更简洁。
  • 先执行 *dest = *src 完成拷贝,然后 dest++src++ 分别自增。

方式三:极致精简版本(利用赋值表达式的值)

c 复制代码
#include <assert.h>

char* my_strcpy(char* dest, const char* src) {
    char* ret = dest;          // 保存目标起始地址用于返回
    assert(dest && src);       // 断言,保证指针有效性

    while (*dest++ = *src++) { // 拷贝字符,同时判断是否为 '\0'
        ;                      // 空循环体
    }

    return ret;                // 返回目标空间的起始地址
}

int main() {
    char arr1[] = "abcdef";
    char arr2[20] = "-------------";

    char* p = my_strcpy(arr2, arr1);

    printf("%s\n", arr2);      // 输出: abcdef
    printf("%s\n", p);         // 输出: abcdef

    return 0;
}

代码分析

  • 这是最经典的 strcpy 模拟实现方式,代码极其简洁。
  • *dest++ = *src++ 先执行赋值操作,然后返回赋值的结果(即拷贝的字符)。
  • 当拷贝到 '\0' 时,赋值表达式的结果为 0(假),循环结束。
  • 关键点'\0' 已经被拷贝到目标空间,因此不需要再单独拷贝 '\0'
  • 空循环体 ; 表示循环条件中已经完成了所有工作。

代码结束后 '\0' 的位置详解(以方式三为例)

假设 arr1(源字符串)的内存布局为:'a' 'b' 'c' 'd' 'e' 'f' '\0'arr2(目标空间)初始化为:'-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'(共20个字符,最后一个为 '\0')。

逐步执行过程

c 复制代码
while (*dest++ = *src++) {
    ;
}

逐次执行过程如下:

循环次数 *src (赋值前) *dest = *src (赋值后) dest 自增后指向 src 自增后指向 赋值结果(判断条件)
第1次 'a' dest[0] = 'a' 指向 arr2 第2个元素 指向 'b' 'a' (非0,继续)
第2次 'b' dest[1] = 'b' 指向 arr2 第3个元素 指向 'c' 'b' (非0,继续)
第3次 'c' dest[2] = 'c' 指向 arr2 第4个元素 指向 'd' 'c' (非0,继续)
第4次 'd' dest[3] = 'd' 指向 arr2 第5个元素 指向 'e' 'd' (非0,继续)
第5次 'e' dest[4] = 'e' 指向 arr2 第6个元素 指向 'f' 'e' (非0,继续)
第6次 'f' dest[5] = 'f' 指向 arr2 第7个元素 指向 '\0' 'f' (非0,继续)
第7次 '\0' dest[6] = '\0' 指向 arr2 第8个元素 指向 src 末尾之后 '\0' (0,循环结束)

内存布局变化

拷贝前

复制代码
arr2: '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'
      ↑dest (起始位置)
arr1: 'a' 'b' 'c' 'd' 'e' 'f' '\0'
      ↑src (起始位置)

拷贝后

复制代码
arr2: 'a' 'b' 'c' 'd' 'e' 'f' '\0' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'
      ↑ret (返回地址)                ↑dest (循环结束后的位置)
arr1: 'a' 'b' 'c' 'd' 'e' 'f' '\0'
                                  ↑src (循环结束后的位置)

关键结论

  1. 源字符串的 '\0' 被拷贝到了目标空间 :在第7次循环中,*dest++ = *src++ 将源字符串末尾的 '\0' 写入了 arr2[6] 位置。这是 strcpystrncpy 的重要区别------strcpy 一定会拷贝 '\0'

  2. 目标空间原有的 '\0' 未被覆盖arr2 初始化为 "-------------"(13个 '-' 加一个 '\0'),拷贝后 arr2[0]~arr2[5]'a'~'f' 覆盖,arr2[6]'\0' 覆盖,arr2[7]~arr2[19] 保持不变(其中 arr2[19] 仍然是初始的 '\0')。但 printf("%s", arr2) 只打印到第一个 '\0'(即 arr2[6]),所以输出为 "abcdef"

  3. dest 指针的最终位置 :循环结束后,dest 指向 arr2[7](即新 '\0' 之后的下一个位置),而 ret 始终指向 arr2[0](起始地址)。

  4. 方式一和方式二中的 '\0' 处理 :在方式一和方式二中,循环结束后通过 *dest = *src 单独拷贝 '\0',效果与方式三完全相同------源字符串的 '\0' 被写入目标空间。

三种实现方式对比

实现方式 代码行数 特点 适用场景
方式一 较多 逻辑清晰,每一步都明确分开 初学者理解,教学演示
方式二 中等 代码简洁,指针自增合并 日常开发,兼顾可读性和简洁性
方式三 最少 极致精简,利用赋值表达式返回值 高手写法,追求代码极致简洁

重点总结

  1. strcpy 会拷贝 '\0' :源字符串末尾的 '\0' 也会被拷贝到目标空间,这是确保目标字符串正确结束的关键。
  2. 目标空间必须足够大 :这是使用 strcpy 最容易出错的地方,可能导致缓冲区溢出。
  3. 返回值是目标地址 :返回 destination 的原始值,支持链式调用。
  4. 模拟实现的核心 :逐个字符拷贝,直到遇到 '\0' 为止,并且 '\0' 也会被拷贝。
  5. 最经典的实现while (*dest++ = *src++); 是C语言中广为流传的精简写法。

5. strcat的使用和模拟实现

strcat 是C语言中用于字符串追加(拼接)的函数,定义在 <string.h> 头文件中。

c 复制代码
char *strcat(char *destination, const char *source);

函数说明

项目 说明
功能 source 指向的源字符串中的所有字符(包括 '\0')追加到 destination 指向的目标字符串的末尾
参数 destination:指向目标空间的指针(必须已有字符串内容);source:指向源字符串的指针
返回值 返回目标空间的起始地址(即 destination 的值)
头文件 <string.h>

5.1 代码演示

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char dest[20] = "hello ";   // 目标空间,已有字符串 "hello "
    char src[] = "world";       // 源字符串

    printf("追加前: dest = \"%s\"\n", dest);

    // 使用 strcat 进行字符串追加
    char *ret = strcat(dest, src);

    printf("追加后: dest = \"%s\"\n", dest);
    printf("返回值: ret = \"%s\"\n", ret);

    // 验证返回值就是目标空间的起始地址
    if (ret == dest) {
        printf("返回值与目标地址相同\n");
    }

    return 0;
}

运行结果

复制代码
追加前: dest = "hello "
追加后: dest = "hello world"
返回值: ret = "hello world"
返回值与目标地址相同

使用注意事项

  1. 源字符串必须以 '\0' 结束strcat 会一直拷贝直到遇到源字符串中的 '\0'
  2. 目标字符串中也必须有 '\0'strcat 需要找到目标字符串中的 '\0' 来确定追加的起始位置,否则无法知道从哪里开始追加。
  3. 目标空间必须足够大 :目标空间的大小必须能够容纳原有字符串 + 源字符串的全部内容(包括 '\0'),否则会导致缓冲区溢出。
  4. 目标空间必须可修改:目标空间不能是字符串常量或只读内存区域。
  5. 内存重叠问题:源字符串和目标空间不能有重叠,否则行为是未定义的。

5.2 模拟实现

c 复制代码
#include <stdio.h>
#include <assert.h>

char* my_strcat(char* dest, const char* src) {
    char* ret = dest;          // 保存目标起始地址用于返回
    assert(dest && src);       // 断言,保证指针有效性

    // 第一步:找到目标字符串的末尾(即 '\0' 的位置)
    while (*dest) {            // 等价于 while (*dest != '\0')
        dest++;
    }
    // 此时 dest 指向目标字符串末尾的 '\0'

    // 第二步:将源字符串拷贝到目标末尾(包括 '\0')
    while (*dest++ = *src++) { // 拷贝字符,同时判断是否为 '\0'
        ;                      // 空循环体
    }
    // 循环结束时,源字符串的 '\0' 已经被拷贝到目标空间

    return ret;                // 返回目标空间的起始地址
}

int main() {
    char arr1[20] = "hello";   // 目标空间,内容为 "hello\0"
    char arr2[] = "world";     // 源字符串,内容为 "world\0"

    char* p = my_strcat(arr1, arr2);

    printf("%s\n", arr1);      // 输出: helloworld
    printf("%s\n", p);         // 输出: helloworld

    return 0;
}

代码逐步分析

假设 arr1 的内存布局为:'h' 'e' 'l' 'l' 'o' '\0' ...(剩余空间未初始化),arr2 的内存布局为:'w' 'o' 'r' 'l' 'd' '\0'

第一步:找 '\0'

c 复制代码
while (*dest) {    // *dest 依次为 'h','e','l','l','o',都不为0,继续循环
    dest++;        // dest 指针依次后移
}
// 当 *dest 指向 '\0' 时,条件为假,循环结束
// 此时 dest 指向 arr1 中 'o' 后面的 '\0' 位置

第二步:数据拷贝

c 复制代码
while (*dest++ = *src++) {  // 先赋值,再自增,然后判断赋值结果
    ;
}

逐次执行过程如下:

循环次数 *src (赋值前) *dest = *src (赋值后) dest 自增后指向 src 自增后指向 赋值结果(判断条件)
第1次 'w' dest[0] = 'w' 指向下一个位置 指向 'o' 'w' (非0,继续)
第2次 'o' dest[1] = 'o' 指向下一个位置 指向 'r' 'o' (非0,继续)
第3次 'r' dest[2] = 'r' 指向下一个位置 指向 'l' 'r' (非0,继续)
第4次 'l' dest[3] = 'l' 指向下一个位置 指向 'd' 'l' (非0,继续)
第5次 'd' dest[4] = 'd' 指向下一个位置 指向 '\0' 'd' (非0,继续)
第6次 '\0' dest[5] = '\0' 指向下一个位置 指向 src 末尾之后 '\0' (0,循环结束)

代码结束后 '\0' 的位置详解

关键问题:代码结束后,'\0' 在哪里?是否被取代了?

  1. 目标字符串原有的 '\0' 被覆盖了 :在第一步中,dest 指针移动到了 "hello" 末尾的 '\0' 位置。在第二步的第1次循环中,*dest++ = *src++'w' 写入了这个位置,原有的 '\0''w' 覆盖

  2. 新的 '\0' 被追加到了末尾 :在第二步的第6次循环中,*dest++ = *src++ 将源字符串末尾的 '\0' 拷贝到了 "helloworld" 之后的位置。此时 dest 指向的是 "helloworld" 之后的下一个位置(即新 '\0' 之后的位置)。

  3. 最终内存布局

    复制代码
    追加前:  arr1 = 'h' 'e' 'l' 'l' 'o' '\0' [未定义] [未定义] ...
    追加后:  arr1 = 'h' 'e' 'l' 'l' 'o' 'w'  'o'  'r'  'l'  'd'  '\0' [未定义] ...
    • 原有的 '\0'(第6个元素)被 'w' 取代。
    • 新的 '\0' 被追加到了第11个元素的位置("helloworld" 共10个字符,第11个位置是新的 '\0')。
  4. 结论 :原有的 '\0' 被源字符串的第一个字符取代 ,新的 '\0' 由源字符串末尾的 '\0' 提供 ,被追加到拼接后字符串的末尾。目标字符串始终以 '\0' 结尾,但 '\0' 的位置发生了变化。

重点总结

  1. strcat 会覆盖目标字符串的 '\0' :目标字符串末尾的 '\0' 会被源字符串的第一个字符覆盖。
  2. strcat 会拷贝源字符串的 '\0' :源字符串末尾的 '\0' 会被拷贝到拼接后字符串的末尾,确保结果字符串以 '\0' 结尾。
  3. 目标空间必须足够大 :这是使用 strcat 最容易出错的地方,可能导致缓冲区溢出。
  4. 目标字符串必须有 '\0' :否则 strcat 无法找到追加的起始位置。
  5. 模拟实现的核心 :先找到目标字符串的 '\0',然后从该位置开始拷贝源字符串(包括 '\0')。

6. strcmp的使用和模拟实现

strcmp 是C语言中用于比较两个字符串的函数,定义在 <string.h> 头文件中。

c 复制代码
int strcmp(const char *str1, const char *str2);

函数说明

项目 说明
功能 比较 str1str2 指向的两个字符串,从第一个字符开始逐字符比较其ASCII码值,直到遇到不相等的字符或遇到 '\0' 为止
参数 str1:指向要比较的第一个字符串的指针;str2:指向要比较的第二个字符串的指针
返回值 第一个字符串大于第二个字符串,返回大于0 的数字;相等则返回 0 ;第一个字符串小于第二个字符串,返回小于0的数字
头文件 <string.h>

返回值详解

strcmp 的返回值遵循以下规则:

  • str1 > str2 :返回一个大于0的整数(不一定是1,不同编译器实现可能不同)。
  • str1 == str2 :返回 0
  • str1 < str2 :返回一个小于0的整数(不一定是-1)。

注意 :标准只规定返回正数、0或负数,并没有规定具体数值。因此,不要if (strcmp(s1, s2) == 1) 来判断大于,而应该用 if (strcmp(s1, s2) > 0)

比较规则

  1. 逐字符比较:从两个字符串的第一个字符开始,比较它们的ASCII码值。
  2. 遇到不相等则停止:一旦找到ASCII码值不相等的两个字符,比较结束,根据这两个字符的ASCII码值大小决定返回值。
  3. 遇到 '\0' 则停止 :如果一直比较到某个字符串的末尾(遇到 '\0')都没有发现不相等的字符,则较短的字符串被视为"小于"较长的字符串。
  4. 完全相等:只有当两个字符串长度相等且每个对应位置的字符都相同时,才返回0。

代码演示

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "abcdef";
    char str2[] = "abcdef";   // 与str1完全相同
    char str3[] = "abcdeg";   // 第6个字符 'f' vs 'g'
    char str4[] = "abc";      // 较短字符串
    char str5[] = "abz";      // 第3个字符 'c' vs 'z'

    // 情况1:完全相等
    int result1 = strcmp(str1, str2);
    printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str2, result1);
    // 输出: 0(相等)

    // 情况2:第6个字符不同('f' < 'g')
    int result2 = strcmp(str1, str3);
    printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str3, result2);
    // 输出: 负数('f'的ASCII码102 < 'g'的ASCII码103)

    // 情况3:str1比str4长(str4先遇到'\0')
    int result3 = strcmp(str1, str4);
    printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str4, result3);
    // 输出: 正数('d'的ASCII码100 > '\0'的ASCII码0)

    // 情况4:第3个字符不同('c' < 'z')
    int result4 = strcmp(str4, str5);
    printf("strcmp(\"%s\", \"%s\") = %d\n", str4, str5, result4);
    // 输出: 负数('c'的ASCII码99 < 'z'的ASCII码122)

    // 正确判断方式:使用 >0、==0、<0
    if (result1 == 0) {
        printf("\"%s\" 等于 \"%s\"\n", str1, str2);
    }

    if (result2 < 0) {
        printf("\"%s\" 小于 \"%s\"\n", str1, str3);
    }

    if (result3 > 0) {
        printf("\"%s\" 大于 \"%s\"\n", str1, str4);
    }

    return 0;
}

运行结果

复制代码
strcmp("abcdef", "abcdef") = 0
strcmp("abcdef", "abcdeg") = -1
strcmp("abcdef", "abc") = 100
strcmp("abc", "abz") = -23
"abcdef" 等于 "abcdef"
"abcdef" 小于 "abcdeg"
"abcdef" 大于 "abc"

使用注意事项

  1. 不要直接比较返回值是否为1或-1:标准只规定返回正数、0或负数,不同编译器实现可能返回不同的具体数值(如GCC可能返回ASCII码差值,而MSVC可能返回1或-1)。
  2. 字符串必须以 '\0' 结尾 :否则 strcmp 会继续向后访问内存,导致越界访问。
  3. 区分大小写strcmp 是区分大小写的,'A'(ASCII 65)和 'a'(ASCII 97)被视为不同的字符。
  4. 不要用 == 直接比较字符串if (str1 == str2) 比较的是指针地址,而不是字符串内容,必须使用 strcmp

6.1 模拟实现

下面展示两种 strcmp 的模拟实现方式:

方式一:使用 if-else 判断返回值

c 复制代码
#include <stdio.h>
#include <assert.h>

int my_strcmp(const char* s1, const char* s2) {
    assert(s1 && s2);          // 断言,保证指针有效性

    while (*s1 == *s2) {       // 当两个字符相等时继续循环
        if (*s1 == '\0') {     // 如果遇到字符串末尾的 '\0'
            return 0;          // 说明两个字符串完全相等,返回0
        }
        s1++;                  // 指针后移,比较下一个字符
        s2++;
    }
    // 循环结束说明找到了不相等的字符
    if (*s1 > *s2) {
        return 1;              // s1的字符ASCII码更大,返回正数
    } else {
        return -1;             // s1的字符ASCII码更小,返回负数
    }
}

int main() {
    char arr1[] = "abcdef";
    char arr2[] = "acq";
    int r = my_strcmp(arr1, arr2);

    if (r > 0)
        printf(">\n");
    else if (r < 0)
        printf("<\n");
    else
        printf("==\n");

    return 0;
}

代码分析

  1. assert(s1 && s2):确保传入的指针都不是空指针,防止程序崩溃。
  2. while (*s1 == *s2):当两个字符相等时,继续循环比较下一个字符。
  3. if (*s1 == '\0') return 0; :如果在循环中遇到了 '\0',说明两个字符串同时到达末尾且之前所有字符都相等,因此返回0表示相等。
  4. s1++; s2++;:指针后移,准备比较下一对字符。
  5. 循环结束后的判断 :当 *s1 != *s2 时退出循环,此时根据两个字符的ASCII码大小关系返回1或-1。

执行过程分析 (以 arr1 = "abcdef"arr2 = "acq" 为例):

循环次数 *s1 *s2 *s1 == *s2 操作 说明
第1次 'a' (97) 'a' (97) 相等 s1++, s2++ 继续比较
第2次 'b' (98) 'c' (99) 不相等 退出循环 找到第一个不相等的字符

退出循环后:

  • *s1 = 'b' (ASCII 98)
  • *s2 = 'c' (ASCII 99)
  • *s1 > *s2 为假,执行 else 分支,返回 -1
  • 因此 "abcdef" < "acq",输出 <

方式二:使用指针相减(更接近库函数实现)

c 复制代码
#include <stdio.h>
#include <assert.h>

int my_strcmp(const char* s1, const char* s2) {
    assert(s1 && s2);          // 断言,保证指针有效性

    while (*s1 == *s2) {       // 当两个字符相等时继续循环
        if (*s1 == '\0') {     // 如果遇到字符串末尾的 '\0'
            return 0;          // 说明两个字符串完全相等,返回0
        }
        s1++;                  // 指针后移,比较下一个字符
        s2++;
    }
    // 返回两个字符的ASCII码差值
    return *s1 - *s2;          // 正数表示s1>s2,负数表示s1<s2,0表示相等
}

int main() {
    char arr1[] = "abcdef";
    char arr2[] = "acq";
    int r = my_strcmp(arr1, arr2);

    if (r > 0)
        printf(">\n");
    else if (r < 0)
        printf("<\n");
    else
        printf("==\n");

    return 0;
}

代码分析

  1. return *s1 - *s2;:这是方式一与方式二的核心区别。直接返回两个字符的ASCII码差值,而不是固定返回1或-1。

    • 如果 *s1 > *s2,差值为正数(如 'b' - 'c' = 98 - 99 = -1,但这里 'b' < 'c' 所以是负数)。
    • 如果 *s1 < *s2,差值为负数。
    • 这种方式更接近标准库的实现,返回值能反映字符差异的大小。
  2. 与方式一的区别

    • 方式一固定返回 1-1,只表示大小关系,不反映差异程度。
    • 方式二返回ASCII码差值,如 'b' - 'c' = -1'd' - 'a' = 3,能反映字符差异的大小。

执行过程分析 (以 arr1 = "abcdef"arr2 = "acq" 为例):

循环次数 *s1 *s2 *s1 == *s2 操作 说明
第1次 'a' (97) 'a' (97) 相等 s1++, s2++ 继续比较
第2次 'b' (98) 'c' (99) 不相等 退出循环 找到第一个不相等的字符

退出循环后:

  • *s1 = 'b' (ASCII 98)
  • *s2 = 'c' (ASCII 99)
  • *s1 - *s2 = 98 - 99 = -1
  • 返回 -1,表示 "abcdef" < "acq"

两种实现方式对比

实现方式 返回值特点 优点 缺点
方式一(if-else) 固定返回1或-1 返回值固定,易于理解 丢失了字符差异信息
方式二(指针相减) 返回ASCII码差值 更接近标准库实现,保留差异信息 返回值不固定,初学者可能困惑

重点总结

  1. strcmp 逐字符比较ASCII码值 :从第一个字符开始,直到遇到不相等的字符或 '\0'
  2. 返回值规则 :大于返回正数,等于返回0,小于返回负数。不要== 1== -1 来判断。
  3. 字符串必须以 '\0' 结尾:否则会导致越界访问。
  4. 区分大小写:大写字母和小写字母的ASCII码值不同。
  5. 模拟实现的核心 :在 while 循环中逐字符比较,遇到不相等或 '\0' 时返回结果。
  6. 最经典的实现return *s1 - *s2; 是C语言中广为流传的精简写法,直接利用ASCII码差值作为返回值。

7. strncpy函数详解

strncpy函数是C语言标准库中用于字符串拷贝的函数,它允许指定最大拷贝字符数,从而提供比strcpy函数更高的安全性。下面我将详细解释其功能、用法,并重点比较它与strcpy的区别,使用C语言代码示例演示不同场景下\0(字符串终止符)的位置变化。

7.1 strncpy函数介绍

函数原型:char *strncpy(char *destination, const char *source, size_t num);

  • 功能 :将source指向的字符串拷贝到destination指向的空间中,最多拷贝num个字符。
  • 参数
    • destination:指向目标空间的指针。
    • source:指向源字符串的指针。
    • num:最大拷贝字符数(类型为size_t,通常是无符号整数)。
  • 返回值 :返回destination的起始地址。
  • 行为
    • 如果source字符串的长度小于num,则拷贝整个字符串(包括\0),并用\0填充剩余空间。
    • 如果source字符串的长度大于或等于num,则只拷贝前num个字符,且不会自动添加\0(需要手动处理)。
    • 源字符串不一定需要以\0结尾,但拷贝后目标字符串可能不完整。

7.2 比较strcpy和strncpy函数

  • strcpy函数

    • 功能:拷贝整个字符串直到遇到\0
    • 风险:如果目标空间不足,会导致缓冲区溢出(越界),引发未定义行为(如程序崩溃或安全漏洞)。
    • 示例:char dest[5]; strcpy(dest, "HelloWorld"); 会溢出,因为源字符串长度10 > 目标空间5。
    • 重点:不安全,因为它不检查目标空间大小。
  • strncpy函数

    • 功能:指定最大拷贝长度num,限制拷贝字符数。
    • 优势:更安全,因为开发者必须显式指定num,强制考虑目标空间大小。
    • 行为:
      • 如果num小于源字符串长度,拷贝num个字符,但不添加\0(目标字符串可能未终止)。
      • 如果num大于源字符串长度,拷贝整个字符串并填充\0到剩余空间。
    • 重点:避免了盲目溢出,但需要手动处理\0以确保字符串正确终止。

核心区别 :strcpy依赖于源字符串的\0,易导致溢出;strncpy通过num参数控制拷贝长度,减少了溢出风险,但增加了对\0的管理需求。

7.3 C语言代码示例

下面通过代码演示strncpy的使用,并列举两种场景:当拷贝字符数超过源字符串长度时(num > 源长度),和没有超过时(num <= 源长度)。重点展示\0的位置。

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    // 场景1: num 小于源字符串长度 (没有超过)
    char source1[] = "HelloWorld";  // 长度10 (包括\0)
    char dest1[10];  // 目标空间大小10
    size_t num1 = 5;  // 只拷贝5个字符
    
    strncpy(dest1, source1, num1);
    // 手动添加\0以确保字符串终止
    dest1[num1] = '\0';
    
    printf("场景1 (num <= 源长度):\n");
    printf("源字符串: %s\n", source1);
    printf("目标字符串: %s\n", dest1);  // 输出 "Hello"
    // \0位置: 在dest1[5]处 (索引从0开始)

    // 场景2: num 大于源字符串长度 (超过)
    char source2[] = "ABC";  // 长度4 (包括\0)
    char dest2[10];  // 目标空间大小10
    size_t num2 = 8;  // 拷贝8个字符
    
    strncpy(dest2, source2, num2);
    // 不需要手动添加\0,因为strncpy会自动填充
    
    printf("\n场景2 (num > 源长度):\n");
    printf("源字符串: %s\n", source2);
    printf("目标字符串: %s\n", dest2);  // 输出 "ABC"
    // \0位置: 在dest2[3]处(源字符串结束),剩余空间填充\0(dest2[4]到dest2[7]为\0)

    return 0;
}

代码解释

  • 场景1 (num <= 源长度)

    • num1 = 5,源字符串长度10。
    • strncpy拷贝前5个字符('H','e','l','l','o')到dest1
    • \0位置 :拷贝后dest1没有自动添加\0(因为num <= 源长度),所以手动在dest1[5]添加\0来终止字符串。输出为"Hello"。
    • 重点:如果不手动添加\0dest1可能不是有效字符串,导致后续操作出错。
  • 场景2 (num > 源长度)

    • num2 = 8,源字符串长度4(包括\0)。
    • strncpy拷贝整个源字符串('A','B','C','\0')到dest2
    • \0位置 :拷贝后dest2[3]是源字符串的\0,strncpy自动用\0填充剩余空间(dest2[4]dest2[7])。输出为"ABC"。
    • 重点:strncpy确保目标字符串以\0终止,并填充剩余空间。

7.4 安全考虑总结

  • 为什么strncpy更安全 :通过指定num,开发者必须评估目标空间大小(例如,num应小于或等于destination的大小)。这减少了缓冲区溢出的风险。
  • 使用建议
    • 总是确保num不大于destination的空间大小。
    • num小于源长度时,手动添加\0(如destination[num] = '\0';)。
    • 优先使用strncpy替代strcpy,尤其在处理用户输入或未知长度数据时。
  • 风险提示 :如果num设置不当(如大于目标空间),strncpy仍可能溢出;因此,结合sizeof(destination)来计算num是最佳实践。

strncpy函数在字符串操作中提供了更可控的拷贝机制,通过理解其行为和正确处理\0,可以显著提升代码安全性。


8. strncat函数的使⽤

strncat 函数详解

功能: 将源字符串 (source) 的最多 num 个字符 (包括 \0 之前的字符)追加 到目标字符串 (destination) 的末尾(覆盖目标字符串原有的终止符 \0),并在追加后的新字符串末尾自动添加一个新的终止符 \0

函数原型:

c 复制代码
char * strncat ( char * destination, const char * source, size_t num );

参数:

  • destination: 指向目标字符串(必须足够大以容纳追加后的内容,包括新的 \0)。
  • source: 指向要追加的源字符串。
  • num: 指定要从源字符串 source最多 追加的字符数(不包括 \0)。

返回值: 返回指向目标字符串 destination 的指针(即追加后的新字符串的起始地址)。


strcat vs strncat 核心区别

  1. 参数差异:

    • strcat 只有两个参数:destinationsource
    • strncat 有三个参数:destinationsourcenum。这个 num 参数是关键。
  2. 追加行为差异:

    • strcat: 它会一直追加 源字符串 source 的内容,直到遇到源字符串中的 \0 为止 。这意味着它会将源字符串的整个内容 (包括其 \0)都复制过去(覆盖目标原有的 \0),并在新的结尾处自动添加一个新的 \0。它完全依赖源字符串以 \0 结尾
    • strncat: 它只追加源字符串 source最多 num 个字符 (不包括 \0)。无论源字符串中是否有 \0,或者 \0 在什么位置,它都只追加 num 个字符(或更少,如果源字符串长度小于 num)。然后,它总是在最终结果的末尾添加一个新的 \0 。它不要求源字符串必须以 \0 结尾 (但源字符串通常应该以 \0 结尾)。
  3. 安全性与灵活性:

    • strcat不安全的 。如果源字符串很长或者目标字符串空间不足,strcat 会一直复制直到遇到 \0,这很容易导致缓冲区溢出(Buffer Overflow),覆盖目标缓冲区之后的内存,引发程序崩溃或安全漏洞。
    • strncat更安全的 。通过 num 参数,开发者可以显式控制 最多追加多少字符,从而避免缓冲区溢出 的风险。开发者需要确保 destination 有足够的剩余空间容纳追加的 num 个字符加上 新添加的 \0(即 destination 剩余空间至少为 num + 1)。同时,strncat 也更灵活,因为它可以处理不以 \0 结尾的字符数组(尽管这不是标准用法)。

\0 的位置处理详解

strncat 函数在追加完成后,总是在 新字符串的末尾添加一个新的 \0 。源字符串中可能存在的 \0 会被当作普通字符处理(如果它出现在前 num 个字符内)。

具体分两种情况讨论:

情况一:num 小于或等于 source\0 之前的有效字符数

  • strncat 会精确地复制源字符串 source 中的 num 个字符(不包括 \0)到 destination 原有 \0 的位置(覆盖这个 \0)。
  • 然后,在追加的 num 个字符之后,立即添加一个新的 \0
  • \0 的位置: 在目标字符串 destination 原有内容的末尾偏移 num 个字符处。

示例代码:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char dest[20] = "Hello"; // dest: ['H','e','l','l','o','\0', ...]
    char src[] = "World!";   // src:  ['W','o','r','l','d','!','\0']

    strncat(dest, src, 3); // 只追加 "Wor",共3个字符

    // 结果:dest 变成 "HelloWor"
    // 内存布局:['H','e','l','l','o','W','o','r','\0', ...]
    // 新添加的 \0 在 'r' 后面

    printf("Result: %s\n", dest); // 输出: HelloWor
    return 0;
}

情况二:num 大于 source\0 之前的有效字符数

  • strncat 会复制源字符串 source 中的所有字符,直到遇到源字符串中的 \0 (即使 num 指定得更大,复制也会在遇到 \0 时停止)。
  • 然后,在源字符串的 \0 之后(即目标字符串原有内容的末尾加上源字符串所有有效字符之后),立即添加一个新的 \0
  • \0 的位置: 在目标字符串 destination 原有内容的末尾偏移 strlen(source) 个字符处(因为复制了源的所有有效字符)。

示例代码:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char dest[20] = "Hello"; // dest: ['H','e','l','l','o','\0', ...]
    char src[] = "World!";   // src:  ['W','o','r','l','d','!','\0']

    strncat(dest, src, 10); // 想追加10个字符,但 src 只有6个有效字符 + \0

    // 实际行为:复制 src 的所有字符直到遇到 \0 ("World!"),共6个字符。
    // 结果:dest 变成 "HelloWorld!"
    // 内存布局:['H','e','l','l','o','W','o','r','l','d','!','\0', ...]
    // 新添加的 \0 在 '!' 后面

    printf("Result: %s\n", dest); // 输出: HelloWorld!
    return 0;
}

关键点总结

  1. num 的含义: 它限制了从 source最多 复制的\0 字符数
  2. \0 的添加: strncat 总是在 最终拼接后的字符串末尾添加一个新的 \0。这个行为是固定的。
  3. 源字符串的 \0
    • 如果 num 小于源字符串有效长度,源字符串中的 \0 不会被复制(它可能还在源字符串里,但不会被 strncat 处理)。
    • 如果 num 大于源字符串有效长度,复制会在遇到源字符串的 \0 时停止,并且这个源 \0 本身不会被复制到目标。
  4. 安全性: 使用 strncat 时,程序员必须 确保目标缓冲区 destination足够的剩余空间 。所需空间至少是:strlen(destination) + num + 1(原有长度 + 最多追加的字符数 + 1 给新的 \0)。这是防止缓冲区溢出的关键。
  5. 灵活性: strncat 可以安全地用于处理固定长度的字符数组(即使它们内部没有 \0),只要 num 不超过数组长度。而 strcat 必须要求源字符串以 \0 结尾。

安全使用 strncat 的示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    // 目标缓冲区,初始大小为 10
    char dest[10] = "Dest"; // 当前长度 4 (strlen("Dest")=4)
    char src[] = "SourceString"; // 长度 12

    // 计算 dest 剩余空间 (sizeof(dest) - strlen(dest) - 1)
    // sizeof(dest)=10, strlen(dest)=4, 剩余空间为 10 - 4 - 1 = 5
    // 这5个位置:1个给新 \0,剩下4个可以追加字符
    size_t available = sizeof(dest) - strlen(dest) - 1;

    // 安全追加:最多追加 available 个字符 (即4个)
    strncat(dest, src, available);

    // 结果:dest 变成 "DestSour" (4 + 4 = 8字符,加上末尾 \0)
    // 内存布局: ['D','e','s','t','S','o','u','r','\0', 剩余位置]
    printf("Result: %s\n", dest); // 输出: DestSour

    return 0;
}

在这个例子中,通过计算 available,我们确保了 strncat 不会超出 dest 的边界,从而避免了缓冲区溢出。


9. strncmp函数的使⽤

strncmp函数的使用详解

strncmp函数是C语言标准库中的一个字符串比较函数,用于比较两个字符串的内容,但可以指定最多比较的字符数(num)。这使其比strcmp函数更灵活和安全,特别是在处理可能不完整的字符串或避免缓冲区溢出时。下面我将逐步解释其功能、参数、返回值,并与strcmp进行对比,最后通过代码示例和表格展示不同场景。

9.1 strncmp函数的功能和参数

  • 函数原型int strncmp(const char *str1, const char *str2, size_t num);
    • str1str2:指向要比较的两个字符串的指针。
    • num:指定最多比较的字符个数(size_t类型,通常为无符号整数)。
  • 功能:逐字符比较str1和str2指向的字符串,但最多只比较num个字符。如果遇到'\0'(字符串结束符)或达到num个字符,则停止比较。
  • 返回值
    • 如果str1大于str2(按字典顺序),则返回一个大于0的数字(例如,1)。
    • 如果str1等于str2,则返回0。
    • 如果str1小于str2,则返回一个小于0的数字(例如,-1)。
      注意:返回值的具体数值由实现定义,但符号表示比较结果(>0>0>0、=0=0=0 或 <0<0<0)。

9.2 strncmp与strcmp的比较

  • strcmp函数int strcmp(const char *str1, const char *str2);
    • 比较整个字符串,直到遇到'\0'结束符。如果字符串较长,它会一直比较下去,可能导致不安全的行为(如访问无效内存)。
  • 关键区别
    • 参数不同:strncmp多了一个num参数,允许控制比较的最大长度。
    • 灵活性:strncmp可以比较任意长度的子串,而不受字符串实际长度的限制。
    • 安全性:strncmp通过限制比较长度,减少了缓冲区溢出的风险(例如,当字符串未正确以'\0'结尾时)。strcmp在遇到非终止字符串时可能引发未定义行为。

9.3 strncmp的优势

  • 更安全:指定num参数可以防止比较超出字符串的有效范围,避免潜在的安全漏洞。
  • 更灵活:适用于比较部分字符串,例如在解析固定长度数据或处理网络协议时。

9.4 代码示例

下面是一个简单的C程序,演示strncmp的使用。代码包括比较不同字符串的场景。

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    const char *str1 = "hello";
    const char *str2 = "world";
    const char *str3 = "hello";
    const char *str4 = "he";

    // 场景1:比较整个字符串(num超过实际长度)
    int result1 = strncmp(str1, str3, 10);  // num=10,但字符串只有5字符
    printf("比较 'hello' 和 'hello',num=10: 返回值 = %d\n", result1);  // 输出: 0(相等)

    // 场景2:比较部分字符串(num不超过实际长度)
    int result2 = strncmp(str1, str2, 3);    // 比较前3字符: "hel" vs "wor"
    printf("比较 'hello' 和 'world',num=3: 返回值 = %d\n", result2);  // 输出: 负数('e' < 'o'? 实际依赖ASCII)

    // 场景3:比较到'\0'位置
    int result3 = strncmp(str1, str4, 3);    // 比较前3字符: "hel" vs "he"(str4较短)
    printf("比较 'hello' 和 'he',num=3: 返回值 = %d\n", result3);  // 输出: 正数('l' > '\0'?)

    return 0;
}

代码解释

  • 场景1:num=10大于字符串长度(5),比较在'\0'处停止,返回0表示相等。
  • 场景2:num=3小于字符串长度,比较前3个字符,由于"hel"和"wor"不同('h' vs 'w'),返回负数。
  • 场景3 :num=3,但str4只有2个字符(以'\0'结尾),比较在str4的'\0'处停止,str1的第三个字符'l'大于'\0',返回正数。
    注意:返回值的具体数值可能因编译器而异,但符号表示顺序。

9.5 不同场景下的行为表格

以下表格展示了strncmp在不同情况下的行为,重点关注当比较长度(num)是否超过或不超过字符串的'\0'位置。'\0'位置指字符串的结束符位置。

情况描述 str1内容 str2内容 num值 返回值 解释
num 超过 '\0'位置(即num > 字符串长度) "abc"(长度3) "abc"(长度3) 5 0 比较最多5字符,但实际字符串以'\0'结束在位置3。比较在'\0'处停止,内容相同,返回0。
num 不超过 '\0'位置(即num ≤ 字符串长度) "apple"(长度5) "app"(长度3) 3 0 比较前3字符("app" vs "app"),内容相同,返回0。即使str2较短,比较在num=3处完成。
num 不超过 '\0'位置,但内容不同 "abc"(长度3) "abd"(长度3) 3 负数(例如-1) 比较前3字符:'a'='a', 'b'='b', 'c'<'d'(ASCII中99<100),所以返回小于0的数字。
num 超过 '\0'位置,且str2较短 "hello"(长度5) "he"(长度2) 4 正数(例如1) 比较最多4字符。str2在位置2有'\0',比较在str2的'\0'处停止。str1的第三个字符'l' > '\0'(因为'\0'的ASCII为0),所以返回大于0的数字。

表格说明

  • 在"num超过'\0'位置"时,比较提前在第一个'\0'处停止,忽略多余的num。
  • 在"num不超过'\0'位置"时,比较严格按num个字符进行,即使字符串更长。
  • 返回值基于字符的ASCII值比较:如果字符不同,较早的不等字符决定结果(c1−c2c1 - c2c1−c2,其中c1c1c1和c2c2c2是字符的整数值)。

总结

strncmp函数通过引入num参数,提供了比strcmp更高的灵活性和安全性。它允许精确控制比较长度,避免了潜在的内存访问错误。在实际编程中,推荐使用strncmp来处理不确定长度的字符串,特别是在安全敏感的场合。记住:总是检查num值以确保它不超过缓冲区大小,并结合字符串终止符'\0'来设计逻辑。


10. strstr 的使用和模拟实现

函数原型

c 复制代码
char *strstr(const char *str1, const char *str2);

功能
strstr 函数用于在字符串 str1 中查找子字符串 str2 的第一次出现位置。如果找到,返回指向该位置的指针;否则返回 NULL。使用前需包含头文件 <string.h>

参数

  • str1:指向被查找的字符串的指针。
  • str2:指向要查找的子字符串的指针。

返回值

  • 成功:返回 str2str1 中首次出现的地址。
  • 失败:返回 NULL

10.1 代码演示

以下示例演示 strstr 的基本用法:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    const char *str1 = "Hello, world!";
    const char *str2 = "world";
    
    char *result = strstr(str1, str2);
    if (result != NULL) {
        printf("子字符串找到位置: %s\n", result); // 输出: world!
    } else {
        printf("子字符串未找到\n");
    }
    return 0;
}

输出:

复制代码
子字符串找到位置: world!

重点

  • strstr 区分大小写(例如,"World" 不会被匹配)。
  • 如果 str2 是空字符串(""),则返回 str1 的起始地址。
  • 时间复杂度平均为 O(n)O(n)O(n),最坏情况 O(n×m)O(n \times m)O(n×m),其中 nnn 是 str1 长度,mmm 是 str2 长度。

10.2 strstr 的模拟实现

以下是 strstr 的手动实现(模拟):

c 复制代码
#include <stdio.h>

char *my_strstr(const char *str1, const char *str2) {
    if (*str2 == '\0') { // 如果 str2 是空字符串
        return (char *)str1;
    }
    
    const char *p1 = str1;
    while (*p1 != '\0') {
        const char *p1_start = p1;
        const char *p2 = str2;
        
        while (*p1 != '\0' && *p2 != '\0' && *p1 == *p2) {
            p1++;
            p2++;
        }
        
        if (*p2 == '\0') { // 子字符串匹配完成
            return (char *)p1_start;
        }
        
        p1 = p1_start + 1; // 移动到下一个位置
    }
    
    return NULL; // 未找到
}

int main() {
    const char *str1 = "C programming";
    const char *str2 = "prog";
    
    char *result = my_strstr(str1, str2);
    if (result != NULL) {
        printf("模拟实现找到位置: %s\n", result); // 输出: programming
    } else {
        printf("未找到\n");
    }
    return 0;
}

模拟实现解析

  1. 检查 str2 是否为空,是则返回 str1
  2. 遍历 str1,逐个字符与 str2 比较。
  3. 如果 str2 的所有字符匹配,返回起始地址。
  4. 时间复杂度与标准实现类似。

11. strtok 函数的使用

函数原型

c 复制代码
char *strtok(char *str, const char *delim);

功能
strtok 用于根据分隔符 delim 将字符串 str 拆分为多个子字符串。它会修改原始字符串,将分隔符替换为 \0(字符串结束符)。使用前需包含 <string.h>

参数

  • str:首次调用时传入待分割的字符串;后续调用传入 NULL,表示继续分割同一字符串。
  • delim:包含所有分隔符的字符串(每个字符独立视为分隔符)。

返回值

  • 成功:返回当前子字符串的指针。
  • 失败:返回 NULL(无更多子字符串)。

使用步骤

  1. 首次调用:传入 strdelim
  2. 后续调用:传入 NULL 和相同 delim
  3. 结束:当返回 NULL 时,分割完成。

11.1 代码演示

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "apple,banana;cherry";
    const char *delim = ",;";
    
    // 首次调用
    char *token = strtok(str, delim);
    while (token != NULL) {
        printf("子字符串: %s\n", token);
        token = strtok(NULL, delim); // 后续调用
    }
    return 0;
}

输出:

复制代码
子字符串: apple
子字符串: banana
子字符串: cherry

11.2 注意事项

  1. 破坏性操作strtok 直接修改原始字符串(替换分隔符为 \0)。如需保留原字符串,应先拷贝:

    c 复制代码
    char original[] = "data1;data2";
    char copy[50];
    strcpy(copy, original); // 先拷贝
    char *token = strtok(copy, ";");
  2. 连续分隔符:多个连续分隔符被视为单个分隔符,不返回空字符串:

    c 复制代码
    char str[] = "a,,b";
    strtok(str, ","); // 返回 "a",跳过连续分隔符
  3. 空指针处理 :如果首次调用传入 NULL,行为未定义。确保首次调用传入有效字符串。

  4. '\0' 位置分析strtok 在找到分隔符时将其替换为 \0,下表展示不同情况:

    情况 原始字符串 分隔符 替换后效果 子字符串 备注
    标准情况 "one,two" "," "one\0two" "one" 和 "two" 分隔符被替换为 \0
    连续分隔符 "a,b" "," "a\0\0b" "a" 和 "b" 跳过连续分隔符,不产生空字符串
    字符串结尾 "end;" ";" "end\0" "end" 结尾分隔符被替换,子字符串以 \0 结束
    无分隔符 "hello" "," 无变化 "hello" 返回整个字符串,不插入额外 \0

    解释

    • "存放数字超过" 可能指缓冲区溢出,但 strtok 不分配内存;它操作现有字符串。确保字符串以 \0 结尾,否则行为未定义。
    • "没有超过" 时,\0 在分隔符位置插入;"超过" 时(如字符串未正确终止),可能导致未定义行为。

12. strerror 函数的使用

函数原型

c 复制代码
char *strerror(int errnum);

功能
strerror 将错误码 errnum 转换为对应的错误信息字符串,并返回该字符串的首地址。主要用于标准库函数错误(如文件操作)。使用前需包含 <string.h><errno.h>

参数

  • errnum:错误码,通常传递全局变量 errno 的值(库函数失败时设置)。

返回值

  • 成功:返回错误信息字符串的指针。
  • 失败:无显式失败(但无效 errnum 可能返回未知错误字符串)。

12.1 代码演示

c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

int main() {
    FILE *file = fopen("nonexistent.txt", "r");
    if (file == NULL) {
        printf("错误码: %d\n", errno); // 输出错误码
        printf("错误信息: %s\n", strerror(errno)); // 输出错误字符串
    }
    return 0;
}

输出示例(假设文件不存在):

复制代码
错误码: 2
错误信息: No such file or directory

12.2 perror 函数

函数原型

c 复制代码
void perror(const char *str);

功能
perror 直接打印错误信息:先打印参数 str,后跟冒号、空格和 strerror(errno) 的内容。简化错误处理:

c 复制代码
#include <stdio.h>
#include <errno.h>

int main() {
    FILE *file = fopen("invalid.txt", "r");
    if (file == NULL) {
        perror("文件打开失败"); // 输出: 文件打开失败: No such file or directory
    }
    return 0;
}

重点

  • strerrorperror 依赖于 errno,确保在库函数失败后立即使用(errno 可能被后续调用覆盖)。
  • 错误码范围由系统定义(如 POSIX 标准),常见错误码如 EACCES(权限不足)。
相关推荐
Upsy-Daisy4 小时前
AI Agent 项目学习笔记(七):RAG 高级扩展——过滤检索、PgVector 与云知识库
人工智能·笔记·学习
智者知已应修善业5 小时前
【51单片机LED闪烁10次数码管显示0-9】2023-12-14
c++·经验分享·笔记·算法·51单片机
智者知已应修善业5 小时前
【51单片机2按键控制1个敞亮LED灯闪烁和熄灭】2023-11-3
c++·经验分享·笔记·算法·51单片机
AI科技星6 小时前
第二章 平行素数对网格:矩形→等腰梯形拓扑变换(完整公理终稿)
c语言·开发语言·线性代数·算法·量子计算·agi
w2018006 小时前
二年级下册语文看图写话作文:蛋壳的奇妙之旅
笔记
daanpdf6 小时前
初三中考英语作文模板万能句型及范文大全电子版
笔记
nnsix7 小时前
设计模式 - 建造者模式 笔记
笔记·设计模式·建造者模式
社交怪人8 小时前
【歌手大奖赛】信息学奥赛一本通C语言解法(题号2072)
c语言·算法
穗余8 小时前
2026 AI x Web3 School共学营笔记-Day1
人工智能·笔记·web3