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

时间:2013-05-17 17:10:22

标签: c arrays string pointers casting

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

答:为什么printf("%s",(char[]){'H','i','\0'});能像传统的Hi一样工作并打印printf("%s","Hi");?我们可以使用(char[]){'H','i','\0'}代替我们的C代码中的任何地方都有"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-faults警告?它让我感到惊讶,因为在C中,char[]不应该分解为char*,就像我们在函数参数中传递它一样,为什么它在这里没有这样做{{1}失败?我的意思是,是不是将char*作为参数传递给与char demo[]相同的函数?为什么结果在这里不相同?

请帮帮我。我觉得我还没有理解C.的基本知识。我非常失望。谢谢!!

5 个答案:

答案 0 :(得分: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*的隐式转换(除了空指针常量的特殊情况,它没有&#39 ; t适用于此处)。 gcc通过发出警告完成了它的工作,但它可以(和恕我直言)应该用一个致命的错误消息拒绝它。它将使用-pedantic-errors选项执行此操作。

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

详细了解三种情况中的每一种情况:

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

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

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

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

这使用了C99(1999年版的ISO C标准)添加到语言中的功能,称为复合文字。它类似于字符串文字,因为它创建一个匿名对象并引用该对象的值。复合文字的格式为:

( 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"。但是最初的价值是什么?

标准要求标量对象的初始值设定项为单个表达式,可选择用花括号括起来。但由于某种原因,该要求属于" Semantics",因此违反它并不违反约束条件;它只是未定义的行为。这意味着编译器可以执行任何喜欢的操作,并且可能会也可能不会发出诊断信息。 gcc的作者显然决定发出警告并忽略列表中的第一个初始化程序。

之后,它等同于:

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

常量'A'属于int类型(由于历史原因,它是int而不是char,但同样的参数将适用于任何一种方式) 。没有从intchar*的隐式转换,实际上上面的初始化程序是违反约束的。这意味着编译器必须发出诊断(gcc确实),并且可能拒绝该程序(除非您使用-pedantic-errors,否则gcc不会)。一旦发出诊断,编译器就可以做任何喜欢的事情;这种行为是不确定的(在这一点上有一些语言 - 律师的不同意见,但这并不重要)。 gcc选择A的值从int转换为char*(可能是出于历史原因,回到当C的输入类型比它的类型更弱时今天),产生一个 garbage 指针,其表示可能看起来像0x00000041或0x0000000000000041`。

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

要记住两件重要的事情:

  1. 如果您的编译器打印警告,请密切关注它们。 gcc特别发出许多警告,恕我直言应该是致命的错误。 从不忽略警告,除非您了解警告的含义,足以让您的知识覆盖编译器的作者。

  2. 数组和指针是非常不同的东西。 C语言的几个规则似乎合谋使它看起来像它们一样。您可以暂时逃避假设数组只不过是伪装的指针,但这种假设最终会回来咬你。阅读comp.lang.c FAQ的第6部分;它比我更好地解释了数组和指针之间的关系。

答案 1 :(得分:7)

关于代码段#2:

代码的工作原理是因为C99中的一个新功能,称为复合文字。您可以在多个位置阅读相关内容,包括GCC's documentationMike Ash's article以及一些谷歌搜索。

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

关于代码段#3:

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

0x48

因此,你必须非常小心你使用复合文字做什么 - 它们是一个强大的工具,但很容易用它们射击自己。

答案 2 :(得分:3)

(好吧......有人完全重写了这个问题。改写答案。)

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

48 49 00 xx

当它传递该数组的内容时,仅在第二种情况下,它将这些字节作为要打印的字符串的地址。这取决于这4个字节如何转换为实际CPU硬件中的指针,但让我们说“414200FF”是地址(因为我们猜测第4个字节是0xFF。无论如何我们都在做这个。)我们也假设一个指针是4个字节长和一个字节顺序和类似的东西。答案无关紧要,但其他人可以自由阐述。

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

将传递给printf()函数,它会尝试转到该地址以获取要打印的字符。 (发生Seg故障是因为该地址可能不在机器上,并且未分配给您的过程进行读取。)

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

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

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

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

char

预测?

答案 3 :(得分:2)

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

答案 4 :(得分:-1)

第三个版本甚至不应该编译。 'H'不是指针类型的有效初始值设定项。 GCC默认为您提供警告但不是错误。