C语言字符串完全入门教程 – wiki基地


C语言字符串完全入门教程

欢迎来到C语言的世界!字符串是编程中最常见也是最重要的数据类型之一。然而,与许多现代高级语言不同,C语言没有内置的、可以直接操作的“字符串”类型。在C语言中,字符串实际上是字符数组,并且遵循一套特定的约定。正是这些特性使得C语言的字符串既强大又灵活,但也伴随着一些需要格外注意的细节和陷阱。

本教程旨在帮助你彻底理解C语言字符串的本质、操作方法以及常见的注意事项,带你从入门到熟练掌握。

我们将涵盖以下内容:

  1. C语言字符串的本质:字符数组与空终止符
  2. 字符串的声明与初始化
  3. 字符串的输入与输出
  4. C标准库 <string.h>:字符串常用操作函数详解
    • 计算长度:strlen()
    • 复制:strcpy()strncpy()
    • 连接:strcat()strncat()
    • 比较:strcmp()strncmp()
    • 查找:strchr(), strrchr(), strstr()
  5. 字符串与指针:深入理解其关系
  6. 字符串的常见问题与陷阱
  7. 总结与实践建议

准备好了吗?让我们开始吧!

1. C语言字符串的本质:字符数组与空终止符

在C语言中,字符串被定义为以空字符(Null Character, \0)结尾的字符序列

这意味着:

  • 字符串不是一个独立的数据类型,它存储在字符数组中。
  • 字符串的结束位置由一个特殊的字符 \0 标记。这个 \0 字符的ASCII值为0。
  • 在处理字符串时,C语言的许多函数都会依赖于 \0 来判断字符串的结束。

举个例子:

字符串 “Hello” 在内存中实际上是这样存储的:

‘H’ ‘e’ ‘l’ ‘l’ ‘o’ ‘\0’

这是一个包含6个字符的序列:’H’, ‘e’, ‘l’, ‘l’, ‘o’, 和 ‘\0’。虽然我们通常认为 “Hello” 有5个字符,但在C语言中存储它需要至少6个字节(每个字符占1个字节,包括 \0)。

核心要点:空终止符 \0 是C语言字符串的灵魂! 没有它,字符数组就只是字符数组,而不是字符串。任何C字符串处理函数在遇到 \0 时都会停止操作。

2. 字符串的声明与初始化

声明一个字符串,本质上就是声明一个字符数组。

方法一:声明一个固定大小的字符数组

c
char str[20]; // 声明一个可容纳最多19个字符(外加一个\0)的数组

这里声明了一个名为 str 的字符数组,它可以存储20个字符。这意味着它可以存储一个长度不超过19的字符串(因为最后一个位置要留给 \0)。刚声明时,数组中的内容是未定义的(垃圾值),它还不是一个有效的字符串。

方法二:声明并初始化字符数组

你可以使用字符串字面量(string literal)来初始化字符数组。字符串字面量是用双引号 " 括起来的字符序列,例如 "Hello, world!"

c
char message[50] = "Hello, C language!"; // 初始化一个字符数组

当你使用字符串字面量初始化字符数组时,C编译器会自动在字面量的末尾添加 \0。上面的例子中,"Hello, C language!" 包含18个字符,编译器会自动在末尾添加一个 \0,所以总共是19个字符。数组 message 的大小是50,足够容纳这个字符串。

方法三:让编译器自动确定数组大小

如果你不指定数组大小,编译器会根据初始化字符串字面量的长度(包括 \0)来确定数组大小。

c
char greeting[] = "Hi"; // 编译器会创建一个大小为 3 的数组 ('H', 'i', '\0')

这种方法非常方便,但需要注意,数组的大小一旦确定就不能改变。

方法四:逐个字符初始化

你也可以像初始化普通数组一样,逐个字符地初始化。但务必手动添加 \0

c
char city[] = {'B', 'e', 'i', 'j', 'i', 'n', 'g', '\0'}; // 一个长度为 8 的字符串

如果我们忘了添加 \0

c
char not_a_string[] = {'A', 'B', 'C'}; // 这不是一个有效的C字符串!

not_a_string 只是一个包含 ‘A’, ‘B’, ‘C’ 三个字符的数组。如果尝试用 %s 格式符打印它,或者使用 strlen 等函数处理它,程序可能会一直读到内存中某个随机的 \0 才会停止,导致输出乱码甚至程序崩溃。

