Menu

C 语言字符串:字符数组、空结束符与示例

C 语言没有字符串类型,字符串就是以 '\0' 字节结尾的 char 数组。本页先建立这个思维模型,再讲字面量、打印、遍历,以及为什么不能用 = 复制字符串。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

大多数语言都会给你一个知道自身长度的 string 类型,C 没有。在 C 中,字符串只是一个 char 数组,外加一条规则:文本后面跟着一个值为零的字节,写作 '\0',叫作空结束符。C 字符串的其他一切都源于这一条约定。

字符串就是 char 数组加上 '\0'

用字面量初始化一个 char 数组,就声明了一个字符串:

这个数组看起来有四个字节,而不是三个:

name:  +-----+-----+-----+------+
       | 'A' | 'd' | 'a' | '\0' |
       +-----+-----+-----+------+
index:    0     1     2     3

sizeof(name) 是 4。编译器数了字符个数,为结束符加了一个,然后替你确定了数组大小。正因为有这个额外的字节,char name[3] = "Ada"; 就是一个迟早会出问题的 bug:文本放得下,结束符却放不下,后续的任何代码都无法知道字符串在哪里结束。

如果之后想修改内容,就自己指定大小并留出余量:

char name[32] = "Ada";   /* 3 characters, a terminator, 28 bytes spare */

初始化会把未使用的字节填为零,这没有坏处。

为什么结束符很重要

C 函数都不接收长度。printf("%s", s) 只拿到一个地址,然后一路向后打印字节,直到遇到 '\0'。所以没有结束符的 char 数组不是字符串,把它当作字符串使用,就会读到紧随其后的任意内存。

要记住的规则:sizeof 给出存储空间,strlen 给出文本长度。对于 char buf[32] = "Ada",sizeof buf 是 32,strlen(buf) 是 3。它们回答的是不同的问题,把两者搞混是最常见的 C bug 之一。

字符串字面量与 char 数组

下面两行看起来很像,行为却大不相同:

char  a[] = "hello";   /* an array you own, initialized from the literal */
char *p   = "hello";   /* a pointer aimed at the literal itself */

a 是一个 6 字节的数组,存放文本的一份私有副本,你可以修改它。p 指向程序的只读字符串数据;字面量是共享的,通过 p 写入是未定义行为,通常会在运行时而不是编译时崩溃。

只要指针指向字面量,就写成 const char *。这样编译器会在编译时拒绝写入,而不是让它变成一次崩溃。在两者之间做选择,本质上是指针和数组的问题,指针与数组一页中有详细介绍。

还有一个值得知道的区别:sizeof a 是 6,而 sizeof p 是指针的大小(在大多数系统上是 8),与文本长短无关。

打印和读取字符串

%s 打印字符串,%c 打印单个字符。

读取输入时,完全不要用 gets。它无法被安全地使用,已经从语言中移除了。应该用 fgets,它接收缓冲区大小,不会溢出:

fgets 会保留你按下的换行符,而字符串里几乎从来都不需要它。strcspn 那一行找到第一个 '\n' 并用结束符覆盖它,这是去掉换行符的一种简洁的标准写法。

逐个字符遍历字符串

因为结尾是用标记表示的,而不是用计数,惯用的循环直接检查字符本身:

条件 text[i] != '\0' 常被简写为 text[i],因为 '\0' 是零,也就是假。两种写法都正确;学习阶段,显式的写法更易读。

也可以用指针代替下标来遍历,这同样是地道的 C 写法:

调用 toupper 之前转换为 unsigned char 不是装饰:<ctype.h> 中的函数对负值是未定义的,而对于大于 127 的字节,普通 char 可能是负数。

不能用 = 复制字符串

这是每个新手都会撞上的墙:

char a[10];
a = "hello";        /* error: assignment to expression with array type */

char b[10] = "hi";
char c[10];
c = b;              /* error - same reason */

数组名不是可修改的值,所以 = 无从下手。唯一能这样简写的地方是声明时的初始化,那是由编译器完成的另一种操作。

要在运行时复制,就用 <string.h> 中的 strcpy 复制字节:

同样的道理也解释了为什么 == 不能比较字符串。if (a == b) 比较的是两个地址,所以对于存着相同文本的两个不同数组,结果为假。应该改用 strcmp(a, b) == 0。这两个函数以及它们更安全的版本,都在字符串函数一页中介绍。

字符串数组

字符串列表可以是指针数组(用于只读的固定文本),也可以是二维 char 数组(每一项都需要可修改时):

指针数组存放三个指向只读字面量的地址,紧凑但不可修改。char[3][16] 的写法占用 48 字节你自己的内存,所以每一行都可以重写。

常见错误

  • 忘了结束符的那个字节。char s[5] = "hello" 没有空间放 '\0'。大小要按 strlen + 1 计算。
  • 该用 strlen 的地方用了 sizeof。对指针用 sizeof 得到的是指针的大小,而不是文本的长度。
  • 通过 char * 写入字面量。这类指针要声明为 const char *。
  • 用 == 比较。那比较的是地址。应该用 strcmp。
  • 用 scanf("%s", buf) 读取。它没有大小限制,很容易溢出;优先用 fgets。

常见问题

C 语言中如何声明字符串?

声明为 char 数组:char name[] = "Ada";。C 没有内置的字符串类型,这个数组存放 'A'、'd'、'a',以及隐藏的第四个字节 '\0',它标志着字符串的结尾。如果之后还想修改文本,就给数组一个明确的大小:char name[32] = "Ada";。

C 语言中的空结束符是什么?

标志字符串结尾的字节 '\0'(值为零)。每个标准函数,比如 printf("%s")、strlen、strcpy,都会一直向后读取,直到遇到这个字节。没有结束符的 char 数组不是字符串,把它传给这些函数会读到内存末尾之外。

C 语言中为什么不能用 = 给字符串赋值?

因为数组不是能用 = 复制的值。char a[10]; a = "hi"; 无法通过编译。应该用 <string.h> 中的 strcpy(a, "hi") 复制字符,或者在声明时初始化:char a[10] = "hi";,这是唯一可以这样简写的地方。

C 语言中 char * 和 char [] 有什么区别?

char s[] = "hi" 创建一个可修改的数组,里面是文本的一份私有副本。char *p = "hi" 创建一个指向字符串字面量的指针,字面量位于只读内存中:读取没问题,通过它写入就会崩溃。需要修改时用数组,只读时用指针。

Coddy 编程语言插图

和 Coddy 一起学编程

开始