为什么printf("%s",(char[]){'H','i','\0'})可以作为printf("%s","Hi"),但是printf("%s",(char*){'H','i','\0'});会失败?

7

我真的需要帮助。这动摇了我在C语言方面的基础。非常感谢长而详细的答案。我的问题分为两部分。

A: 为什么 printf("%s",(char[]){'H','i','\0'}); 能够工作并打印出 Hi,就像传统的 printf("%s","Hi");那样呢?我们可以在任何C代码中使用 (char[]){'H','i','\0'} 代替 "Hi" 吗?它们是同义的吗?我的意思是,在C中写 "Hi" 通常意味着将 Hi 存储到内存的某个位置,并传递指向该位置的指针。对于看起来不太好看的 (char[]){'H','i','\0'} 是不是也可以这样说?它们是完全相同的吗?

B:printf("%s",(char[]){'H','i','\0'}) 成功工作时,与 printf("%s","Hi") 相同,为什么 printf("%s",(char*){'A','B','\0'} 失败并且会引发段错误,即使有警告?这真让我惊讶,因为在C中,char[] 应该分解为 char*,就像当我们将它们作为函数参数传递时那样,为什么这里不会如此,并且 char* 会失败?我的意思是,将 char demo[] 作为函数参数传递与将 char demo* 传递是否相同?为什么这里的结果不同?

请帮助我。我感觉自己甚至还没有理解C语言的基础知识。我非常失望。谢谢!


12
数组不是指针,指针也不是数组。 - William Pursell
但是,作为函数的数组参数会退化成指针。 - Micha Wiedenmann
6
哦,我的天啊,那个标题。请保持简短而简洁。 - Richard J. Ross III
@NeerajT,目前就是这样。我的标题有语法错误,我已经修复了。个人认为它不可能再变得更好了。 - Richard J. Ross III
3
有几个投票将这个帖子标记为重复,但没有提供它所谓的重复问题的链接。有许多关于数组与指针的问题,但这里的问题是一个类型为 char* 的复合字面量。 - Keith Thompson
显示剩余10条评论
5个回答

8
你的第三个例子:
printf("%s",(char *){'H','i','\0'});

严格来讲,这并不符合法律要求(严格来说这是一种约束违规),编译时应该会收到至少一个警告。当我使用默认选项用gcc编译时,我收到了6个警告:

c.c:3:5: warning: initialization makes pointer from integer without a cast [enabled by default]
c.c:3:5: warning: (near initialization for ‘(anonymous)’) [enabled by default]
c.c:3:5: warning: excess elements in scalar initializer [enabled by default]
c.c:3:5: warning: (near initialization for ‘(anonymous)’) [enabled by default]
c.c:3:5: warning: excess elements in scalar initializer [enabled by default]
c.c:3:5: warning: (near initialization for ‘(anonymous)’) [enabled by default]
printf 的第二个参数是一个复合字面量。虽然将类型设置为 char* 的复合字面量是合法的(但奇怪的),但在这种情况下,复合字面量的初始化列表部分是无效的。
在打印警告后,gcc 似乎执行的操作是:(a) 将表达式 'H'(具有 int 类型)转换为 char*,产生垃圾指针值,以及 (b) 忽略初始化元素 'i''\0' 的剩余部分。结果是一个 char* 指针值,它指向(可能虚拟的)地址 0x48 - 假设基于 ASCII 字符集。
忽略多余的初始值是有效的(但值得发出警告),但是从 intchar* 没有隐式转换(除了空指针常量的特殊情况,本例中不适用)。gcc 发出警告已经完成了它的工作,但我认为它可以并且应该用致命错误消息拒绝它。使用 -pedantic-errors 选项会这样做。
如果编译器警告你关于这些行,你应该在你的问题中包含这些警告。如果没有,要么提高警告级别,要么换一个更好的编译器。
更详细地介绍每个情况发生的内容:
printf("%s","Hi");
"%s""Hi"这样的C字符串字面量会创建一个匿名的静态分配的char数组。(该对象并非const,但试图修改它会产生未定义的行为;虽然这不是理想的,但出于历史原因而存在。)添加了一个终止符'\0'使其成为有效的字符串。
在大多数情况下(例外情况是当它是一元sizeof&运算符的操作数时,或者当它是用于初始化数组对象的初始化器中的字符串字面量时),具有数组类型的表达式会被隐式转换为(“衰减”为)指向数组第一个元素的指针。因此,传递给printf的两个参数的类型是char*printf使用这些指针来遍历各自的数组。
printf("%s",(char[]){'H','i','\0'});

这里使用了C99(ISO C标准的1999年版)新增的一个特性,叫做复合字面量。它类似于字符串字面量,可以创建一个匿名对象并引用该对象的值。复合字面量的形式如下:

( type-name ) { initializer-list }

当对象具有指定类型并被初始化为初始化程序列表给定的值时,它就会被创建。

以上内容几乎等同于:

char anon[] = {'H', 'i', '\0'};
printf("%s", anon);

需要注意的是,printf函数的第二个参数是一个数组对象,并且它会自动“衰变”成指向该数组第一个元素的指针;printf函数会使用该指针来遍历整个数组。

最后,介绍一下:

printf("%s",(char*){'A','B','\0'});

正如你所说,这个失败得很彻底。复合字面量的类型通常是数组或结构体(或联合体);我实际上没有想到它可能是一个标量类型,比如指针。以上几乎等同于:

char *anon = {'A', 'B', '\0'};
printf("%s", anon);

显然,`anon` 是 `char*` 类型,这正是 `printf` 期望使用的 `"%s"` 格式。但它的初始值是什么?
标准要求标量对象的初始化器必须是一个单一表达式,可选地用大括号括起来。但由于某种原因,该要求在“语义学”下,因此违反它不是约束性违规;它只是未定义的行为。这意味着编译器可以做任何它想做的事情,并且可能会或可能不会发出诊断报告。gcc 的作者似乎决定发出警告并忽略列表中除第一个初始化程序以外的所有初始化程序。
在那之后,它变成了等价于:
char *anon = 'A';
printf("%s", anon);

常量'A'的类型为int(出于历史原因,它的类型是int而不是char,但相同的参数适用于任何一种类型)。从intchar*没有隐式转换,实际上上述初始化程序是一个约束违规。这意味着编译器必须发出诊断(gcc会),并且可以拒绝程序(gcc不会,除非您使用-pedantic-errors)。一旦发出诊断,编译器可以做任何它想做的事情;行为未定义(在这一点上有一些语言律师上的分歧,但这并不重要)。gcc选择将A的值从int转换为char* (可能是出于历史原因,回到比今天更少强类型的C时代),从而产生了一个具有表示形式0x000000410x0000000000000041垃圾指针。

然后将此垃圾指针传递给printf,它尝试使用它来访问内存中该位置的字符串。结果就是一片混乱。

有两件事情很重要:

  1. 如果您的编译器打印警告,请特别注意。gcc特别为许多我认为应该是致命错误的事情发出警告。除非您充分理解警告的含义并且足够了解超过编译器作者的知识,否则永远不要忽略警告。

  2. 数组和指针是非常不同的东西。C语言的几个规则似乎都在暗示它们是相同的。你可以进行临时的假设数组只是指针的伪装,但这种假设最终会反噬你。阅读comp.lang.c FAQ第六部分;它比我还能更好地解释数组和指针之间的关系。


@Thokchom 不,它不会。通常,“Hi”将是指向静态数据中分配的字符串的指针(因此是const字符串),而复合字面量将始终在堆栈上分配,并且它将是可变的。 - Richard J. Ross III
@RichardJ.RossIII,您的意思是(char[]){'H','i','\0'}被翻译成类型char*,而"Hi"被翻译成类型const char* - Thokchom
@RichardJ.RossIII (char[]){'H','i','\0'} 转换为 char* 类型,对吗?否则它怎么能作为 %s 的参数呢? - Thokchom
@RichardJ.RossIII 你所说的原始指针是指 char* 类型的原始指针吗?或者像你之前关于 "Hi" 的描述一样,是指 const char* 类型的原始指针? - Thokchom
我喜欢把指针想象成内存中某个东西的地址。它是机器硬件固有的概念,每台机器处理它的方式可能略有不同,但都很相似。然后你需要将C语言所做的映射到那个概念上,因为C语言是一个“编译器”,它生成机器的原始指令。 - Lee Meador
显示剩余8条评论

7
关于片段#2:
这段代码能够工作是因为C99中有一个新特性,叫做复合字面量。你可以在多个地方阅读到它们,包括GCC文档, Mike Ash的文章和一些谷歌搜索。
基本上,编译器在堆栈上创建了一个临时数组,并用3个字节填充它 - 0x48, 0x690x00。一旦创建了这个临时数组,它就会被衰减成一个指针并传递给printf函数。关于复合字面量非常重要的一点是,默认情况下它们不是const,像大多数C字符串一样。
关于片段#3:
您实际上并没有创建数组 - 您正在将标量初始化器中的第一个元素转换为指针,这种情况下是H0x48。 通过将printf语句中的%s更改为%p,您可以看到输出结果如下:

0x48
因此,您必须非常小心地处理复合文字 - 它们是强大的工具,但很容易被它们搞砸。

请勿介意回滚。由于问题是用我自己的话表述的,并且有一些微妙之处,因此我几乎无法回忆起我的意图。 - Thokchom
请查看我在Keith Thompson的回答下所问的问题。如果您能澄清这一点,我会很感激。 - Thokchom

3

第三个数组包含十六进制字节。(我们不知道第四个字节是什么):

48 49 00 xx

在第二种情况下,当它传递该数组的内容时,它将这些字节作为要打印的字符串的地址。这取决于这4个字节如何在您的实际 CPU 硬件中转换为指针,但假设它说“414200FF”是地址(因为我们猜测第四个字节是0xFF。我们也假设指针的长度为4个字节和字节序等等。答案无关紧要,但其他人可以详细说明。

注意:其他答案中有人认为它将0x48扩展为(int) 0x00000048并将其称为指针。可能是这样。但如果 GCC 这样做了,而 @KiethThompson 没有说他检查生成的代码,那就不意味着其他 C 编译器会做同样的事情。结果是一样的。

这被传递到 printf() 函数中,它尝试去该地址获取一些字符以打印。(段错误发生是因为该地址可能不存在于机器上,并且未被分配给您的进程进行读取。)

在第2种情况下,它知道它是一个数组而不是指针,因此传递存储字节的内存地址,printf() 可以这样做。

有关更正式的语言,请参见其他答案。

需要考虑的一件事是,至少有些 C 编译器可能不知道从调用 printf 到调用任何其他函数的调用。因此,它获取"格式字符串"并为该调用存储指针(恰好是字符串),然后接受第二个参数并根据函数的声明存储它获取的任何内容,无论是 intchar 还是指针。然后函数根据相同的声明从调用者放置它们的位置中提取这些内容。第二个及更多参数的声明必须非常通用,才能接受指针、int、double 和所有可能存在的不同类型。(我所说的是编译器可能不会在决定如何处理第二个和后续参数时查看格式字符串)

对于以下情况,这可能很有趣:

printf("%s",{'H','i','\0'});
printf("%s",(char *)(char[]){'H','i','\0'}); // This works according to @DanielFischer

预测?


printf("%s",(char *)(char[]){'H','i','\0'});是可行的。你将 char[](复合文字)转换为char*(虽然这种转换本来就会自动完成),完全有效,没有问题。 - Daniel Fischer
@DanielFischer,我需要一些进一步的澄清,这些是我在问题中没有清楚提到的。我已经在Keith的答案下面作为评论提到了这些内容。你能花一分钟时间为这些问题发布你自己的答案吗? - Thokchom
@DanielFischer 为了明确表述 1) 因为 %s 需要一个 char* 参数,这是否意味着 (char[]){'H','i','\0'} 最终会转换为类型 char*2) 在所有方面,(char[]){'H','i','\0'} 是否与 "Hi" 完全相同?我们是否可以在任何需要使用字符串 "Hi" 的地方使用它,例如作为库函数的参数,如 strlen() 或在指针赋值时?由于从类型 char[]char* 的转换/分解,它是否保证是类型 char* - Thokchom