重要区别:字符数组 vs. 字符串字面量作为指针

除了用字符数组声明字符串外,你还可以使用字符指针指向字符串字面量:

c
char *name = "Alice"; // char pointer pointing to a string literal

这里 name 是一个 char 类型的指针,它指向存储在内存中某个位置的字符串字面量 "Alice" 的第一个字符 ‘A’。

区别

  • char array[] = "..."array 是一个数组名,它代表了数组的首地址。数组的内容存储在栈上(通常),并且是可修改的
  • char *pointer = "..."pointer 是一个指针变量,它存储了字符串字面量(通常存储在程序的只读数据段)的首地址。这个指针可以指向别处,但它所指向的那个字符串字面量本身是不可修改的。尝试修改 *pointer 指向的内容会导致未定义行为(Undefined Behavior),通常表现为程序崩溃(段错误)。

所以,如果你需要一个可以修改内容的字符串,请使用 char array[] 的方式声明。如果你只是想指向一个固定的字符串常量,可以使用 char *pointer = "..."

3. 字符串的输入与输出

C语言提供了多种方式来处理字符串的输入和输出。

输出字符串:

  • printf("%s", str);:使用 %s 格式符打印字符串。printf 会从 str 指向的地址开始,一直打印字符直到遇到 \0
  • puts(str);:打印字符串,并在末尾自动添加一个换行符 \nputs 也依赖于 \0 来确定字符串的结束。

“`c

include

int main() {
char str[] = “Hello, world!”;
printf(“%s\n”, str); // Output: Hello, world!
puts(str); // Output: Hello, world! (followed by a newline)
return 0;
}
“`

putsprintf("%s\n", str) 效率略高,因为它不需要解析格式串。但 printf 更灵活,可以结合其他文本和变量一起输出。

输入字符串:

从标准输入(键盘)读取字符串时需要特别小心,主要涉及缓冲区溢出问题。

  • scanf("%s", str);:使用 %s 格式符读取字符串。scanf 会读取非空白字符序列,并在遇到空白字符(空格、制表符、换行符)时停止。它会自动在读取的字符末尾添加 \0

    危险! scanf("%s", str) 的问题在于,它不知道 str 数组有多大,如果输入的字符串长度超过了 str 数组的容量(包括 \0 的空间),就会发生缓冲区溢出,写入到数组边界以外的内存区域,导致程序错误或安全漏洞。

    “`c

    include

    int main() {
    char buffer[10]; // 只能安全存储最多 9 个字符的字符串
    printf(“Enter a word: “);
    scanf(“%s”, buffer); // 如果输入超过 9 个字符,这里会出问题!
    printf(“You entered: %s\n”, buffer);
    return 0;
    }
    “`

  • gets(str);:读取一行输入直到遇到换行符或文件结束符,并将换行符替换为 \0

    极度危险! gets 同样不知道目标缓冲区的大小,没有任何办法防止缓冲区溢出。在任何现代C代码中都不应该使用 gets 函数!它已经被C11标准移除。

  • fgets(buffer, size, stdin);:从指定的输入流(这里是 stdin,标准输入)读取最多 size-1 个字符到 buffer 中,或者直到遇到换行符或文件结束符。它会保留读取到的换行符(如果读取到了),并在末尾添加 \0

    这是读取一行字符串的更安全的方式,因为它限制了读取的字符数量,防止了缓冲区溢出。

    “`c

    include

    int main() {
    char buffer[20]; // 可以安全存储最多 19 个字符的字符串(包括 \0)
    printf(“Enter a line: “);
    // 从标准输入读取最多 19 个字符到 buffer 中
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
    printf(“You entered: %s”, buffer);
    } else {
    printf(“Error reading input.\n”);
    }
    return 0;
    }
    “`

    使用 fgets 时需要注意:
    1. 它可能会包含末尾的换行符 \n。如果需要去除换行符,可以手动处理:
    c
    // 去除 fgets 读取到的换行符
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len-1] == '\n') {
    buffer[len-1] = '\0';
    }

    2. 如果输入超出了 size-1 个字符,fgets 只会读取前 size-1 个,剩下的会留在输入缓冲区中,影响后续的输入操作。可能需要额外的代码来清空输入缓冲区。

