char a [的语义

use*_*568 14 c c++ pointers semantics

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

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

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

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

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

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

printf("%p %p", a, &a);
Run Code Online (Sandbox Code Playgroud)

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

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

jal*_*alf 18

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

所以scanf("%s", a)需要一个指针,而不是一个数组,所以数组退化成一个指向数组第一个元素的指针.

当然scanf("%s", &a)也可以,因为那显然是数组的地址.

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

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


Joh*_*itb 11

好吧,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函数,重点由me(7.15.1.1/2)添加:

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

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

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

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


Wil*_*Cau 6

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

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

 ---+-----+---
 ...|0..99|...
 ---+-----+---
    ^
    a == &a
Run Code Online (Sandbox Code Playgroud)

为了比较,如果数组是malloc'd,那么指针有一个单独的变量,和a != &a.

char *a;
a = malloc(100);
Run Code Online (Sandbox Code Playgroud)

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

 ---+---+---+-----+---
 ...| a |...|0..99|...
 ---+---+---+-----+---
    ^       ^
    &a  !=  a
Run Code Online (Sandbox Code Playgroud)

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

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


Chr*_*oph 5

AC阵列可以隐式转换为指针到它的第一元件(C99:TC3 6.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); - 应该可以