你的第三个例子:
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 字符集。
忽略多余的初始值是有效的(但值得发出警告),但是从
int
到
char*
没有隐式转换(除了空指针常量的特殊情况,本例中不适用)。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
,但相同的参数适用于任何一种类型)。从
int
到
char*
没有隐式转换,实际上上述初始化程序是一个约束违规。这意味着编译器
必须发出诊断(gcc会),并且
可以拒绝程序(gcc不会,除非您使用
-pedantic-errors
)。一旦发出诊断,编译器可以做任何它想做的事情;行为未定义(在这一点上有一些语言律师上的分歧,但这并不重要)。gcc选择将
A
的值从
int
转换为
char*
(可能是出于历史原因,回到比今天更少强类型的C时代),从而产生了一个具有表示形式
0x00000041
或
0x0000000000000041
的
垃圾指针。
然后将此垃圾指针传递给printf
,它尝试使用它来访问内存中该位置的字符串。结果就是一片混乱。
有两件事情很重要:
如果您的编译器打印警告,请特别注意。gcc特别为许多我认为应该是致命错误的事情发出警告。除非您充分理解警告的含义并且足够了解超过编译器作者的知识,否则永远不要忽略警告。
数组和指针是非常不同的东西。C语言的几个规则似乎都在暗示它们是相同的。你可以进行临时的假设数组只是指针的伪装,但这种假设最终会反噬你。阅读comp.lang.c FAQ第六部分;它比我还能更好地解释数组和指针之间的关系。
char*
的复合字面量。 - Keith Thompson