总结输入方法: 优先使用 fgets 来读取一行字符串,并根据需要去除换行符。对于读取单个“单词”(非空白字符序列),如果能确定最大长度,可以使用 scanf 的宽度限定符 %Ns (例如 scanf("%9s", buffer); ),但这仍然可能留下输入缓冲区中的剩余字符,并且不如 fgets 灵活。永远不要使用 gets

4. C标准库 <string.h>:字符串常用操作函数详解

C标准库提供了 <string.h> 头文件,其中包含了一系列用于操作字符串的函数。使用这些函数可以方便地进行字符串的长度计算、复制、连接、比较、查找等操作。

使用这些函数前,务必包含头文件:#include <string.h>

4.1 计算长度:strlen()

  • 功能: 计算字符串的长度(不包括末尾的 \0)。
  • 函数签名: size_t strlen(const char *s);
  • 参数: s – 指向待计算长度的字符串的指针。
  • 返回值: 字符串的长度(类型为 size_t,通常是无符号整型)。
  • 注意: strlen 从给定的地址开始查找 \0,因此,如果传入的不是一个有效的C字符串(即没有 \0 终止),它会一直读下去,导致错误甚至崩溃。

“`c

include

include

int main() {
char str[] = “C Programming”;
size_t len = strlen(str);
printf(“The length of \”%s\” is %zu\n”, str, len); // Output: The length of “C Programming” is 13
return 0;
}
“`

4.2 复制字符串:strcpy()strncpy()

  • strcpy()

    • 功能: 将源字符串复制到目标地址。
    • 函数签名: char *strcpy(char *dest, const char *src);
    • 参数: dest – 目标地址(字符数组);src – 源字符串。
    • 返回值: dest 的值(即目标地址)。
    • 危险! strcpy 不检查目标缓冲区的大小。如果 dest 的空间不足以容纳 src 字符串(包括 \0),就会发生缓冲区溢出

    “`c

    include

    include

    int main() {
    char src[] = “Hello”;
    char dest[10]; // 目标缓冲区大小为 10
    strcpy(dest, src); // src (“Hello\0”) 需要 6 个字节,dest 有 10 个,安全
    printf(“Copied string: %s\n”, dest); // Output: Copied string: Hello

    char long_src[] = "This is a very long string.";
    char small_dest[10]; // 目标缓冲区大小为 10
    // strcpy(small_dest, long_src); // <-- 危险!long_src 太长,会导致缓冲区溢出!
    // printf("Copied string: %s\n", small_dest);
    return 0;
    

    }
    “`

  • strncpy()

    • 功能: 将源字符串的前 n 个字符复制到目标地址。
    • 函数签名: char *strncpy(char *dest, const char *src, size_t n);
    • 参数: dest – 目标地址;src – 源字符串;n – 要复制的最大字符数。
    • 返回值: dest 的值。
    • 注意: strncpy 是为了处理定长缓冲区而设计的,但它的行为有点特殊:
      1. 它最多复制 n 个字符。
      2. 如果 src 的长度小于 n,剩余的空间会用 \0 填充。
      3. 如果 src 的长度大于或等于 nstrncpy 只会复制前 n 个字符, 不会 dest 的末尾添加 \0 这意味着如果 src 刚好或超过 n 长度,dest 可能不是一个有效的C字符串。

    “`c

    include

    include

    int main() {
    char src[] = “World”;
    char dest1[10];
    strncpy(dest1, src, sizeof(dest1) – 1); // 复制最多 9 个字符
    dest1[sizeof(dest1) – 1] = ‘\0’; // 手动确保 null 终止(安全做法)
    printf(“Copied (strncpy, safe): %s\n”, dest1); // Output: Copied (strncpy, safe): World

    char src2[] = "abcdefghijklmn"; // 14 chars
    char dest2[10]; // Size 10
    strncpy(dest2, src2, sizeof(dest2)); // 复制 10 个字符
    // dest2 的内容是 "abcdefghij"。它没有以 
    char src2[] = "abcdefghijklmn"; // 14 chars
    char dest2[10]; // Size 10
    strncpy(dest2, src2, sizeof(dest2)); // 复制 10 个字符
    // dest2 的内容是 "abcdefghij"。它没有以 \0 结尾!
    // printf("Copied (strncpy, potentially unsafe): %s\n", dest2); // <-- 危险!可能会打印乱码
    return 0;
    
    结尾! // printf("Copied (strncpy, potentially unsafe): %s\n", dest2); // <-- 危险!可能会打印乱码 return 0;

    }
    ``
    **安全建议:** 使用
    strncpy复制字符串时,总是将n设置为目标缓冲区大小减1,并在复制后**手动**在目标缓冲区的末尾添加\0,以确保其是有效的C字符串。或者使用更现代、更安全的函数(如 POSIX 标准中的strcpy_s或 C11 的memcpy_s结合长度检查),但这些函数并非所有编译器都支持。最通用的做法是**先检查源字符串长度,再使用strcpystrncpy` 结合手动终止。**

