我最近在向同事解释原因时为自己感到尴尬
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);
并打印两次相同的地址。
有人可以向我解释发生了什么事吗?
答案 0 :(得分:18)
嗯,&a
案例应该是显而易见的。您可以完全按预期获取数组的地址。
a
有点微妙,但答案是a
是数组。正如任何C程序员所知,当将数据作为函数参数传递时,数组会在最轻微的挑衅时退化为指针,例如 。
所以scanf("%s", a)
需要一个指针,而不是一个数组,所以数组会退化为指向数组第一个元素的指针。
当然scanf("%s", &a)
也有效,因为它明确地是数组的地址。
编辑:糟糕,看起来我完全没有考虑scanf实际上期望的参数类型。这两种情况都会产生指向同一地址但指向不同类型的指针。 (指向char的指针,指向字符数组的指针)。
我很高兴地承认我对省略号(...)的语义知之甚少,我总是像瘟疫一样避免这种语法,所以看起来转换为scanf最终使用的类型可能是未定义的行为。阅读评论和litb的答案。你通常可以信任他,让这些东西正确。 ;)
答案 1 :(得分:11)
嗯,当看到“%s”时,scanf期望将char *指针作为下一个参数。但是你给它的是指向char [100]的指针。你给它一个char(*)[100]
。它根本不能保证工作,因为编译器当然可以对数组指针使用不同的表示。如果您打开gcc的警告,您还会看到显示正确的警告。
当你提供一个参数对象,它是一个参数,在函数中没有列出的参数(因此,就像scanf的情况一样,当格式字符串后面有vararg样式的“...”参数时),数组将退化为指向其第一个元素的指针。也就是说,编译器将创建一个char*
并将其传递给printf。
所以,永远不会使用&a
,并使用“%s”将其传递给scanf。好的编译器,如你的,会正确警告你:
警告:参数与相应的格式字符串转换不兼容
当然,&a
和(char*)a
已存储相同的地址。但这并不意味着您可以互换使用&a
和(char*)a
。
一些标准引用特别显示指针参数不如何自动神奇地转换为void*
,以及整个事物是如何未定义的行为。
除非它是sizeof运算符或一元&的操作数。运营商,或者是 用于初始化数组的字符串文字,具有类型''数组类型''的表达式被转换为到类型为的指针''指向类型''的指针到数组对象的初始元素。 (
6.3.2.1/3
)
所以,总是这样做 - 在类型可能不同的情况下收听有效案例时,下面不再明确提及。
函数原型声明符中的省略号表示法导致参数类型转换在最后声明的参数之后停止。默认参数提升是在尾随参数上执行的。 (
6.5.2.2/7
)
关于va_arg
行为提取传递给printf的参数的行为,这是一个vararg函数,由我添加的重点(7.15.1.1/2
):
每次调用va_arg宏都会修改ap以便 连续参数的值依次返回。参数类型应为类型 指定的名称,以便只需通过将
*
固定为类型即可获得指向具有指定类型的对象的指针的类型。如果没有实际的下一个参数,或者type与实际的下一个参数的类型不兼容(根据默认参数提升而提升),行为是未定义的,除了以下情况:
- 一种类型是有符号整数类型,另一种类型是相应的无符号整数 类型,值可以在两种类型中表示;
- 一种类型是指向void 的指针,另一种是指向字符类型的指针。
嗯,这是默认参数提升的内容:
如果表示被调用函数的表达式具有不包含a的类型 原型,对每个参数执行整数提升,并对其进行参数化 将float类型提升为double。这些被称为默认参数 促销活动。 (
6.5.2.2/6
)
答案 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 2nd Ed. p.99很好地描述了它:
索引之间的对应关系 和指针算术非常接近。 根据定义,变量的值 或者类型数组的表达式是 数组元素零的地址。 因此在作业
pa=&a[0];
之后pa
和a
具有相同的值。以来 数组的名称是。的同义词 初始元素的位置, 作业pa=&a[0]
也可以 写作pa=a;
答案 3 :(得分:5)
AC数组可以隐式转换为指向其第一个元素的指针(C99:TC36.3.2.1§3),即很多情况下a
(类型为char [100]
)行为与&a[0]
(类型为char *
)的行为相同。这解释了为什么传递a
作为参数将起作用。
但是不要开始认为这种情况总是如此:数组和指针之间存在重要差异,例如关于赋值,sizeof
以及我现在无法想到的任何其他内容......
&a
实际上是其中一个陷阱:这将创建一个指向数组的指针,即它具有类型char (*) [100]
(和不 char **
)。这意味着&a
和&a[0]
将指向相同的内存位置,但会有不同的类型。
据我所知,这些类型之间没有隐式转换,也不保证它们也具有兼容的表示。我能找到的只有C99:TC36.2.5§27,它没有说明关于数组的指针:
[...]指向其他类型的指针不需要具有相同的表示或对齐要求。
但也有6.3.2.3§7:
[...]当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。
所以演员(char *)&a
应该按预期工作。实际上,我在这里假设数组的最低寻址字节将是其第一个元素的最低寻址字节 - 不确定这是否有保证,或者编译器是否可以在数组前面添加任意填充,但是如果是这样,那将是非常奇怪的......
无论如何,&a
仍需要转换为char *
(或void *
- 标准保证这些类型具有兼容的表示形式)。问题是除了默认参数提升之外,不会有任何转换应用于变量参数,即你必须自己明确地进行转换。
总结:
&a
的类型为char (*) [100]
,其位代表可能与char *
不同。因此,显式强制转换必须由程序员完成,因为对于变量参数,编译器无法知道应该将值转换为什么。这意味着只会执行默认参数提升,正如 litb 指出的那样,不包括转换为void *
。由此得出:
scanf("%s", a);
- 好scanf("%s", &a);
- 糟糕scanf("%s", (char *)&a);
- 应该没问题答案 4 :(得分:4)
抱歉,有点偏离主题:
这让我想起了8年前我在全职编写C时读到的一篇文章。我找不到文章,但我认为它的标题是“数组不是指针”或类似的东西。无论如何,我确实遇到了这个有趣的阅读C arrays and pointers FAQ。
答案 5 :(得分:0)
char [100]
是100个相邻char
的复杂类型,其sizeof
等于100。
被转换为指针((void*) a
),此变量产生第一个char
的地址。
引用此类型的变量(&a
)会产生整个变量的地址,而该变量也恰好是第一个char
的地址