C语言字符串处理

在C语言中,字符串是以空字符 '\0' 结尾的字符数组。由于C语言没有提供专门的字符串类型,所有字符串操作都需要借助字符数组和标准库函数完成。 <string.h> 头文件中提供了大量用于字符串处理的函数,涵盖复制、连接、比较、查找、分割等常见操作。 掌握这些函数是编写健壮、高效C程序的基础。本章将详细介绍这些函数的用法、注意事项及安全编程技巧。

📝 字符串的表示与存储

C语言中字符串的两种常见表示方式:

  • 字符数组char str[10] = "Hello";,存储在栈或静态区,可修改内容。
  • 字符指针char *str = "Hello";,指向字符串常量,通常存储在只读数据段,不可修改
重要: 通过字符指针定义的字符串字面量是只读的,试图修改会导致未定义行为(通常程序崩溃)。需要可修改的字符串时,必须使用字符数组或动态分配内存。
#include <stdio.h>

int main() {
    char str1[] = "Hello";     // 可修改
    char *str2 = "World";      // 不可修改

    str1[0] = 'h';             // 正确
    // str2[0] = 'w';          // 错误!可能导致程序崩溃

    printf("%s %s\n", str1, str2);
    return 0;
}

📏 字符串长度:strlen

strlen 函数返回字符串的长度(不包括结尾的 '\0')。

size_t strlen(const char *str);
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    printf("字符串: \"%s\"\n", str);
    printf("长度: %zu\n", strlen(str));   // 输出 13(不包括结尾的'\0')
    return 0;
}
注意: strlen 返回 size_t 类型,这是一个无符号整数类型,使用 %zu 格式符输出。

📋 字符串复制:strcpy / strncpy

🔹 strcpy(不安全的复制)

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

src 指向的字符串(包括结尾的 '\0')复制到 dest 指向的数组。如果目标数组不够大,会发生缓冲区溢出。

char dest[10];
strcpy(dest, "Hello");     // 正确,占6字节(包括'\0')
// strcpy(dest, "Hello, World!"); // 错误!目标空间不足,溢出

🔹 strncpy(更安全的受限复制)

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

最多复制 n 个字符到 dest。如果 src 的长度小于 n,则剩余部分用 '\0' 填充;如果大于等于 n,则不会自动添加结尾的 '\0',需要手动处理。

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

int main() {
    char dest[10];
    strncpy(dest, "Hello, World!", sizeof(dest) - 1);  // 最多复制9个字符
    dest[sizeof(dest) - 1] = '\0';                     // 确保以'\0'结尾
    printf("%s\n", dest);   // 输出 "Hello, Wo"
    return 0;
}
注意: strncpy 不会自动添加结尾的 '\0',如果源字符串长度 ≥ n,目标字符串将不会以空字符结尾,后续使用可能出错。

🔗 字符串连接:strcat / strncat

🔹 strcat(不安全的连接)

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

src 追加到 dest 的末尾,覆盖 dest 原来的 '\0'。要求 dest 有足够空间容纳拼接后的字符串。

🔹 strncat(安全的受限连接)

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

最多追加 n 个字符,并自动添加结尾的 '\0'

#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);   // 输出 "Hello, World!"
    return 0;
}

⚖️ 字符串比较:strcmp / strncmp

比较两个字符串的字典序,返回:

  • 0:相等
  • <0:第一个不匹配的字符在 str1 中较小(或 str1str2 的前缀)
  • >0:第一个不匹配的字符在 str1 中较大
int strcmp(const char *str1, const char *str2);
int strncmp(const char *str1, const char *str2, size_t n);  // 只比较前n个字符
#include <stdio.h>
#include <string.h>

int main() {
    char s1[] = "apple";
    char s2[] = "banana";
    char s3[] = "apple";

    if (strcmp(s1, s2) < 0)
        printf("%s 小于 %s\n", s1, s2);

    if (strcmp(s1, s3) == 0)
        printf("%s 等于 %s\n", s1, s3);

    // 比较前3个字符
    if (strncmp(s1, s2, 3) < 0)
        printf("前3个字符比较:apple < banana\n");

    return 0;
}

🔍 字符串查找:strchr, strrchr, strstr

  • strchr(str, c):在字符串中查找字符 c 第一次出现的位置,返回指针,未找到返回 NULL
  • strrchr(str, c):查找字符 c 最后一次出现的位置。
  • strstr(str, substr):查找子串 substr 第一次出现的位置。
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World! Hello, C!";
    char *p;

    p = strchr(str, 'W');
    if (p) printf("找到 'W' 在位置: %ld\n", p - str);  // 输出7

    p = strrchr(str, 'l');
    if (p) printf("最后一个 'l' 在位置: %ld\n", p - str);  // 输出14

    p = strstr(str, "World");
    if (p) printf("子串 \"World\" 起始位置: %ld\n", p - str);  // 输出7

    return 0;
}