4.3 连接字符串:strcat()strncat()

  • strcat()

    • 功能: 将源字符串连接到目标字符串的末尾。
    • 函数签名: char *strcat(char *dest, const char *src);
    • 参数: dest – 目标字符串(连接后结果存储在这里,需要有足够的空间);src – 源字符串。
    • 返回值: dest 的值。
    • 危险! strcat 不检查目标缓冲区是否有足够的空间容纳连接后的字符串。它从 dest 中找到 \0 的位置,然后从那里开始复制 src 的内容(包括 src\0)。如果空间不足,就会发生缓冲区溢出

    “`c

    include

    include

    int main() {
    char dest[50] = “Hello”;
    char src[] = ” World!”;
    strcat(dest, src); // dest 原长 5,src 长 7(包括 ! 和 \0),总长 12 + \0 = 13。dest 50 空间足够。
    printf(“Concatenated string: %s\n”, dest); // Output: Concatenated string: Hello World!

    char small_dest[10] = "Short"; // 原长 5 + 
    char small_dest[10] = "Short"; // 原长 5 + \0 = 6 字节
    char long_src[] = " and Long"; // 长 9 + \0 = 10 字节
    // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节!
    // printf("Concatenated string: %s\n", small_dest);
    return 0;
    
    = 6 字节 char long_src[] = " and Long"; // 长 9 +
    char small_dest[10] = "Short"; // 原长 5 + \0 = 6 字节
    char long_src[] = " and Long"; // 长 9 + \0 = 10 字节
    // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节!
    // printf("Concatenated string: %s\n", small_dest);
    return 0;
    
    = 10 字节 // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节! // printf("Concatenated string: %s\n", small_dest); return 0;

    }
    “`

  • strncat()

    • 功能: 将源字符串的前 n 个字符连接到目标字符串的末尾。
    • 函数签名: char *strncat(char *dest, const char *src, size_t n);
    • 参数: dest – 目标字符串;src – 源字符串;n – 要从 src 中复制的最大字符数。
    • 返回值: dest 的值。
    • 注意: strncatstrcat 安全,因为它限制了从 src 复制的字符数量。更重要的是,它总是会在连接后的字符串末尾添加 \0
      它复制 srcn 个字符或到 src\0 为止(取较小者),然后在这个结果后添加 \0

    “`c

    include

    include

    int main() {
    char dest[20] = “Hello”; // Original length 5. Max space 19 + \0 = 20
    char src[] = ” World Wide Web”; // Length 15
    size_t dest_len = strlen(dest); // Current length of dest: 5
    size_t space_left = sizeof(dest) – dest_len – 1; // Remaining space for chars: 20 – 5 – 1 = 14

    // 连接 src 的前 space_left 个字符
    strncat(dest, src, space_left);
    printf("Concatenated (strncat): %s\n", dest); // Output: Concatenated (strncat): Hello World Wide
    
    // 如果 space_left 小于 src 的实际长度,只会复制一部分
    // 如果 space_left 大于等于 src 的实际长度,会复制 src 的全部内容并添加 
    // 连接 src 的前 space_left 个字符
    strncat(dest, src, space_left);
    printf("Concatenated (strncat): %s\n", dest); // Output: Concatenated (strncat): Hello World Wide
    // 如果 space_left 小于 src 的实际长度,只会复制一部分
    // 如果 space_left 大于等于 src 的实际长度,会复制 src 的全部内容并添加 \0
    char dest2[20] = "Hello";
    strncat(dest2, " C!", 20 - strlen(dest2) - 1); // 连接 " C!" 的前 (20-5-1)=14 个字符。实际只有 3个字符。
    printf("Concatenated (strncat 2): %s\n", dest2); // Output: Concatenated (strncat 2): Hello C!
    return 0;
    
    char dest2[20] = "Hello"; strncat(dest2, " C!", 20 - strlen(dest2) - 1); // 连接 " C!" 的前 (20-5-1)=14 个字符。实际只有 3个字符。 printf("Concatenated (strncat 2): %s\n", dest2); // Output: Concatenated (strncat 2): Hello C! return 0;

    }
    ``strncat仍然需要确保目标缓冲区dest有足够的空间来容纳**原字符串长度 + 要复制的字符数 + 1 (for \0)**。所以,通常在使用strncat(dest, src, n)之前,会计算目标字符串当前的长度,加上要连接的n个字符长度,确保不超过目标缓冲区的总大小。上面的例子中计算space_left` 就是这种安全用法的体现。

