如何将字符数组用作字符串?

15

我知道C语言中的字符串实际上只是字符数组。所以我尝试了以下代码,但结果却很奇怪,比如会输出垃圾数据或程序崩溃:

#include <stdio.h>

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

这为什么不起作用?

使用 gcc -std=c17 -pedantic-errors -Wall -Wextra 编译没有问题。


注意: 本文旨在作为一个常见问题的规范FAQ,以解决在声明字符串时未分配空间给NUL终止符导致的问题。

4个回答

17

C字符串是以空字符结尾的字符数组。

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

因此,每次分配字符串空间时,必须包括足够的空间来容纳空字符。您的示例没有这样做,它只为"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语言中编写空终止符字符的最常见方法是使用所谓的“八进制转义序列”,看起来像这样:'\0'。这与编写0完全相同,但\用作自说明代码,以表明零明确意味着空终止符。例如,if(str[i] == '\0')这样的代码将检查特定字符是否为null终止符。
请注意,空终止符一词与空指针或NULL宏无关!这可能会让人感到困惑 - 名称非常相似但含义非常不同。这就是为什么空终止符有时被称为一个L的NUL,不要与NULL或空指针混淆。有关详细信息,请参见此SO问题的答案。
您代码中的"hello"称为字符串文字。这应该被视为只读字符串。""语法意味着编译器将自动在字符串文字的末尾附加空终止符。因此,如果打印sizeof("hello"),您将得到6而不是5,因为您获取包括空终止符的数组大小。
它可以在gcc下干净地编译,实际上,甚至没有警告。这是因为C语言中的一个微妙细节/缺陷允许使用包含与数组空间大小相同的字符的字符串字面量初始化字符数组,然后静默丢弃空终止符(C17 6.7.9/15)。出于历史原因,该语言故意以这种方式行为,有关详细信息,请参见Inconsistent gcc diagnostic for string initialization。还要注意,C++在这里不同,不允许使用此技巧/缺陷。

1
... 也许还包括 char *str = "hello"; ... str[0] = foo; 的问题。 - Jabberwocky
@WeatherVane 应该在这里另有一个常见问题解答:https://dev59.com/kXRB5IYBdhLWcg3w1Kv0 - Lundin
C字符串是字符数组的一部分,其中一个元素(不一定是最后一个)是0,或者根据上下文,是指向包含后续元素中的0的字符数组元素的指针。 - pmg
我已经使用了char[n+1]的范例数十年,显式地为字符串声明提供了空间以容纳空终止符号。它是“自我记录代码”的真正例子。 - David R Tribble
这篇文章已经被扩展并整合到更广泛和更详细的字符串处理 FAQ 中,可以在 Codidact - C 语言编程中常见的字符串处理陷阱 找到。我不再在 SO 上发布详细的自问自答 Q&A。 - Lundin
显示剩余3条评论

5

从C标准(7.1.1术语定义)

1 字符串是由连续字符序列组成,并以第一个空字符结尾的。有时会使用多字节字符串来强调对包含在字符串中的多字节字符进行特殊处理,或避免与宽字符串混淆。指向字符串的指针是指向其初始(地址最低)字符的指针。字符串的长度是空字符之前的字节数,字符串的值是所包含字符的值序列,按顺序排列。

在这个声明中:

char str [5] = "hello";

字符串字面值"hello"的内部表示形式如下:

{ 'h', 'e', 'l', 'l', 'o', '\0' }

因此它有6个字符,包括终止零。它的元素用于初始化字符数组str,该数组只保留5个字符的空间。

C标准(相对于C++标准)允许在不使用字符串字面值的终止零作为初始化程序时初始化字符数组。

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

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

char str [6] = "hello";

或只需
char str [] = "hello";

在最后一种情况下,字符数组的大小取决于与字符串文本相同的初始化器数量,这个数量等于6。

3

所有的 字符串 都可以被视为 字符数组 (是的),但不是所有的 字符数组 都可以被视为 字符串 (不是的)。

为什么不是?为什么很重要?

除了其他回答解释字符串的长度没有作为字符串本身的一部分存储以及引用字符串定义标准的答案外,另一方面是 "C 库函数如何处理字符串?"

虽然字符数组可以容纳相同的字符,但它只是一个字符数组,除非最后一个字符之后跟随 空字符。这个 空字符 是使字符数组能够被视为(处理为)字符串的原因。

C 中的所有期望参数为字符串的函数都期望字符序列以 空字符 结尾。为什么呢?

这与所有字符串函数的工作方式有关。由于长度不作为数组的一部分包含在内,字符串函数会在数组中向前扫描,直到找到 空字符 (例如,'\0' -- 相当于十进制 0)。请参阅 ASCII 表和说明。无论您是使用 strcpystrchrstrcspn 等函数,所有字符串函数都依靠存在 空字符 来定义字符串的结束位置。

通过比较 string.h 中两个类似函数的例子,可以强调 空字符 的重要性。例如:

    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终止字符,因此需要显式指定要复制的字节数作为第三个参数。这第三个参数提供了与strcpy相同的大小信息,strcpy只需向前扫描,直到找到nul终止字符即可推导出该信息。
(这也强调了如果未向期望字符串的任何函数(如strcpy)提供nul终止字符串,会发生什么错误 - 它不知道在哪里停止,并且会快乐地跑过内存段引发未定义行为,直到在内存中偶然发现nul字符- 或者发生分段错误)
这就是为什么期望nul终止字符串的函数必须传递nul终止字符串并且为什么它很重要的原因。

我喜欢这个答案,因为它介绍了在字符串上添加另一层语义的想法。一个char数组就是那样的。现在,我可以定义一个字符串为“一个字符数组,其中最后一个字符的值为0”。但我也可以用另一种方式来定义它(例如:“一个字符数组,第一个字符表示后面有多少个字符” - 即Pascal字符串)。并不是说一个字符串必须以0结尾,而是C库将单词字符串定义为具有该含义。 - spectras
是的,语义是C(以及在C++中的.c_str()),Pascal字符串或其他语言中的概念会有不同的处理方式。但目标是相同的。想象一下一个缺少长度信息的Pascal字符串 :) - David C. Rankin

0

直观地...

将数组视为变量(保存事物)和字符串视为值(可放置在变量中)。

它们绝对不是同一件事。 在您的情况下,变量太小,无法容纳字符串,因此字符串被截断。 (C中的“引用字符串”在末尾具有隐式空字符。)

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

请注意,通常的赋值和比较运算符(= == <等)不会按照您的预期工作。 但是,一旦您知道自己在做什么,strxyz函数族就非常接近了。请参阅C FAQ关于字符串数组的内容。


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