我真的需要帮助。它动摇了我在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的基本知识。我很失望。谢谢!!
- 数组不是指针,指针不是数组。
- 但是函数的数组参数退化为指针。
- @我早就在我的笔记本里注意到了这一点,并在上面读了很多遍。
- 但在这两种情况下,对printf()的调用都将传递一个指针。在第二种情况下,我怀疑它传递的是数组的确切字节内容,而在第一种情况下,传递的是数组的地址。
- @如果你告诉我这是如何解释我这个问题的话,这会有帮助的。
- 数组和指针之间的转换是隐式的,但这并不意味着它们是相同的。
- 哦,天哪,那个头衔。请简明扼要。
- 我在编辑你的文章时花了不少时间来提高可读性。请随意扩展我的编辑内容,但不要只是回滚它。你的文章很难以原版阅读。
- @Neerajt这是一个无效的编辑-问题不在于什么时候发生了segfault,而在于它为什么发生。该死的机器人评论家。
- @RichardJ.RossIII那么我们应该把它改为"为什么C中的printf会导致segfault"?
- @尼拉吉特,这就是现在的情况。我的书名有一个语法错误,我改正了。就我个人而言,我不知道怎么会好得多。
- 你说你"无视警告"。请在您的问题中包括警告。
- 有几张选票将此作为副本关闭,但没有链接到一个问题,该问题被认为是副本。关于数组和指针有很多问题,但这里的问题是类型为char*的复合文本。
- @丹尼尔菲舍尔和所有人,我很遗憾在我姐姐占用电脑的时候,我不得不在发帖后离开。让我检查一下答案。
- @罗西三世保持冠军头衔,恢复休息,因为在我的问题中有很多细微的差别,我需要。
第三个例子:好的。
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的字符集。好的。
忽略多余的初始值设定项是有效的(但值得警告),但是没有从int到char*的隐式转换(除了在这里不适用的空指针常量的特殊情况)。GCC已经通过发出警告来完成它的工作,但是它可以(并且imho应该)以致命的错误消息拒绝它。它将使用-pedantic-errors选项来实现。好的。
如果编译器警告您这些行,您应该在您的问题中包含这些警告。否则,要么提高警告级别,要么得到更好的编译器。好的。
更详细地了解三种情况中的每种情况:好的。
像"%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 ); |
显然,anon是char*类型,这是printf对"%s"格式的期望。但初始值是多少?好的。
标准要求标量对象的初始值设定项是单个表达式,可以选择用大括号括起来。但是由于某种原因,这个需求是在"语义"下的,所以违反它不是一个约束违反;它只是未定义的行为。这意味着编译器可以做它喜欢做的任何事情,并且可能或可能不会发出诊断。GCC的作者显然决定发出警告,忽略列表中除了第一个初始值设定项之外的所有内容。好的。
之后,它就相当于:好的。
1 2
| char *anon = 'A';
printf("%s", anon ); |
常数'A'是int型(出于历史原因,它是int而不是char型,但这两种说法都适用)。没有从int到char*的隐式转换,事实上,上述初始值设定项是违反约束的。这意味着编译器必须发出一个诊断(gcc会发出),并且可能拒绝程序(gcc不会发出,除非使用-pedantic-errors)。一旦诊断被发出,编译器就可以做它喜欢做的任何事情;行为是未定义的(在这一点上有一些语言律师的分歧,但实际上并不重要)。GCC选择将A的值从int转换为char*(可能是出于历史原因,追溯到C的强类型比现在还要弱的时候),从而产生一个垃圾指针,其表示形式可能类似于0x00000041或0x0000000000000000041`。好的。
然后该垃圾指针被传递给printf,后者试图使用它访问内存中该位置的字符串。欢闹随之而来。好的。
要记住两件重要的事情:好的。
如果编译器打印警告,请密切注意它们。GCC尤其对许多事情发出警告,认为IMHO应该是致命错误。除非您理解警告的含义,否则不要忽略警告,因为您的知识足以覆盖编译器作者的警告。好的。
数组和指针是非常不同的东西。C语言的一些规则似乎合谋在一起,使其看起来像是相同的。你可以暂时不去假设数组只是伪装的指针,但是这个假设最终会回来咬你。阅读comp.lang.c常见问题解答的第6节;它比我更好地解释了数组和指针之间的关系。好的。
好啊。
- 我收到了你在回答中提到的确切警告!!
- 很不幸,你回答我的时候我不在,我想问一下:为什么江户十一〔五〕行?%s期望有一个char*型的论点,那么我是否可以得出(char[]){'H','i','\0'}最终分解为char*型的结论?最后,(char[]){'H','i','\0'}是否是"Hi"的确切替代品?
- 我必须强调我在上述评论的第二部分中要问的是什么。在C代码的所有上下文中,'(char[]){'H','i','\0'}`` mean the string 是"嗨"吗?比如strlen()、sizeof、strcpy()?
- @不是,不是。通常,"Hi"是指向静态数据中分配的字符串的指针(因此,const字符串),而复合文字将始终在堆栈上分配,并且它是可变的。
- @Richardj.RossIII你是说(char[]){'H','i','\0'}翻译成char*型,而"Hi"翻译成const char*型?
- @RichardJ.Rossii(char[]){'H','i','\0'}是否翻译成char*型,对吗?否则,它如何有资格成为%s的论据?
- @thokchom no,复合文字(例如(char[]) { ... })是一个数组,当传递给printf时,该数组将衰减为指针。"..."只是一个原始指针,如果分配给一个指针,它就变成一个数组。
- @你是说char*型的原始指针?或者像你之前所说的关于"Hi"的const char*和"Hi"一样?
- @在这种情况下,原始指针thokchom是char *类型,但是修改它是未定义的行为。它应该几乎总是被分配给const char *类型的变量,但是文本本身在默认情况下不是常量。C关于字符串的规则是一些最复杂的规则,因此在某个时刻很难遵循。当有疑问时,char *为常量,而char []为常量。
- @RichardJ.Rossiiii请过来聊聊。
- 让我们在聊天中继续讨论
- 我喜欢把指针看作是记忆中某个东西的地址。它是机器硬件固有的概念,每台机器的处理方式可能略有不同,但都是相似的。然后您必须映射C所做的与之相反的事情,因为C是一个"编译器",并为机器生成原始指令。
- 我不希望对这次投票的否决有什么解释,但我很感激。
关于代码段2:
由于c99中的一个新特性(称为复合文字),代码工作正常。你可以在几个地方阅读它们,包括GCC的文档,MikeAsh的文章,以及谷歌搜索。
实际上,编译器在堆栈上创建一个临时数组,并用3个字节填充它——0x48、0x69和0x00。该临时数组一旦创建,就衰减为指针并传递给printf函数。关于复合文字,需要注意的一件非常重要的事情是,它们与大多数C字符串一样,默认情况下不是const。
关于代码段3:
实际上,您根本没有创建数组——您正在将标量初始化器中的第一个元素(在本例中是H或0x48转换为指针。您可以通过将printf语句中的%s改为%p来看到这一点,它为我提供了以下输出:
因此,你必须非常小心地处理复合文字——它们是一个强大的工具,但很容易用它们把你自己射到脚上。
- +答案是1。请不要介意回滚。我几乎记不起我想问什么,因为问题是用我自己的话说的,并且有一些细微的差别。
- 请看看我在基思汤普森的回答下问他什么。如果你能澄清这一点,我会很高兴的。
(好吧…有人彻底改写了这个问题。修改答案。)
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 |
预言?
- printf("%s",(char *)(char[]){'H','i','\0'});将起作用。你把一个EDOCX1(复合文字)转换成一个char*(无论如何转换都会自动完成),完全有效,没问题。
- @丹尼尔菲舍尔,我需要进一步的澄清,我在我的问题中没有明确地提到。我在基思的回答下面提到了这些评论。你能花一分钟的时间来发表你自己的答案吗?
- @丹尼尔菲舍尔明确地说1)由于%s预期char*的论点,这是否意味着(char[]){'H','i','\0'}最终会转化为char*类型?2)(char[]){'H','i','\0'}在各方面是否与"Hi"完全相同?每当我们想使用字符串"Hi"like作为库函数(如strlen())的参数时,或者在分配给指针时,可以使用它吗?由于从char[]型到char*型的转换/分解,是否保证为char*型?
在每种情况下,编译器都会创建char[3]类型的初始化对象。在第一种情况下,它将对象视为数组,因此它将指向其第一个元素的指针传递给函数。在第二种情况下,它将对象视为指针,因此它传递对象的值。printf需要一个指针,当作为指针处理时,对象的值无效,因此程序在运行时崩溃。
- "它把对象当作指针"是什么意思?这是李美多怀疑的数组的确切字节内容吗?
- 指针按值传递。通过向第一个元素传递指针来传递数组。强制转换告诉编译器将对象视为指针,因此它按值传递对象,因为指针是按值传递的。
- WilliamPursell C中没有对象,但C++中没有。object这个词到底指什么?
- @ NeRaJT C确实有对象,但它们不同于C++中的对象。"对象"是一块内存。来自comp.lang.c常见问题解答:"任何可以由C程序操作的数据:一个简单的变量、一个数组、一个结构、一块malloc'ed内存等。"
第三个版本甚至不应该编译。'H'不是指针类型的有效初始值设定项。GCC会警告您,但默认情况下不会出错。
- 它确实编译,因为它实际上是一个有效的C程序。根据标准,标量初始值设定项可以有多余的元素,这些元素应该被忽略。
- 'H'仍然不是char*的有效初始值设定项。除了空指针常量的特殊情况外,没有从int('H'的类型)到char*的隐式转换。
- 不,@richardj.rossiiii,"标量的初始值设定项应该是单个表达式,可以选择用大括号括起来。"多余的初始化器调用未定义的行为。编译器不需要接受它。
- 实际上,过多的初始值设定项也是一个问题,但不是我提到的问题。