4.4 比较字符串:strcmp()strncmp()

  • strcmp()

    • 功能: 逐个字符比较两个字符串,直到发现差异或到达任一字符串的 \0
    • 函数签名: int strcmp(const char *s1, const char *s2);
    • 参数: s1 – 第一个字符串;s2 – 第二个字符串。
    • 返回值:
      • 如果 s1s2 相等,返回 0。
      • 如果 s1 在字典序上小于 s2,返回一个小于 0 的整数。
      • 如果 s1 在字典序上大于 s2,返回一个大于 0 的整数。
    • 比较是基于字符的ASCII值进行的。

    “`c

    include

    include

    int main() {
    char s1[] = “apple”;
    char s2[] = “apply”;
    char s3[] = “apple”;

    printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s2, strcmp(s1, s2)); // Output: ... < 0 (e.g., -1)
    printf("strcmp(\"%s\", \"%s\") = %d\n", s2, s1, strcmp(s2, s1)); // Output: ... > 0 (e.g., 1)
    printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s3, strcmp(s1, s3)); // Output: ... == 0
    
    char s4[] = "Apple";
    printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s4, strcmp(s1, s4)); // Output: ... > 0 (lowercase 'a' > uppercase 'A')
    return 0;
    

    }
    “`

  • strncmp()

    • 功能: 比较两个字符串的前 n 个字符。
    • 函数签名: int strncmp(const char *s1, const char *s2, size_t n);
    • 参数: s1, s2 – 要比较的字符串;n – 要比较的最大字符数。
    • 返回值:strcmp 类似(小于 0, 0, 大于 0)。
    • 比较会在找到差异、达到 n 个字符或遇到任一字符串的 \0 时停止(取满足条件的最早时刻)。

    “`c

    include

    include

    int main() {
    char s1[] = “applepie”;
    char s2[] = “applejuice”;

    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple")
    printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju")
    
    char s3[] = "app";
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple")
    printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju")
    char s3[] = "app";
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app")
    // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0)
    return 0;
    
    ' vs s3 till '
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple")
    printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju")
    char s3[] = "app";
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app")
    // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0)
    return 0;
    
    ', "applepie" vs "app") // It stops at the '
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple")
    printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju")
    char s3[] = "app";
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app")
    // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0)
    return 0;
    
    ' in s3, then compares 'l' in s1 with '
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple")
    printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju")
    char s3[] = "app";
    printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app")
    // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0)
    return 0;
    
    ' (ASCII 0) return 0;

    }
    “`

