C 语言的字符串本质上是字符数组,以值为 0 的空字符 \0 标记结束。C 语言没有独立的 string 类型,理解这一点是掌握后续字符串操作的基础。

两种定义方式

若只需引用固定文本、不需要修改内容,可使用指针指向字符串字面量:

const char * name = "张三";

上述形式仅适合读取,若尝试修改字面量内容,属于未定义行为。若需要在运行期修改字符,应定义字符数组:

char name[] = "张三";

方括号中省略长度时,由编译器根据初始化内容自动计算数组大小,与显式写出字符数再加 1 的写法等价:

char name[] = "John";
/* 等价于 */
char name[5] = "John";

多出的 1 个位置用于存放字符串结束符 \0。运行时函数通过从首字符起逐个读取,直到遇到 \0 判定字符串结束。若未预留结束符位置,读取可能越过数组边界,引发缓冲区溢出等安全问题。

J name[0] o name[1] h name[2] n name[3] \0 name[4] 字符串长度 4,但数组要 5 格,最后一格留给 \0
char name[5] = "John":4 个可见字符与 1 个 \0 结束符;程序依 \0 判断边界。

打印字符串

printf 使用 %s 输出字符串,可与其他格式符组合输出文本与数值:

const char * name = "张三";
int age = 27;
printf("%s 今年 %d 岁。\n", name, age);
/* 输出:张三 今年 27 岁。 */

输出末尾建议添加 \n,否则下一次 printf 可能与当前行连在一起显示。

读取字符串

简单场景可使用 scanf 读入字符串。注意:数组名本身表示首地址,不要在数组名前加 &。

C 语言字符串输入示例运行
#include <stdio.h>

int main(void) {
    char str[6];
    scanf("%5s", str);     // 上限 5,留一个给 \0
    printf("你输入的是:%s\n", str);
    return 0;
}

scanf 的 %s 在遇到空格、制表符或换行时会停止读取。例如输入 Hello World 通常只能得到 Hello。若需要读取包含空格的整行,应使用 fgets:

char line[100];
fgets(line, sizeof(line), stdin);   // 读到换行为止,换行符也一并存进来
printf("整行内容:%s", line);

fgets 会将行尾换行符一并读入。若需去掉换行,可使用:line[strcspn(line, "\n")] = '\0';

字符串的长度:strlen

strlen 返回字符串长度,不包含结尾的 \0:

#include <string.h>

const char * name = "Nikhil";
printf("%zu\n", strlen(name));   // 6

字符串的比较:strcmp / strncmp

字符串内容不能使用 == 比较,该运算符比较的是指针地址而非字符序列。应使用 strcmp,或在有长度限制时优先使用 strncmp:

const char * name = "张三";
if (strcmp(name, "张三") == 0) {      // 相等时返回 0
    printf("对的,是你。\n");
} else {
    printf("你不是张三,再见。\n");
}

二者返回值规则相同:内容完全一致时返回 0;若前者字典序更大返回正数,更小返回负数。差别主要在比较长度是否设上限,对比如下。

函数比较范围适用场景
strcmp(a, b)从首字符起比较,直到某一字符串遇到 \0两个字符串均已正确结束,需要完整内容比较
strncmp(a, b, n)最多比较 n 个字符定长字段、前缀匹配,或需要限制比较长度以降低越界风险

下面用两组字符串对比返回值(非 0 表示不相等,0 表示在各自比较规则下相等):

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

int main(void) {
    const char *cmd = "delete file.txt";
    const char *kw  = "delete";

    printf("strcmp(cmd, kw)        = %d\n", strcmp(cmd, kw));
    printf("strncmp(cmd, kw, 6)    = %d\n", strncmp(cmd, kw, 6));

    printf("strcmp(\"abc\", \"abd\")     = %d\n", strcmp("abc", "abd"));
    printf("strncmp(\"abc\", \"abd\", 2) = %d\n", strncmp("abc", "abd", 2));
    return 0;
}
/* 典型输出:
   strcmp(cmd, kw)        = 正数   (较短串已结束,较长串仍有字符)
   strncmp(cmd, kw, 6)    = 0      (只比较前 6 个字符 "delete")
   strcmp("abc", "abd")     = 负数   (比较到第三个字符)
   strncmp("abc", "abd", 2) = 0      (只比较前 2 个字符 "ab") */

第一组说明:strcmp 会一直比到某一串遇到 \0,因此 "delete" 与更长的 "delete file.txt" 判为不相等;若只需判断命令前缀是否为 delete,应使用 strncmp(cmd, kw, 6)(长度取关键字字符数,不含 \0)。第二组说明:在只关心前若干字符是否一致时,用 strncmp 并显式指定 n,可避免把后面尚未比较的差异算进去。

处理用户输入、网络数据或固定宽度文本时,通常优先使用 strncmp,并结合缓冲区大小与 n 的取值做边界检查。

字符串的拼接:strcat / strncat

strcat 把源串从首字符起一直追加到目标串末尾的 \0 之前;strncat(dst, src, n) 则最多从 src 取 n 个字符追加,并在结果末尾写入 \0。二者都要求 dst 中已有合法字符串,且剩余空间足以容纳原有内容、本次追加内容及新的结束符。

函数追加范围适用场景
strcat(dst, src)src 中直到 \0 的全部字符已确认 src 较短且 dst 缓冲区足够大
strncat(dst, src, n)最多 n 个来自 src 的字符(遇 \0 提前结束)只需接上一段前缀、限制单次追加长度,或配合缓冲区大小做边界控制

下面在同一初始内容下对比两种写法的结果:

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

int main(void) {
    char d1[20] = "Hello";
    char d2[20] = "Hello";
    const char *src = "World";

    strcat(d1, src);
    strncat(d2, src, 3);

    printf("strcat  后: %s\n", d1);   /* HelloWorld */
    printf("strncat 后: %s\n", d2);   /* HelloWor */

    char path[16] = "/tmp/";
    const char *name = "report-2025.txt";
    char path2[16] = "/tmp/";

    strcat(path, name);              /* 整段文件名接在后面 */
    strncat(path2, name, 6);         /* 只接前 6 个字符 "report" */

    printf("%s\n", path);            /* /tmp/report-2025.txt */
    printf("%s\n", path2);           /* /tmp/report */
    return 0;
}

第一组说明:strcat 会把 "World" 全部接到 "Hello" 之后;strncat(d2, src, 3) 只追加前 3 个字符,因此得到 "HelloWor"。第二组说明:拼接路径与文件名时,若只需试探前缀或受数组长度限制,可用 strncat 指定 n,而不是无上限地 strcat。

无论使用哪一个函数,都应先估算 strlen(dst) + 追加长度 + 1 是否小于目标数组容量;空间不足时属于缓冲区溢出,行为未定义。更稳妥的做法是改用带长度上限的接口(如 snprintf)或动态分配内存。

字符串字面量为什么只读

双引号中的 "Hello World" 称为字符串字面量,通常存放在只读存储区,修改其内容属于未定义行为。需要可写缓冲区时使用字符数组;仅引用固定文本时使用 const char *:

const char * s = "Hello World";   // 只读,安全
char t[] = "Hello World";          // 可读写,t[0] = 'h' 没问题

字符串基于字符数组:可以按索引访问或修改单个字符,但不支持直接用 = 做整体赋值,也不能用 == 比较内容,相关操作应使用 string.h 中的函数。下一章将介绍函数的定义与调用。