✂️ 字符串分割:strtok

strtok 用于将字符串按分隔符拆分为多个令牌(token)。注意:它会修改原字符串(将分隔符替换为 '\0'),且不是线程安全的。

char *strtok(char *str, const char *delim);
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "apple,banana,orange,grape";
    const char delim[] = ",";

    // 第一次调用传入原字符串
    char *token = strtok(str, delim);
    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, delim);  // 后续调用传NULL
    }

    // 此时 str 已被修改,原字符串中的分隔符被替换为 '\0'
    // 所以 str 现在只包含第一个令牌 "apple"
    return 0;
}
替代方案: 如果需要保留原字符串,可以先复制一份再分割。线程安全版本可使用 strtok_r(POSIX)或 strtok_s(C11可选)。

📝 格式化字符串:sprintf / snprintf

将格式化数据写入字符串,类似于 printf,但输出目标是字符串。

int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);  // 安全版本
#include <stdio.h>

int main() {
    char buffer[50];
    int a = 10;
    float b = 3.14159;

    // 不安全:可能溢出
    // sprintf(buffer, "a=%d, b=%.2f", a, b);

    // 安全:指定最多写入49个字符,留一个给'\0'
    snprintf(buffer, sizeof(buffer), "a=%d, b=%.2f", a, b);
    printf("%s\n", buffer);   // 输出 "a=10, b=3.14"

    return 0;
}
推荐: 始终使用 snprintf 替代 sprintf,避免缓冲区溢出风险。

🛡️ 安全字符串操作建议

  • 优先使用受限函数:如 strncpystrncatsnprintfstrncmp,并注意手动添加结尾 '\0'
  • 使用 sizeofstrlen 计算缓冲区剩余空间:在拼接或复制时确保不溢出。
  • 避免直接操作字符串字面量:如需修改,使用字符数组或动态分配内存。
  • 谨慎使用 strtok:注意它修改原字符串且不是线程安全的,考虑使用 strtok_r 或手动解析。
  • 检查函数返回值:如 strchrstrstr 可能返回 NULL,应始终检查。
  • 使用更安全的库(如果可用):C11 提供了 strcpy_sstrcat_s 等边界检查函数,但需要编译器支持(如 MSVC)且非所有平台都支持。

📋 综合示例:统计单词数

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

int countWords(const char *str) {
    int count = 0;
    int inWord = 0;

    while (*str) {
        if (isspace(*str)) {
            inWord = 0;
        } else if (!inWord) {
            inWord = 1;
            count++;
        }
        str++;
    }
    return count;
}

int main() {
    char sentence[200];
    printf("请输入一个英文句子: ");
    fgets(sentence, sizeof(sentence), stdin);

    // 移除换行符
    size_t len = strlen(sentence);
    if (len > 0 && sentence[len-1] == '\n')
        sentence[len-1] = '\0';

    int words = countWords(sentence);
    printf("单词数: %d\n", words);

    // 使用 strtok 分割单词并输出
    printf("单词列表:\n");
    char copy[200];
    strcpy(copy, sentence);   // 复制一份,因为 strtok 会修改原字符串
    char *token = strtok(copy, " \t\n");
    while (token) {
        printf("  %s\n", token);
        token = strtok(NULL, " \t\n");
    }

    return 0;
}

⚠️ 常见错误与注意事项

  • 缓冲区溢出:使用 strcpystrcat 等不安全函数时未保证目标空间足够,是C程序中最常见的安全漏洞之一。
  • 字符串未以 '\0' 结尾:使用 strncpy 后忘记手动添加结尾空字符,导致后续字符串操作越界。
  • 混淆字符数组与指针:尝试修改字符串字面量导致段错误。
  • 忽略函数返回值:如 strchr 返回 NULL 时未检查,继续使用该指针导致崩溃。
  • 在多线程中使用 strtokstrtok 使用静态内部状态,不是线程安全的。使用 strtok_r 替代。
  • 使用 sizeof 对指针计算长度:当字符串作为函数参数传递时,sizeof(str) 返回的是指针大小(通常8),而不是数组长度。应额外传递长度参数。

字符串处理是C语言编程的核心技能之一。掌握标准库提供的函数,并养成安全编码的习惯,能够有效避免缓冲区溢出等常见问题。 下一章我们将学习指针,这是C语言的精髓,也是理解字符串底层操作的关键。