char a [的语义

我最近在向同事解释原因时感到很尴尬

char a[100]; scanf("%s", &a); // notice a & in front of 'a' 

是非常糟糕的,稍微好一点的方法是:

 char a[100]; scanf("%s", a); // notice no & in front of 'a' 

好。 对于每个人都准备告诉我为什么不应该出于安全原因使用scanf:放松。 这个问题实际上是关于“&a”与“a”的含义。

问题是,在我解释了为什么它不起作用之后,我们尝试了它(使用gcc)并且它起作用=))。 我赶紧跑了

 printf("%p %p", a, &a); 

它会打印两次相同的地址。

任何人都可以向我解释发生了什么事吗?

那么, &a案例应该是显而易见的。 您可以完全按预期获取数组的地址。 a有点微妙,但答案是a 数组。 正如任何C程序员所知,数组有一种倾向,即在最轻微的挑衅时退化为指针, 例如将其作为函数参数传递时。

因此scanf("%s", a)需要一个指针,而不是数组,因此该数组会退化为指向数组第一个元素的指针。

当然scanf("%s", &a)可以工作,因为它明确地是数组的地址。

编辑:哎呀,看起来我完全没有考虑scanf实际上期望的参数类型。 两种情况都会产生指向同一地址但指针类型不同的指针。 (指向char的指针,指向字符数组的指针)。

而且我很高兴地承认我对省略号(…)的语义知之甚少,我总是像瘟疫那样避免,所以看起来像转换到最终使用的scanf类型可能是未定义的行为。 阅读评论和litb的答案。 你通常可以相信他能把这些东西弄好。 ;)

好吧,scanf希望在看到“%s”时将char *指针作为下一个参数。 但是你给它的是一个指向char [100]的指针。 你给它一个char(*)[100] 。 它根本不能保证工作,因为编译器当然可以对数组指针使用不同的表示。 如果您打开gcc的警告,您将看到显示正确的警告。

当你提供一个参数对象,这个参数是一个参数,在函数中没有列出的参数(因此,就像scanf的情况一样,当格式字符串后面有vararg样式的“…”参数时),数组将退化为指向其第一个元素的指针。 也就是说,编译器将创建一个char*并将其传递给printf。

因此, 永远不要使用&a并使用“%s”将其传递给scanf。 好的编译器,如你的,会正确警告你:

警告:参数与相应的格式字符串转换不兼容

当然, &a(char*)a存储了相同的地址。 但这并不意味着你可以互换地使用&a(char*)a


一些标准引号特别显示指针参数如何自动地转换为void* ,以及整个事物是如何未定义的行为。

除非它是sizeof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串文字,否则将类型为”array of type”的表达式转换为类型为”指针的表达式type”指向数组对象的初始元素。 ( 6.3.2.1/3

所以,总是这样做 – 当类型可能不同时,在监听有效情况时,不再明确地提到它。

函数原型声明符中的省略号表示法导致参数类型转换在最后声明的参数之后停止。 默认参数提升是在尾随参数上执行的。 ( 6.5.2.2/7

关于va_arg行为如何提取传递给printf的参数,这是一个vararg函数,我强调了这一点( 7.15.1.1/2 ):

每次调用va_arg宏都会修改ap,以便依次返回连续参数的值。 参数类型应该是一个指定的类型名称,这样只需通过post a * a to type就可以获得指向具有指定类型的对象的指针类型。 如果没有实际的下一个参数,或者type与实际的下一个参数的类型不兼容(根据默认参数提升而提升),则行为是未定义的 ,除了以下情况:

  • 一种类型是有符号整数类型,另一种类型是相应的无符号整数类型,并且该值可在两种类型中表示;
  • 一种类型是指向void的指针,另一种是指向字符类型的指针。

那么,这是默认参数提升的内容:

如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将具有float类型的参数提升为double。 这些被称为默认参数促销。 ( 6.5.2.2/6

我用C编程已经有一段时间,但这是我的2c:

char a[100]没有为数组的地址分配单独的变量,因此内存分配如下所示:

  ---+-----+--- ...|0..99|... ---+-----+--- ^ a == &a 

为了比较,如果数组是malloc’d,则指针有一个单独的变量,并且a != &a

 char *a; a = malloc(100); 

在这种情况下,内存看起来像这样:

  ---+---+---+-----+--- ...| a |...|0..99|... ---+---+---+-----+--- ^ ^ &a != a 

K&R第二版。 p.99很好地描述了它:

索引和指针算术之间的对应关系非常接近。 根据定义,类型数组的变量或表达式的值是数组的元素零的地址。 因此在赋值后pa=&a[0]; paa具有相同的值。 由于数组的名称是初始元素位置的同义词,因此赋值pa=&a[0]也可以写为pa=a;

AC数组可以隐式转换为指向其第一个元素的指针(C99:TC36.3.2.1§3),即很多情况下a (其类型为char [100] )的行为与&a[0]相同&a[0] (其类型为char * )。 这解释了为什么传递as参数会起作用。

但是不要开始认为这种情况总是如此:数组和指针之间存在重要差异,例如关于赋值, sizeof以及我现在无法想到的任何其他内容……

&a实际上是这些陷阱之一:这将创建一个指向数组的指针,即它的类型为char (*) [100] (而不是 char ** )。 这意味着&a&a[0]将指向相同的内存位置,但将具有不同的类型。

据我所知,这些类型之间没有隐式转换,也不保证它们也具有兼容的表示。 我能找到的只有C99:TC36.2.5§27,它没有说明关于数组的指针:

[…]指向其他类型的指针不需要具有相同的表示或对齐要求。

但也有6.3.2.3§7:

[…]当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。 结果的连续递增(直到对象的大小)产生指向对象的剩余字节的指针。

所以演员(char *)&a应该按预期工作。 实际上,我在这里假设数组的最低寻址字节将是其第一个元素的最低寻址字节 – 不确定这是否有保证,或者编译器是否可以在数组前面添加任意填充,但是如果是这样,那将是非常奇怪的……

无论如何,这仍然需要转换为char * (或void * – 标准保证这些类型具有兼容的表示)。 问题是除了默认参数提升之外,不会有任何转换应用于变量参数,即你必须自己明确地进行转换。


总结一下:

&a的类型为char (*) [100] ,它可能具有与char *不同的位表示。 因此,必须由程序员进行显式强制转换,因为对于变量参数,编译器无法知道应该将值转换为什么。 这意味着只会进行默认参数提升,正如litb指出的那样,它不包括转换为void * 。 它遵循:

  • scanf("%s", a); – 好
  • scanf("%s", &a); – 不好
  • scanf("%s", (char *)&a); – 应该可以

对不起,有点偏离主题:

这让我想起了8年前我在全职编写C时读到的一篇文章。 我找不到文章,但我认为它的标题是“数组不是指针”或类似的东西。 无论如何,我确实遇到了这个C数组和指针常见问题 ,这是有趣的阅读。

char [100]是100个相邻char的复杂类型,其sizeof等于100。

被转换为指针( (void*) a ),此变量产生第一个char的地址。

引用此类型的变量( &a )会产生整个变量的地址,而后者恰好也是第一个char的地址