MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言字符串函数要注意的关键点

2023-02-037.8k 阅读

字符串基础概念回顾

在深入探讨 C 语言字符串函数的关键点之前,我们先来回顾一下字符串在 C 语言中的基本概念。在 C 语言中,字符串实际上是以空字符 '\0' 结尾的字符数组。例如:

char str1[] = "Hello";
// 等价于
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'};

这里 str1str2 定义了两个相同的字符串,'\0' 起到了标识字符串结束的作用。这个空字符虽然不显示,但对于字符串的处理至关重要,许多字符串函数都依赖它来确定字符串的边界。

常用字符串函数及其关键点

strlen 函数

strlen 函数用于计算字符串的长度,不包括字符串末尾的空字符 '\0'。其函数原型为:

size_t strlen(const char *str);

下面是一个简单的使用示例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    size_t len = strlen(str);
    printf("字符串长度为: %zu\n", len);
    return 0;
}

关键点

  1. 空字符的影响strlen 从传入的字符串指针开始,逐个检查字符,直到遇到 '\0' 为止。所以如果字符串没有正确以 '\0' 结尾,strlen 会继续访问内存,直到遇到一个值为 0 的字节,这可能导致未定义行为,例如访问越界。
// 错误示例,字符串未以 '\0' 结尾
char badStr[] = {'H', 'e', 'l', 'l', 'o'};
size_t badLen = strlen(badStr); // 未定义行为
  1. 返回类型strlen 的返回类型是 size_t,这是一个无符号整数类型。在进行比较或者运算时,需要注意与有符号整数类型的混合使用,否则可能会出现意想不到的结果。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "short";
    char str2[] = "very very long string";
    if (strlen(str1) - strlen(str2) > 0) {
        printf("str1 更长\n");
    } else {
        printf("str2 更长\n");
    }
    return 0;
}

在这个例子中,strlen(str1) - strlen(str2) 的结果是一个无符号整数,由于 str2 更长,相减结果为负数,但在无符号整数运算中,这个负数会被转换为一个很大的正数,导致条件判断结果与预期不符。

strcpy 函数

strcpy 函数用于将一个字符串复制到另一个字符串中。其函数原型为:

char *strcpy(char *dest, const char *src);

示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Hello";
    char dest[20];
    strcpy(dest, src);
    printf("复制后的字符串: %s\n", dest);
    return 0;
}

关键点

  1. 目标缓冲区大小strcpy 不会检查目标缓冲区是否足够大来容纳源字符串。如果目标缓冲区过小,会导致缓冲区溢出,这是一个严重的安全漏洞。例如:
// 错误示例,目标缓冲区过小
char smallDest[5];
char largeSrc[] = "This is a long string";
strcpy(smallDest, largeSrc); // 缓冲区溢出
  1. 源字符串必须以 '\0' 结尾strcpy 会从源字符串的起始位置开始复制字符,直到遇到 '\0'。如果源字符串没有正确以 '\0' 结尾,strcpy 会一直复制,直到在内存中遇到 '\0',这同样会导致未定义行为。
  2. 返回值strcpy 返回目标字符串的指针 dest,这使得可以进行链式调用。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[20];
    char str2[] = "Hello";
    char str3[] = ", World!";
    char *result = strcpy(strcpy(str1, str2), str3);
    printf("最终结果: %s\n", result);
    return 0;
}

在这个例子中,先将 str2 复制到 str1,然后再将 str3 复制到 str1str2 之后的位置,实现了字符串的拼接。

strncpy 函数

为了避免 strcpy 可能出现的缓冲区溢出问题,strncpy 函数被引入。其函数原型为:

char *strncpy(char *dest, const char *src, size_t n);

strncpy 最多从源字符串 src 中复制 n 个字符到目标字符串 dest 中。示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Hello, World!";
    char dest[10];
    strncpy(dest, src, sizeof(dest));
    dest[sizeof(dest) - 1] = '\0'; // 手动添加 '\0'
    printf("复制后的字符串: %s\n", dest);
    return 0;
}

