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!别用 == 1 判断
    printf("对的,是你。\n");
} else {
    printf("你不是张三,再见。\n");
}

记死一条:strcmp 相等时返回 0,a 大于 b 返回正数,a 小于 b 返回负数。三个自然的结论:字符串字典序比较靠它,安全版本 strncmp 多一个最多比几个字符的参数。

字符串的拼接:strcat / strncat

strcat 把源串接到目标串末尾。strncat(dst, src, n) 只接最多 n 个字符。注意目标数组必须有足够的剩余空间:

char dest[20] = "Hello";
char src[20] = "World";

strncat(dest, src, 3);     // dest 变成 "HelloWor"
printf("%s\n", dest);

strncat(dest, src, 20);    // 再接 3 个字符 "ld",dest 变成 "HelloWorld"
printf("%s\n", dest);

字符串字面量为什么只读

双引号写出来的 "Hello World"字符串字面量,放在只读内存里,改它是未定义行为。所以要动内容用字符数组,单纯拿来引用固定文本用 const char *

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

字符串背靠数组元件:能按下标访问单个字符、能改单个字符,只是整套的赋值 = 和比较 == 都不行,得靠 string.h 的函数。下一篇进入函数这个重头戏。