4.5 查找字符:strchr()strrchr()

  • strchr()

    • 功能: 在字符串中从头开始查找指定字符的第一次出现。
    • 函数签名: char *strchr(const char *s, int c);
    • 参数: s – 要搜索的字符串;c – 要查找的字符(通常以 int 形式传入)。
    • 返回值: 指向第一次出现字符 c 的位置的指针;如果未找到,返回 NULL
    • 注意: 查找包含字符串末尾的 \0 字符是有效的。

    “`c

    include

    include

    int main() {
    char str[] = “Hello, world!”;
    char *ptr = strchr(str, ‘o’);
    if (ptr != NULL) {
    printf(“First ‘o’ found at position: %ld\n”, ptr – str); // ptr – str gives the index
    printf(“Substring from first ‘o’: %s\n”, ptr); // Output: o, world!
    } else {
    printf(“‘o’ not found.\n”);
    }

    ptr = strchr(str, 'z');
    if (ptr == NULL) {
        printf("'z' not found.\n"); // Output: 'z' not found.
    }
    
    ptr = strchr(str, '
    ptr = strchr(str, 'z');
    if (ptr == NULL) {
    printf("'z' not found.\n"); // Output: 'z' not found.
    }
    ptr = strchr(str, '\0'); // Finding the null terminator is valid
    if (ptr != NULL) {
    printf("Null terminator found at position: %ld\n", ptr - str);
    }
    return 0;
    
    '); // Finding the null terminator is valid if (ptr != NULL) { printf("Null terminator found at position: %ld\n", ptr - str); } return 0;

    }
    “`

  • strrchr()

    • 功能: 在字符串中从头开始查找指定字符的最后一次出现。
    • 函数签名: char *strrchr(const char *s, int c);
    • 参数: s – 要搜索的字符串;c – 要查找的字符。
    • 返回值: 指向最后一次出现字符 c 的位置的指针;如果未找到,返回 NULL
    • 注意: 也包含对 \0 的查找。

    “`c

    include

    include

    int main() {
    char str[] = “programming”;
    char *ptr = strrchr(str, ‘g’);
    if (ptr != NULL) {
    printf(“Last ‘g’ found at position: %ld\n”, ptr – str); // Output: Last ‘g’ found at position: 9
    printf(“Substring from last ‘g’: %s\n”, ptr); // Output: g
    } else {
    printf(“‘g’ not found.\n”);
    }
    return 0;
    }
    “`

4.6 查找子字符串:strstr()

  • 功能: 在一个字符串中查找另一个子字符串的第一次出现。
  • 函数签名: char *strstr(const char *haystack, const char *needle);
  • 参数: haystack – 在其中搜索的字符串(大海);needle – 要查找的子字符串(针)。
  • 返回值: 指向 haystack 中第一次出现 needle 子字符串的起始位置的指针;如果未找到,返回 NULL

“`c

include

include

int main() {
char text[] = “The quick brown fox jumps over the lazy dog.”;
char sub[] = “fox”;
char *ptr = strstr(text, sub);

if (ptr != NULL) {
    printf("Substring \"%s\" found in \"%s\".\n", sub, text);
    printf("Starting from: %s\n", ptr); // Output: Starting from: fox jumps over the lazy dog.
} else {
    printf("Substring \"%s\" not found.\n", sub);
}

char sub2[] = "cat";
 ptr = strstr(text, sub2);
 if (ptr == NULL) {
    printf("Substring \"%s\" not found.\n", sub2); // Output: Substring "cat" not found.
 }

return 0;

}
“`

还有其他一些 <string.h> 中的函数,如 memset, memcpy, memmove 用于字节操作(也可以用于字符串,但要小心 \0),以及 strtok 用于字符串分割等。对于入门,掌握上面列出的核心函数已经足够了。

5. 字符串与指针:深入理解其关系

在C语言中,字符串和指针有着密不可分的关系。字符数组名在很多情况下可以被当作指向数组第一个元素的指针。字符串处理函数通常接受 char * 类型的参数,意味着它们操作的是一个地址,从这个地址开始,直到遇到 \0

回顾一下前面提到的两种声明字符串的方式:

  1. char array[] = "hello";

    • array 是一个字符数组,它在内存中分配了一块存储空间(通常在栈上)。
    • array 这个名字本身在表达式中会衰退(decay)为指向数组第一个元素的指针(char *)。
    • 数组的内容 "hello\0" 存储在这块内存中,可以修改
    • sizeof(array) 会得到数组的实际大小(例如 6)。
    • strlen(array) 会得到字符串的长度(例如 5)。
  2. char *pointer = "world";

    • pointer 是一个 char 类型的指针变量,它本身占用几个字节的存储空间(通常在栈上)。
    • 字符串字面量 "world\0" 通常存储在程序的只读数据段。
    • pointer 存储的是这个只读字符串字面量的首地址。
    • *pointer 指向的内容(即字符串字面量)是不可修改的
    • sizeof(pointer) 会得到指针变量本身的大小(通常是 4 或 8 字节),而不是字符串的长度。
    • strlen(pointer) 会得到字符串的长度(例如 5)。