关键点

  1. 复制长度strncpy 不会自动在目标字符串末尾添加 '\0',除非源字符串的长度小于 n。如果源字符串长度大于或等于 n,目标字符串将不会以 '\0' 结尾,这可能导致后续处理字符串的函数出现错误。所以在使用 strncpy 后,通常需要手动在目标字符串末尾添加 '\0',如上述示例代码所示。
  2. 填充字符:如果源字符串长度小于 nstrncpy 会用 '\0' 填充目标字符串,直到复制了 n 个字符。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Hi";
    char dest[10];
    strncpy(dest, src, sizeof(dest));
    for (int i = 0; i < sizeof(dest); i++) {
        printf("%d ", dest[i]);
    }
    return 0;
}

在这个例子中,src 长度为 2,而 dest 大小为 10,strncpy 复制完 Hi 后,会用 '\0' 填充剩余的 8 个位置。

strcat 函数

strcat 函数用于将一个字符串追加到另一个字符串的末尾。其函数原型为:

char *strcat(char *dest, const char *src);

示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char dest[20] = "Hello";
    char src[] = ", World!";
    strcat(dest, src);
    printf("拼接后的字符串: %s\n", dest);
    return 0;
}

关键点

  1. 目标缓冲区大小strcat 同样不会检查目标缓冲区是否有足够的空间来容纳源字符串。如果目标缓冲区空间不足,会导致缓冲区溢出。例如:
// 错误示例,目标缓冲区空间不足
char smallDest[10] = "Hello";
char largeSrc[] = ", this is a very long addition";
strcat(smallDest, largeSrc); // 缓冲区溢出
  1. 目标字符串必须以 '\0' 结尾strcat 首先会找到目标字符串的 '\0' 位置,然后从这个位置开始将源字符串复制过来。如果目标字符串不以 '\0' 结尾,strcat 会在内存中错误的位置开始复制,导致未定义行为。
  2. 返回值strcat 返回目标字符串的指针 dest,这也支持链式调用。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[30] = "Hello";
    char str2[] = ", ";
    char str3[] = "World!";
    char *result = strcat(strcat(str1, str2), str3);
    printf("最终结果: %s\n", result);
    return 0;
}

strncat 函数

类似于 strncpystrcpy 的改进,strncat 用于安全地追加字符串。其函数原型为:

char *strncat(char *dest, const char *src, size_t n);

strncat 最多从源字符串 src 中追加 n 个字符到目标字符串 dest 的末尾。示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char dest[20] = "Hello";
    char src[] = ", World!";
    strncat(dest, src, sizeof(dest) - strlen(dest) - 1);
    printf("拼接后的字符串: %s\n", dest);
    return 0;
}

关键点

  1. 追加长度strncat 最多追加 n 个字符,或者直到遇到源字符串的 '\0',以先发生者为准。追加完成后,目标字符串会以 '\0' 结尾,不需要手动添加。
  2. 目标缓冲区大小计算:在使用 strncat 时,需要确保目标缓冲区有足够的空间来容纳追加的字符。通常可以通过 sizeof(dest) - strlen(dest) - 1 来计算剩余可用空间,其中 1 是为了给 '\0' 保留位置。例如上述示例代码中,通过这种方式来避免缓冲区溢出。

strcmp 函数

strcmp 函数用于比较两个字符串。其函数原型为:

int strcmp(const char *str1, const char *str2);

它会逐个比较两个字符串的字符,直到遇到不同的字符或者到达某个字符串的末尾。示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Hello";
    char str2[] = "Hello";
    char str3[] = "World";
    int result1 = strcmp(str1, str2);
    int result2 = strcmp(str1, str3);
    if (result1 == 0) {
        printf("str1 和 str2 相等\n");
    }
    if (result2 < 0) {
        printf("str1 小于 str3\n");
    } else if (result2 > 0) {
        printf("str1 大于 str3\n");
    }
    return 0;
}

关键点

  1. 比较规则:如果两个字符串完全相同,strcmp 返回 0;如果 str1 小于 str2(按字典序),返回一个小于 0 的值;如果 str1 大于 str2,返回一个大于 0 的值。比较是基于字符的 ASCII 码值。
  2. 空字符的作用:比较过程中遇到 '\0' 时,如果两个字符串在 '\0' 之前的字符都相同,那么较短的字符串被认为小于较长的字符串。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Hello";
    char str2[] = "Hello, World!";
    int result = strcmp(str1, str2);
    if (result < 0) {
        printf("str1 小于 str2\n");
    }
    return 0;
}