2
在每种情况下,编译器都会创建一个初始化的char[3]类型的对象。在第一种情况下,它将该对象视为数组,因此将指向其第一个元素的指针传递给函数。在第二种情况下,它将该对象视为指针,因此传递该对象的值。printf期望一个指针,当被视为指针时,该对象的值是无效的,因此程序在运行时崩溃。

2
“它将对象视为指针”是什么意思?这是像Lee Meador怀疑的那样精确的字节数组内容吗? - Micha Wiedenmann
指针是按值传递的。通过传递指向第一个元素的指针来传递数组。强制类型转换告诉编译器将对象视为指针,因此它通过值传递,因为指针是按值传递的。 - William Pursell
@WilliamPursell 在 C 语言中没有对象,但在 C++ 中有。那么 object 这个词具体指的是什么? - RAM
1
@NeerajT C语言确实有对象,但它们与C++中的对象不同。一个“对象”是一块内存。来自comp.lang.c faq的解释:“任何可以被C程序操作的数据片段:简单变量、数组、结构体、malloc分配的内存等等。” - William Pursell

-1
第三个版本甚至不应该编译。 ''H'不是指针类型的有效初始化程序。 GCC默认情况下会给出警告但不会报错。

它可以编译,因为它实际上是一个有效的C程序。根据标准,标量初始化器可以具有多余的元素,这些元素应该被忽略。 - Richard J. Ross III
1
“H”仍不是“char”的有效初始化器。除了空指针常量的特殊情况外,int(“H”的类型)到“char”没有隐式转换。 - Keith Thompson
不,@RichardJ.RossIII,“标量的初始化器应该是一个单独的表达式,可以用括号括起来。”过多的初始化器会引发未定义的行为。编译器不需要接受它。 - Daniel Fischer
事实上,过多的初始化程序也是一个问题,但不是我提到的那个。 - R.. GitHub STOP HELPING ICE

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