7

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

A:为什么和传统的一样printf("%s",(char[]){'H','i','\0'});工作和打印?我们可以在 C 代码中的任何地方使用它们的替代品吗?它们的意思是一样的吗?我的意思是,当我们用C 编写时,它通常意味着存储在内存中的某个地方并且一个指向它的指针被传递。看起来丑陋的也可以这样说。它们完全一样吗?Hiprintf("%s","Hi");(char[]){'H','i','\0'}"Hi""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*?为什么这里的结果不一样?

请帮我解决这个问题。我觉得我什至还没有理解 CI 的基本知识,非常失望。谢谢!

4

5 回答 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]

to 的第二个参数printf复合文字。具有 type 的复合文字是合法的(但很奇怪)char*,但在这种情况下,复合文字的初始化列表部分是无效的。

打印警告后,gcc 似乎正在做的是(a)将'H'类型为的表达式转换intchar*,产生一个垃圾指针值,以及(b)忽略初始化器元素的其余部分,'i'以及'\0'。结果是一个char*指向(可能是虚拟的)地址的指针值0x48——假设是基于 ASCII 的字符集。

忽略多余的初始化器是有效的(但值得警告),但没有从intto的隐式转换char*(除了空指针常量的特殊情况,这在此处不适用)。gcc 通过发出警告完成了它的工作,但它可能(而且恕我直言应该)用一个致命的错误消息拒绝它。它将通过该-pedantic-errors选项执行此操作。

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

更详细地了解在这三种情况下会发生什么:

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

AC 字符串字面量类似于"%s"or"Hi"创建一个匿名静态分配的数组char。(此对象不是const,但尝试修改它具有未定义的行为;这并不理想,但有其历史原因。)'\0'添加终止空字符以使其成为有效字符串。

数组类型的表达式,在大多数情况下(例外是当它是一元运算符sizeof&运算符的操作数,或者当它是用于初始化数组对象的初始化程序中的字符串文字时)被隐式转换为 ("decays to") a指向数组第一个元素的指针。所以传递给的两个参数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是 type char*,这是对格式printf的期望。"%s"但是初始值是多少?

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

之后,它就等同于:

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

常量'A'是类型int(由于历史原因,它是int而不是char,但同样的论点适用于任何一种方式)。没有从intto的隐式转换char*,实际上上面的初始化程序是违反约束的。这意味着编译器必须发出诊断(gcc 会),并且可能会拒绝程序(gcc 不会,除非您使用-pedantic-errors)。一旦发出诊断,编译器就可以为所欲为;行为未定义(在这一点上有一些语言律师的分歧,但这并不重要)。gcc 选择Afrom的值转换intchar*(可能由于历史原因,可以追溯到 C 的类型比现在更弱的时候),导致垃圾指针的表示可能看起来像0x000000410x0000000000000041`。

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

有两个重要的事情要记住:

  1. 如果您的编译器打印警告,请密切注意它们。gcc 特别针对许多事情发出警告,恕我直言应该是致命错误。除非您完全理解警告的含义,否则永远不要忽略警告,足以让您的知识覆盖编译器作者的知识。

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

于 2013-05-17T17:42:39.453 回答
7

关于片段#2:

该代码之所以有效,是因为 C99 中的一个新特性,称为复合文字。您可以在多个地方阅读它们,包括GCC 的文档Mike Ash 的文章以及一些 google 搜索。

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

关于片段#3:

您实际上根本没有创建数组 - 您正在将标量初始化程序中的第一个元素强制转换,在这种情况下是H, 或0x48指针。您可以通过将%sprintf 语句中%p

0x48

因此,在处理复合文字时必须非常小心——它们是一个强大的工具,但很容易用它们来打自己的脚。

于 2013-05-17T17:35:28.260 回答
3

(好的......有人完全修改了这个问题。修改了答案。)

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

48 49 00 XX

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

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

它被传递给 printf() 函数,并尝试去该地址以获取一些要打印的字符。(发生段错误是因为该地址可能不存在于机器上,并且无论如何都没有分配给您的进程以供读取。)

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

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

需要考虑的一件事是,至少某些 C 编译器可能不知道printf从对任何其他函数的调用中的调用。因此,它获取"format string"并存储调用的指针(恰好是一个字符串),然后获取第二个参数并存储根据函数声明获得的任何内容,无论是 anint或 achar还是指针称呼。然后,该函数根据相同的声明将它们从调用者放置的任何地方拉出来。第二个和更大参数的声明必须是真正通用的,才能接受指针、整数、双精度和所有可能存在的不同类型。(我的意思是编译器在决定如何处理第二个和后续参数时可能不会查看格式字符串。)

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

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

预测?

于 2013-05-17T17:20:40.630 回答
2

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

于 2013-05-17T17:22:19.400 回答
-1

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

于 2013-05-17T17:39:19.307 回答