C 语言的字符串本质上是字符数组,以值为 0 的空字符 \0 标记结束。C 语言没有独立的 string 类型,理解这一点是掌握后续字符串操作的基础。
两种定义方式
若只需引用固定文本、不需要修改内容,可使用指针指向字符串字面量:
const char * name = "张三";上述形式仅适合读取,若尝试修改字面量内容,属于未定义行为。若需要在运行期修改字符,应定义字符数组:
char name[] = "张三";方括号中省略长度时,由编译器根据初始化内容自动计算数组大小,与显式写出字符数再加 1 的写法等价:
char name[] = "John";
/* 等价于 */
char name[5] = "John";多出的 1 个位置用于存放字符串结束符 \0。运行时函数通过从首字符起逐个读取,直到遇到 \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 读入字符串。注意:数组名本身表示首地址,不要在数组名前加 &。
#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 中的函数。下一章将介绍函数的定义与调用。