字符数组应该如何用作字符串?

How should character arrays be used as strings?

我知道 C 中的字符串只是字符数组。所以我尝试了下面的代码,但它给出了奇怪的结果,例如垃圾输出或程序崩溃:

#include <stdio.h>

int main (void)
{
  char str [5] = "hello";
  puts(str);
}

为什么这不起作用?

它可以用 gcc -std=c17 -pedantic-errors -Wall -Wextra 干净地编译。


注意:此post旨在用作规范常见问题解答,以解决因未能为 NUL 终止符分配空间而引起的问题声明字符串时。

C 字符串是以空终止符结束的字符数组。

所有字符都有一个符号table值。空终止符是符号值 0(零)。它用于标记字符串的结尾。这是必要的,因为字符串的大小没有存储在任何地方。

因此,每次为字符串分配空间时,都必须包含足够的 space 作为空终止符。你的例子没有这样做,它只为 "hello" 的 5 个字符分配空间。正确的代码应该是:

char str[6] = "hello";

或者等效地,您可以编写 5 个字符加 1 个空终止符的自文档代码:

char str[5+1] = "hello";

但您也可以使用它,让编译器进行计数并选择大小:

char str[] = "hello"; // Will allocate 6 bytes automatically

在运行时间内为字符串动态分配内存时,还需要为空终止符分配空间:

char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);

如果您不在字符串末尾附加空终止符,则需要字符串的库函数将无法正常工作,并且您会遇到“未定义行为”错误,例如垃圾输出或程序崩溃。

在 C 中编写空终止符的最常见方法是使用所谓的“八进制转义序列”,如下所示:'[=16=]'。这 100% 等同于编写 0,但 \ 用作自我记录代码以声明零明确表示为空终止符。 if(str[i] == '[=19=]') 等代码将检查特定字符是否为空终止符。

请注意,术语空终止符与空指针或 NULL 宏无关!这可能会造成混淆——名称非常相似,但含义却截然不同。这就是为什么空终止符有时被称为带有一个 L 的 NUL,不要与 NULL 或空指针混淆。有关详细信息,请参阅 this SO question 的答案。

您代码中的 "hello" 称为 字符串文字 。这将被视为只读字符串。 "" 语法意味着编译器会自动在字符串文字的末尾附加一个空终止符。因此,如果您打印出 sizeof("hello"),您将得到 6,而不是 5,因为您得到的是包含空终止符的数组大小。


It compiles cleanly with gcc

的确,连警告都没有。这是因为 C 语言中的一个微妙的 detail/flaw 允许使用字符串文字初始化字符数组,该字符串文字包含与数组中的空间一样多的字符,然后静默丢弃空终止符 (C17 6.7. 9/15)。由于历史原因,该语言故意表现得像这样,详情请参阅 。另请注意,C++ 在这里不同,不允许使用此 trick/flaw。

来自 C 标准(7.1.1 术语定义)

1 A string is a contiguous sequence of characters terminated by and including the first null character. The term multibyte string is sometimes used instead to emphasize special processing given to multibyte characters contained in the string or to avoid confusion with a wide string. A pointer to a string is a pointer to its initial (lowest addressed) character. The length of a string is the number of bytes preceding the null character and the value of a string is the sequence of the values of the contained characters, in order.

在此声明中

char str [5] = "hello";

字符串文字 "hello" 的内部表示类似于

{ 'h', 'e', 'l', 'l', 'o', '[=11=]' }

所以它有 6 个字符,包括终止零。其元素用于初始化字符数组str,仅保留space 5个字符。

C 标准(与 C++ 标准相反)允许在字符串文字的终止零未用作初始值设定项时对字符数组进行此类初始化。

然而,字符数组 str 不包含字符串。

如果您希望数组包含一个字符串,您可以编写

char str [6] = "hello";

或者只是

char str [] = "hello";

在最后一种情况下,字符数组的大小由等于 6 的字符串文字的初始值设定项的数量确定。

所有字符串是否都可以认为是一个字符数组),都可以字符数组被认为是字符串)。

为什么不呢?为什么重要?

除了解释字符串长度不作为字符串的一部分存储在任何地方的其他答案以及对定义字符串的标准的引用之外,反面是 "How do the C library functions handle strings?"

虽然字符数组可以包含相同的字符,但它只是一个字符数组,除非最后一个字符后跟 nul-terminating 字符。 nul-terminating 字符允许将字符数组视为(处理)字符串。

C 中所有期望字符串作为参数的函数都期望字符序列nul-terminated为什么?

它与所有字符串函数的工作方式有关。由于长度不作为数组的一部分包含在内,字符串函数在数组中向前扫描,直到 nul-character(例如 '[=12=]' -- 相当于十进制 0) 找到了。参见 ASCII Table and Description。无论您是否使用 strcpystrchrstrcspn 等。所有字符串函数都依赖于存在的 nul-terminating 字符来定义该字符串的末尾在哪里。

比较 string.h 中的两个相似函数将强调 nul-terminating 字符的重要性。举个例子:

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

strcpy 函数简单地将字节从 src 复制到 dest,直到找到 nul-terminating 字符告诉 strcpy 在哪里停止复制字符。现在采用类似的函数 memcpy:

    void *memcpy(void *dest, const void *src, size_t n);

该函数执行类似的操作,但不考虑或要求 src 参数为字符串。由于 memcpy 不能简单地向前扫描 src 将字节复制到 dest 直到到达 nul-terminating 字符,它需要明确的字节数作为第三个参数复制。第三个参数为 memcpy 提供了相同的大小信息 strcpy 可以简单地向前扫描直到找到 nul-terminating 字符。

(这也强调了如果您未能为函数提供 nul-terminated 字符串,那么 strcpy(或任何需要字符串的函数)出了什么问题 - - 它不知道在哪里停止,并且会愉快地在你的剩余内存段中运行,调用 Undefined Behavior 直到 nul-character 刚刚发生在内存中的某处找到——或者发生分段错误)

这就是 为什么 需要 nul-terminated 字符串的函数必须传递 nul-terminated 字符串和 为什么它很重要 .

直觉上...

将数组视为变量(保存内容),将字符串视为值(可以放置在变量中)。

它们肯定不是一回事。 在您的情况下,变量太小而无法容纳字符串,因此字符串被截断了。 (C 中的"quoted strings" 末尾有一个隐含的空字符。)

但是,可以将字符串存储在比字符串 大得多的数组中。

请注意,通常的赋值和比较运算符(= == < 等)并不像您预期​​的那样工作。但是 strxyz 系列函数非常接近,一旦您知道自己在做什么。见 C FAQ on strings and arrays.