示例:通过指针遍历字符串

“`c

include

int main() {
char str[] = “Example”;
char *ptr = str; // ptr 指向 str 的第一个字符 ‘E’

printf("Iterating through string using pointer:\n");
while (*ptr != '
printf("Iterating through string using pointer:\n");
while (*ptr != '\0') {
printf("%c ", *ptr); // Print the character the pointer is pointing to
ptr++;               // Move the pointer to the next character
}
printf("\n"); // Output: E x a m p l e
return 0;
') { printf("%c ", *ptr); // Print the character the pointer is pointing to ptr++; // Move the pointer to the next character } printf("\n"); // Output: E x a m p l e return 0;

}
“`

理解字符串和指针的关系对于高效和正确地编写C字符串代码至关重要。当你传递一个字符串给函数时,你传递的实际上是它的起始地址(一个指针)。函数通过这个指针访问字符串,并依赖 \0 来知道何时停止。

6. 字符串的常见问题与陷阱

学习C语言字符串,了解常见的陷阱是避免错误的必要步骤。

  1. 缓冲区溢出 (Buffer Overflow): 这是最常见也是最危险的问题。使用 strcpy, strcat, scanf("%s", ...) 而没有检查目标缓冲区大小,是导致安全漏洞的罪魁祸首。永远记住要为 \0 预留一个字节空间。
  2. 忘记空终止符 (\0): 如果手动构建字符数组而忘记添加 \0,或者使用 strncpy 时源字符串长度等于 n 而没有手动添加 \0,那么结果将不是一个有效的C字符串。使用 strlen, printf("%s", ...) 等函数时会读取到无效内存。
  3. 修改字符串字面量: 通过 char *ptr = "..." 声明的字符串是只读的。尝试通过 ptr[i] = ...*ptr = ... 修改它们会导致程序崩溃或未预测的行为。
  4. sizeof vs. strlen: 对于 char array[] = "..." 声明的字符串:sizeof(array) 返回数组的总字节数(包括 \0 及未使用的空间),strlen(array) 返回字符串的实际字符数(不包括 \0)。对于 char *ptr = "..."char *ptr;sizeof(ptr) 返回指针变量的大小,与字符串内容无关;strlen(ptr) 返回指针指向的字符串的长度(如果指针指向一个有效的C字符串)。
  5. 局部字符串数组的生命周期: 在函数内部声明的 char array[] 是局部变量,存储在栈上。函数返回后,这块内存会被回收,返回指向这个局部数组的指针是危险的,因为指针指向的内存可能不再有效。如果要返回字符串,通常需要动态分配内存(使用 malloc 等),或者将字符串复制到调用者提供的缓冲区中。

7. 总结与实践建议

  • 理解本质: C字符串是字符数组,以 \0 结尾。\0 是字符串结束的标志。
  • 安全第一: 永远小心缓冲区溢出。优先使用 fgets 进行输入。使用 strncpystrncat 时,务必计算好目标缓冲区剩余空间,并确保结果字符串以 \0 结尾。在可能的情况下,自己手动检查长度后再进行复制/连接。
  • 掌握 <string.h>: 熟悉并正确使用 strlen, strcpy, strcat, strcmp 等函数。理解它们的参数、返回值和潜在风险。
  • 区分数组与指针: 明确 char array[]char *pointer = "..." 的区别,特别是它们的可修改性和内存位置。不要试图修改字符串字面量。
  • 动手实践: 理论结合实践,多写代码,尝试不同的字符串操作,故意制造一些错误(如缓冲区溢出)来观察程序行为,加深理解。
  • 查阅文档: 遇到不确定的函数用法时,查阅C语言标准库文档是最好的方法。

C语言字符串虽然初看起来有些复杂,但只要掌握了字符数组、空终止符以及标准库函数的使用,并时刻保持对内存和安全性的警惕,你就能有效地处理字符串相关的编程任务。

这篇教程涵盖了C语言字符串的核心概念和常用操作。字符串在C语言中是构建更复杂数据结构(如链表、树中存储字符串数据)和进行文件I/O、网络通信等操作的基础。打好字符串的基础,对于你进一步深入学习C语言至关重要。

祝你在C语言的学习旅程中一切顺利!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部