C字符串:简单问题

6

我下面初始化了三个变量:

char c1[] = "Hello";
char c2[] = { 'H', 'e', 'l', 'l', 'o', '\0'};
char* c3 = "Hello";

我知道c1和c2是相同的,它们都是字符串,因为它们以\0结尾。然而,c3与c1和c2不同。这是因为c3没有以\0结尾吗?这是否意味着c3不是一个字符串?如果c3不是一个字符串,那么为什么printf("%s", c3);不会报错呢?谢谢!
编辑:c1和c2可以被修改,但c3不能,有原因吗?

你只是出于好奇,为什么需要将c3设置为指针? - stanigator
是什么让你得出结论,即 c3 指向的 "Hello" 没有以 \0 结尾? - Chris Eberle
9个回答

10

就 C 语言而言,c3 和其他变量最重要的区别在于你不能尝试使用 c3 修改底层字符。我通常会这样思考:

char *xyz = "xyz";

会在栈上创建一个可修改的指针,并将其指向不可修改的字符序列{'x','y','z','\0'}。另一方面,

char xyz[] = "xyz";
将创建一个可以修改的数组,该数组位于堆栈上,足够大,以容纳字符序列 {'x','y','z','\0'},然后将该字符序列复制到其中。数组内容将是可修改的。请记住,标准对堆栈没有任何规定,但通常是这样完成的。毕竟,这只是一个内存辅助工具。
正式地说,c3 是一个指向字符串常量的指针,而 c1 和 c2 都是字符数组,它们都恰好以空字符结尾。当它们传递给像 printf 这样的函数时,它们会衰减为数组的第一个元素的指针,这意味着它们在函数内部与 c3 的处理方式相同(实际上,在相当多的情况下,它们都会衰减,有关异常情况,请参见下面从 C99 第三个引用)。
C99 的相关部分是“6.4.5 字符串文字”,它解释了为什么不允许修改 c3 指向的内容:
"未指定这些数组是否不同,只要它们的元素具有适当的值即可。如果程序尝试修改这样的数组,则行为未定义。"
以及为什么它确实具有空终止符:
"在第 7 个转换阶段,将值为零的字节或代码附加到由字符串文字或文字产生的每个多字节字符序列。"
在“6.3 转换”下的“6.3.2.1 左值、数组和函数设计符”中,它声明:
"除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串常量,否则具有类型 "type 的数组" 的表达式将被转换为类型 "指向类型的指针" 的表达式,该指针指向数组对象的初始元素,并且不是左值。如果数组对象具有寄存器存储类,则行为未定义。"

当它们被传递给函数时,它们会衰变为指针。在C++中,c1c2也可以通过引用传递,这种情况下它们仍然不会衰变为指针。这取决于函数接收的方式。 - iammilind
您说“您不被允许修改c3。更准确的说法难道不是“您可以修改指针c3,但不能修改它所指向的字符串”吗? - ypercubeᵀᴹ
@ypercube,你说得对。我在第一段就做对了,然后变得有些马虎了。已经修复。 - paxdiablo

5

第一点,

char* c3 = "Hello"; // may be valid C, but bad C++!

这是一种容易出错的风格,所以不要使用它。相反,请使用

const char* c3 = "Hello";

这是一段有效的代码。指针c3指向存储"Hello"字符串的位置的地址。但是,您不能修改*c3(即c3的内容),就像之前的情况一样(如果这样做,它是未定义的行为)。


@iammilind 谢谢!那很有道理。顺便问一下,为什么 c3 不能被修改而其他两个可以呢? - OckhamsRazor
指针 c3 可以 被修改。c3 指向的字符串不安全,不能被修改。 - ypercubeᵀᴹ
请注意您的措辞,“已弃用”在ISO标准中有特定的含义,而此功能在c99或c1x中 被弃用。与之形成对比的是例如 gets,它在c99中被弃用并在c1x中被移除。 - paxdiablo
有趣的是,在C++03中它已被弃用,但现在不再如此。哎呀 - 有很多代码将字符串字面量分配给非const char指针。 - Michael Burr
@pax:在C++0x中,char*p = "foobar";是允许的还是禁止的? - fredoverflow
不太确定,@Fred,我更喜欢C语言。但我很难相信他们会做出像那样的根本性、严重破坏性的改变。 - paxdiablo

3

c3是指向字符串的指针,这就是printf("%s", ...)所期望的参数。

之所以printf("%s", c1)printf("%s", c2)也可以工作,是因为在C语言中,数组很容易在表达式中“腐烂”成指针。事实上,在表达式中,数组名不会被“腐烂”成指针的唯一情况是它作为sizeof运算符或&(取地址)运算符的操作数时。

这导致了一个常见的混淆,即指针和数组在C语言中是等价的,这是不正确的。只是在C语言中,几乎可以在任何指针可用的地方使用数组。唯一的例外是它们不能被赋值,除非它们被下标引用(这会将其视为指针的表达式)。

请注意,最后一个字符串还有一个区别-由于它是字符串字面量,所以它不能被修改(如果尝试修改它,其结果是未定义的)。


1

c1c2分配6个字节的内存,并在其中存储以空字符结尾的字符串。

然而,c3程序内存中分配了(同样以空字符结尾的)字符串,并创建一个指向它的指针,即该字符串与其他指令一起存储,而不是在堆栈(或堆?请有经验者纠正我)上,因此编辑它是不安全的。


1
在 C 语言中,常量 "string" 可以有两种含义,取决于它所使用的上下文。它可以表示可执行文件中的 ro 段中的字符串(尽管我不认为标准明确说明了这一点),使得 const char *foo = "bar" 这个语句将 foo 初始化为指向已加载的可执行文件内存中的位置。如果二进制 blob ("bar") 确实在 ro 段中,并且你执行类似于 foo[0] = 'x' 的操作,你将会得到一个 SIGSEGV
然而,当你写 char x[] = "Hello"(或者 char x[6] = "Hello")时,你是将 "Hello" 作为数组初始化器(就像 int x[2] = { 1, 2 }),而 x 只是一个在堆栈上分配的普通(可写)数组。在这种情况下,"Hello" 只是 {'H', 'e', 'l', 'l', 'o', '\0' } 的简写。

"bar""Hello"都是以空字符结尾的。


0

c3 没有以 NUL 或 NULL 结尾。它是一个指向以 NUL 结尾的字符串的指针。


NUL和NULL有何区别? - Tamer Shlash
1
“NUL”是ASCII中“\0”字符的术语。通常我更常看到使用“NULL”。更正确的说法可能是“null character”,因为在“stddef.h”(和其他头文件)中定义了标识符NULL,并用于表示空指针常量。 - Michael Burr
@Michael Burr:谢谢。我知道并使用空指针常量,但它到底是什么?它不也是整数值0吗? - Tamer Shlash
1
@Mr. TAMER:我并不是想暗示您(或其他任何人)不知道在C源代码中NULL的用途,只是当谈论字符串终止时,它可能不是最好的术语(尽管实际上我认为它并不那么令人困惑,我自己经常使用它)。在C中,它通常被定义为类似于((void*) 0),而在C++中则简单地为0 - Michael Burr

0

这是一个字符串它指向一个字符串,但是存在风险。


1
@iammilind:错误已经被确认! - check123

0

这是一个指向具有不同终止符的字符串的指针。


那个不同的终止是什么? - Aamir

0

C3是指向字符串第一个单元格的指针。 C1、C2只是普通数组,没有被任何人指向。


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