为什么printf(“%s”,(char []){‘H’,’i’,’ 0′})用作printf(“%s”,“Hi”),但是printf(“%s”,(字符*){‘H’,’I’,’ 0′});失败?

Why printf(“%s”,(char[]){'H','i',''}) works as printf(“%s”,“Hi”), but printf(“%s”,(char*){'H','i',''}); fails?

本问题已经有最佳答案,请猛点这里访问。

我真的需要帮助。它动摇了我在C的基础。详细的答案将非常感谢。我已经把我的问题分为两个部分。

A:为什么printf("%s",(char[]){'H','i','\0'});和传统的printf("%s","Hi");一样工作和打印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'}会出现大故障,即使有警告也会出现SEG故障?我很惊讶,因为在C语言中,char[]不应该分解成char*,就像我们在函数参数中传递它一样,为什么在这里不这样做,char*给出失败?我的意思是,将char demo[]作为参数传递给与char demo*相同的函数不是吗?为什么这里的结果不一样?

请帮我解决这个问题。我觉得我还没有理解C的基本知识。我很失望。谢谢!!


第三个例子:好的。

1
printf("%s",(char *){'H','i','\0'});

甚至都不合法(严格地说,这是违反约束的行为),在编译它时,您应该至少得到一个警告。当我使用gcc和默认选项编译它时,得到了6个警告:好的。

1
2
3
4
5
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)将类型为int的表达式'H'转换为char*,生成一个垃圾指针值,以及(b)忽略初始值设定项元素'i''\0'的其余部分。结果是一个char*指针值,指向(可能是虚拟的)地址0x48,假设是基于ASCII的字符集。好的。

忽略多余的初始值设定项是有效的(但值得警告),但是没有从intchar*的隐式转换(除了在这里不适用的空指针常量的特殊情况)。GCC已经通过发出警告来完成它的工作,但是它可以(并且imho应该)以致命的错误消息拒绝它。它将使用-pedantic-errors选项来实现。好的。

如果编译器警告您这些行,您应该在您的问题中包含这些警告。否则,要么提高警告级别,要么得到更好的编译器。好的。

更详细地了解三种情况中的每种情况:好的。

1
printf("%s","Hi");

"%s""Hi"这样的C字符串文字创建一个匿名静态分配的char数组。(此对象不是const,但试图修改它的行为未定义;这不理想,但有历史原因。)添加终止'\0'空字符使其成为有效字符串。好的。

在大多数上下文中(例外情况是它是一元sizeof&运算符的操作数,或者是用于初始化数组对象的初始值设定项中的字符串文字),数组类型的表达式被隐式转换为("decays to")指向数组的第一个指针元素。因此,传递给printf的两个参数是char*类型;printf使用这些指针遍历各自的数组。好的。

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

这使用了由c99(1999年版的iso c标准)添加到语言中的一个特性,称为复合文字。它类似于字符串文字,因为它创建一个匿名对象并引用该对象的值。复合文字的形式如下:好的。

1
( type-name ) { initializer-list }

对象具有指定的类型并初始化为初始值设定项列表给定的值。好的。

以上几乎等同于:好的。

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

同样,printf的第二个参数是指数组对象,它"衰减"到指向数组第一个元素的指针;printf使用该指针遍历数组。好的。

最后,这个:好的。

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

正如你所说,失败的时间太长了。复合文字的类型通常是数组或结构(或联合);实际上我没有想到它可以是标量类型,如指针。以上几乎等同于:好的。

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

显然,anonchar*类型,这是printf"%s"格式的期望。但初始值是多少?好的。

标准要求标量对象的初始值设定项是单个表达式,可以选择用大括号括起来。但是由于某种原因,这个需求是在"语义"下的,所以违反它不是一个约束违反;它只是未定义的行为。这意味着编译器可以做它喜欢做的任何事情,并且可能或可能不会发出诊断。GCC的作者显然决定发出警告,忽略列表中除了第一个初始值设定项之外的所有内容。好的。

之后,它就相当于:好的。

1
2
char *anon = 'A';
printf("%s", anon);

常数'A'int型(出于历史原因,它是int而不是char型,但这两种说法都适用)。没有从intchar*的隐式转换,事实上,上述初始值设定项是违反约束的。这意味着编译器必须发出一个诊断(gcc会发出),并且可能拒绝程序(gcc不会发出,除非使用-pedantic-errors)。一旦诊断被发出,编译器就可以做它喜欢做的任何事情;行为是未定义的(在这一点上有一些语言律师的分歧,但实际上并不重要)。GCC选择将A的值从int转换为char*(可能是出于历史原因,追溯到C的强类型比现在还要弱的时候),从而产生一个垃圾指针,其表示形式可能类似于0x00000041或0x0000000000000000041`。好的。

然后该垃圾指针被传递给printf,后者试图使用它访问内存中该位置的字符串。欢闹随之而来。好的。

要记住两件重要的事情:好的。

  • 如果编译器打印警告,请密切注意它们。GCC尤其对许多事情发出警告,认为IMHO应该是致命错误。除非您理解警告的含义,否则不要忽略警告,因为您的知识足以覆盖编译器作者的警告。好的。

  • 数组和指针是非常不同的东西。C语言的一些规则似乎合谋在一起,使其看起来像是相同的。你可以暂时不去假设数组只是伪装的指针,但是这个假设最终会回来咬你。阅读comp.lang.c常见问题解答的第6节;它比我更好地解释了数组和指针之间的关系。好的。

  • 好啊。


    关于代码段2:

    由于c99中的一个新特性(称为复合文字),代码工作正常。你可以在几个地方阅读它们,包括GCC的文档,MikeAsh的文章,以及谷歌搜索。

    实际上,编译器在堆栈上创建一个临时数组,并用3个字节填充它——0x480x690x00。该临时数组一旦创建,就衰减为指针并传递给printf函数。关于复合文字,需要注意的一件非常重要的事情是,它们与大多数C字符串一样,默认情况下不是const

    关于代码段3:

    实际上,您根本没有创建数组——您正在将标量初始化器中的第一个元素(在本例中是H0x48转换为指针。您可以通过将printf语句中的%s改为%p来看到这一点,它为我提供了以下输出:

    1
    0x48

    因此,你必须非常小心地处理复合文字——它们是一个强大的工具,但很容易用它们把你自己射到脚上。


    (好吧…有人彻底改写了这个问题。修改答案。)

    3数组包含十六进制字节。(我们不知道第四个问题):

    48 49 00 00 XX

    当它传递该数组的内容时,仅在第二种情况下,它将这些字节作为要打印的字符串的地址。这取决于这4个字节如何转换为实际CPU硬件中的指针,但假设它说"414200FF"是地址(因为我们将猜测第4个字节是0xFF)。无论如何,我们都在弥补这一点。)我们还假设指针的长度为4个字节,并且是尾数顺序,诸如此类。答案无关紧要,但其他人可以自由解释。

    注意:其他答案之一似乎认为它需要0x48并将其扩展到(int)0x00000048,并将其称为指针。可以是。但是,如果gcc做到了这一点,@kietthth汤普森没有说他检查了生成的代码,这并不意味着其他一些C编译器也会做同样的事情。结果都是一样的。

    它将被传递到printf()函数,并尝试转到该地址以获得一些要打印的字符。(SEG故障发生是因为该地址可能不在机器上,也没有分配给您的读取过程。)

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

    请参阅其他关于更正式语言的答案。

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

    看看会发生什么可能很有趣:

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

    预言?


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


    第三个版本甚至不应该编译。'H'不是指针类型的有效初始值设定项。GCC会警告您,但默认情况下不会出错。