字符数组是否保证以空字符结尾?

55
#include <stdio.h>

int main() {
    char a = 5;
    char b[2] = "hi"; // No explicit room for `\0`.
    char c = 6;

    return 0;
}

每当我们写一个用双引号括起来的字符串时,C语言会自动为我们创建一个字符数组,该数组包含该字符串并以\0字符结尾。http://www.eskimo.com/~scs/cclass/notes/sx8.html 在上面的示例中,变量b只有2个字符的空间,因此无法放置空终止字符,但编译器会重新组织存储指令的内存,以使a和c存储在b之前,以便在数组末尾留出一个\0的位置。
这是预期行为还是未定义行为?

7
关于存储顺序的那句话,不是为了“为终止符腾出空间”。因为并没有终止符,编译器可以自由选择任何方式来存储变量。 - Weather Vane
7
创建了字符串字面值,并且该字符串字面值包含空字符终止符。在运行时,数组 b 初始化为从字符串字面值中取出的前两个字符,但不包括空字符终止符。(b 不是一个字符串)。 - William Pursell
7
在C语言中,字符串是以NULL结束的字符数组,因此如果没有以NULL结尾,它就不是字符串,只是一个字符数组。许多字符串函数会搜索NULL字符(例如,确定何时应停止将字符从一个字符串复制到另一个字符串),因此如果没有该字符,它们将无法正常工作(例如,一直复制字符,直到在内存中遇到某个随机的NULL字符)。 - Baard Kopperud
1
不是完全相同的问题,但这两个问题都基于同样的困惑:如何初始化一个没有空终止符的字符数组?(https://dev59.com/YLPma4cB1Zd3GeqPwtNs) - ShadowRanger
2
这不是一个“字符串数组”,那应该是 char *array_of_strings[] = {"hi", "mom"};。你可以称之为字符串(如果它有一个0终止符,也就是ASCII nul(不是NULL,@Baard)),或者你可以称之为字符数组。 - Peter Cordes
显示剩余7条评论
4个回答

50
如果数组足够大,除了空终止符外,可以使用字符串初始化char数组。
这在C标准的6.7.9p14节中详细说明:
“字符类型的数组可以通过字符字符串字面值或UTF-8字符串字面值进行初始化,可选地用大括号括起来。 字符串字面值的连续字节(包括终止空字符,如果有空间或数组大小未知)初始化数组的元素。”
但是,这也意味着您不能将该数组视为字符串,因为它没有空终止符。 因此,按照现有方式,由于您没有对b执行任何字符串操作,您的代码是正确的。
您不能使用过长的字符串进行初始化,例如:
char b[2] = "hello";

由于提供的初始化项数量超过数组容量,这违反了约束条件。根据第6.7.9条第2款的规定:

没有初始化项可以为未包含在正在初始化的实体中的对象提供值。

如果您按以下方式声明和初始化数组:

char b[] = "hi"; 

那么,b将是一个大小为3的数组,足以容纳字符串常量中的两个字符和终止空字节,使b成为一个字符串。
总结一下:
如果数组有固定的大小:
- 如果用于初始化它的字符串常量比数组短,数组将包含字符串中的字符,连续的元素设置为0,因此数组将包含一个字符串。 - 如果数组恰好足够大,可以包含字符串的元素但不是空终止符,则数组将包含字符串中的字符而没有空终止符,这意味着该数组不是一个字符串。 - 如果字符串常量(不计空终止符)比数组长,则会触发undefined behavior的约束违规。
如果数组没有明确的大小,则数组的大小将被调整为容纳字符串常量加上终止空字节。

11
是的,但最好只写char b[] = "hi"; - Cheatah
7
@Dan:有趣的事实是:在C++中这是不同的。如果使用双引号初始化程序时,将显式大小设置得太小以容纳尾随的0字节在C++中是不合法的,因此如果您想在C++中这样做,您必须写成char b[] = {'h','i'};。对于SIMD查找表有时很麻烦,例如static char hex_lut[16] = "0123...ef";需要一个第17个字节或者一个不太可读的源代码。以下链接是一个在GCC中使用C和C++模式的示例,其中C没有警告,而C++则会出现错误消息。https://godbolt.org/z/eTx94a4h7 - Peter Cordes
2
@Cheatah,这可能不是编译器添加警告,只是显示定义b的代码行,并且“没有空间”的文本在注释中。 - ilkkachu
4
不,那样并不更好。至少要将其声明为“const”。对于指向“const char”的指针是否比数组更好,显然取决于用途,因为它不能被修改。 - Konrad Rudolph
2
当使用字符串字面量初始化数组时,它的内容(最多到数组大小)会被复制到数组中。字符串字面量本身也可能单独出现在内存中,这取决于个别编译器以及字符串常量是否在其他地方使用。 - dbush
显示剩余4条评论

35
每当我们写一个用双引号括起来的字符串时,C语言自动为我们创建一个字符数组,包含该字符串,并以\0字符结尾。
在这种情况下,这些注释略有误导性。我需要更新它们。
当你像这样写东西时:
char *p = "Hello";

或者

printf("world!\n");

C语言会自动为您创建一个字符数组,大小恰好适合包含以\0结尾的字符串。

但在数组初始化程序的情况下,情况略有不同。当您编写以下内容时:

char b[2] = "hi";

这个字符串只是你要创建的数组的初始化器。因此,你完全可以控制数组的大小。有几种可能性:

char b0[] = "hi";     // compiler infers size
char b1[1] = "hi";    // error
char b2[2] = "hi";    // No terminating 0 in the array. (Illegal in C++, BTW)
char b3[3] = "hi";    // explicit size matches string literal
char b4[10] = "hi";   // space past end of initializer is always zero-initialized

对于b0,你没有指定大小,所以编译器使用字符串初始化程序选择正确的大小,这将是3。
对于b1,你指定了一个大小,但它太小了,所以编译器应该给你一个错误。
对于b2,这是你问的情况,你指定了一个大小,它刚好足够存放字符串初始化程序中的显式字符,但不包括结尾的\0。这是一个特殊情况。它是合法的,但你在b2中得到的不是一个正确的空结尾字符串。由于它最多只是不寻常,编译器可能会给你一个警告。有关此案例的更多信息,请参见this question
对于b3,你指定了一个大小,它刚好适合,所以你得到一个正确大小的字符串数组,就像b0一样。
对于b4,你指定了一个太大的大小,虽然这没有问题。数组中会有额外的空间,超出了结尾的\0。(事实上,这个额外的空间也将被填充为\0。)这个额外的空间可以让你安全地做一些像strcat(b4, ", wrld!")这样的事情。
不用说,大多数时候你会想使用b0形式。计算字符是繁琐且容易出错的。正如C语言的创造者之一Brian Kernighan在这个背景下所写的那样,“让计算机做脏活。”
还有一件事。你写道:

and yet the compiler is reorganizing the memory store instructions so that a and c are stored before b in memory to make room for a \0 at the end of the array.

我不知道那里发生了什么,但可以肯定的是编译器并没有试图“为 \0 留出空间”。编译器可以并经常以它们自己难以捉摸的内部顺序存储变量,既不匹配您声明它们的顺序,也不匹配字母顺序或其他任何您可能想到的顺序。如果在您的编译器下数组 b 之后有额外的空间,其中包含一个像终止字符串一样的 \0,那可能基本上是随机的机会,而不是因为编译器试图对您进行帮助,使得 printf("%s\n", b) 这样的东西更好地定义。(在我尝试过的两个编译器下,printf("%s\n", b) 打印了 hi^E 和 hi ??,清楚地显示了预期的尾随随机垃圾的存在。)

编译器存储变量的顺序通常(尽管取决于编译器)以避免变量之间浪费空间的方式进行。4字节或更大的变量,以及在某些情况下数组,必须从地址开始,该地址是4的倍数,因此如果您周围有2字节或1字节的变量,则可能会重新排序它们以避免浪费字节。(各种变量类型的大小显然也取决于编译器。在这种情况下,“char”在许多编译器中仅为1或2个字节,因此它们可能会被移动,以便“b”可以从4的倍数开始。) - Darrel Hoffman
小问题: “C自动创建...”,严格来说,由于C没有像解释语言一样的运行时类型,因此编译器/优化器从C翻译并输出asm/obj(即在任何执行之前发生的所有事情),同时遵守标准指定的语法,因此可能无法保证在标准不完整的编译器中工作(例如某些高尔夫CC)。当二进制文件中到达这样的代码时,它已经处于有限状态,并且此时C已经不存在了。 - Peter Badida

6

你的问题有两个方面。

  1. String literal. String literal (ie something enclosed in the double quotes) is always the correct null character terminated string.

    char *p = "ABC";  // p references null character terminated string
    
  2. Character array may only hold as many elements as it has so if you try to initialize two element array with three elements string literal, only two first will be written. So the array will not contain the null character terminated C string

    char p[2] = "AB";  // p is not a valid C string.
    

2
一个字符数组不需要以任何东西结尾。它就是一个数组。如果实际内容比数组的维度小,则需要跟踪该内容的大小。
这里的答案似乎已经退化成了一个字符串讨论。并非所有的字符数组都是字符串。但是,如果它们被处理为事实上的字符串,则使用空终止符作为标志是一种非常强的约定。
您的数组可能使用其他内容,并且也可能有分隔符和区域。毕竟,它可能是一个Union或覆盖一个结构体。可能是另一个系统的暂存区。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接