在这个例子中,str1 较短,遇到 '\0' 后,比较结束,str1 被认为小于 str2

strncmp 函数

strncmp 函数用于比较两个字符串的前 n 个字符。其函数原型为:

int strncmp(const char *str1, const char *str2, size_t n);

示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Hello, World!";
    char str2[] = "Hello, Universe!";
    int result = strncmp(str1, str2, 7);
    if (result == 0) {
        printf("前 7 个字符相同\n");
    }
    return 0;
}

关键点

  1. 比较长度strncmp 只比较两个字符串的前 n 个字符,而不是整个字符串。如果在比较完 n 个字符之前两个字符串就已经不同,那么根据不同字符的 ASCII 码值返回相应的比较结果。
  2. 字符串长度小于 n 的情况:如果某个字符串的长度小于 n,则只比较到该字符串的 '\0' 为止。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Hello";
    char str2[] = "Hello, World!";
    int result = strncmp(str1, str2, 10);
    if (result < 0) {
        printf("str1 小于 str2\n");
    }
    return 0;
}

在这个例子中,str1 长度小于 10,比较到 str1'\0' 后,str1 被认为小于 str2

strstr 函数

strstr 函数用于在一个字符串中查找另一个字符串第一次出现的位置。其函数原型为:

char *strstr(const char *haystack, const char *needle);

示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char haystack[] = "Hello, World! This is a test.";
    char needle[] = "World";
    char *result = strstr(haystack, needle);
    if (result!= NULL) {
        printf("找到了子字符串,位置: %ld\n", result - haystack);
    } else {
        printf("未找到子字符串\n");
    }
    return 0;
}

关键点

  1. 查找逻辑strstrhaystack 的起始位置开始,逐个字符匹配 needle。如果找到完全匹配的子字符串,则返回指向该子字符串在 haystack 中起始位置的指针;如果未找到,则返回 NULL
  2. 空字符串情况:如果 needle 是一个空字符串,strstr 会返回 haystack 的指针,因为空字符串被认为存在于任何字符串的起始位置。

strtok 函数

strtok 函数用于将字符串分割成一个个标记(token)。其函数原型为:

char *strtok(char *str, const char *delim);

示例代码如下:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello,World;This is a test";
    char *token = strtok(str, ",; ");
    while (token!= NULL) {
        printf("标记: %s\n", token);
        token = strtok(NULL, ",; ");
    }
    return 0;
}

关键点

  1. 分割逻辑strtok 会在 str 中查找由 delim 中的字符分隔的标记。第一次调用时,传入要分割的字符串 str;后续调用时,为了继续从上次分割的位置往后分割,需要传入 NULLstrtok 会修改原始字符串,将分隔符替换为 '\0'
  2. 保存状态strtok 内部使用了静态变量来保存分割的状态,这意味着它不是线程安全的。在多线程环境下使用 strtok 可能会导致数据竞争问题。如果需要在多线程环境中进行字符串分割,可以考虑使用 strtok_r 函数,它允许通过用户提供的缓冲区来保存状态。

总结与实践建议

通过对上述常见 C 语言字符串函数的关键点分析,我们可以看到在使用这些函数时,需要特别注意缓冲区溢出、字符串是否以 '\0' 结尾以及函数返回值的正确处理等问题。在实际编程中,为了提高代码的安全性和可靠性,建议尽量使用更安全的函数变体,如 strncpystrncat 等,并在使用前仔细检查缓冲区大小。同时,对于字符串的操作要养成良好的习惯,确保字符串的完整性和正确性,避免出现未定义行为。通过不断的实践和经验积累,能够更加熟练和准确地使用 C 语言字符串函数,编写出高质量的代码。

希望本文对您理解 C 语言字符串函数的关键点有所帮助,祝您在 C 语言编程中取得更好的成果。

以上内容围绕 C 语言字符串函数展开,详细阐述了各函数的关键点并辅以代码示例,通过对这些内容的学习,相信能让开发者在使用字符串函数时更加得心应手,编写出健壮的代码。同时,建议开发者在实际项目中不断实践,加深对这些知识的理解和运用。