什么是数组到指针衰减?
Posted
技术标签:
【中文标题】什么是数组到指针衰减?【英文标题】:What is array to pointer decay? 【发布时间】:2009-09-22 17:24:43 【问题描述】:和数组指针有关系吗?
【问题讨论】:
鲜为人知:一元加运算符可以用作“衰减运算符”:给定int a[10]; int b(void);
,那么+a
是一个int 指针,+b
是一个函数指针。如果您想将其传递给接受引用的模板,这很有用。
@litb - parens 也会做同样的事情(例如,(a)应该是一个计算为指针的表达式),对吧?。
std::decay
来自 C++14 将是一种通过一元 + 衰减数组的不那么晦涩的方法。
@JohannesSchaub-litb 因为这个问题同时被标记为 C 和 C++,所以我想澄清一下,虽然 +a
和 +b
在 C++ 中是合法的,但在 C 中是非法的(C11 6.5 .3.3/1 "一元+
或-
运算符的操作数应具有算术类型")
@lege 对。但我想这并不像一元 + 的技巧那样鲜为人知。我提到它的原因不仅仅是因为它会腐烂,而是因为它是一些有趣的东西;)
【参考方案1】:
据说数组“衰减”成指针。声明为 int numbers [5]
的 C++ 数组不能重新指向,即不能说 numbers = 0x5a5aff23
。更重要的是,衰减一词表示类型和维度的损失。 numbers
通过丢失维度信息(计数 5)衰减为 int*
,并且类型不再是 int [5]
。在此处查找cases where the decay doesn't happen。
如果你通过值传递一个数组,你真正要做的是复制一个指针——指向数组第一个元素的指针被复制到参数(其类型也应该是数组元素类型的指针)。这是由于阵列的衰减性质而起作用的;一旦衰减,sizeof
不再给出完整数组的大小,因为它本质上变成了一个指针。这就是为什么首选(以及其他原因)通过引用或指针传递的原因。
传入数组的三种方式1:
void by_value(const T* array) // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])
最后两个将提供正确的sizeof
信息,而第一个不会,因为数组参数已衰减以分配给参数。
1 常量 U 应该在编译时知道。
【讨论】:
第一次传值如何? by_value 将指针传递给数组的第一个元素;在函数参数的上下文中,T a[]
与 T *a
相同。 by_pointer 传递同样的东西,除了指针值现在限定为const
。如果你想传递一个指针到数组(而不是一个指向数组第一个元素的指针),语法是T (*array)[U]
.
“带有指向该数组的显式指针” - 这是不正确的。如果a
是char
的数组,则a
的类型为char[N]
,并将衰减为char*
;但&a
的类型为char(*)[N]
,并且不会衰减。
@FredOverflow:因此,如果U
发生更改,您不必记住在两个地方进行更改,否则会冒无声错误的风险……自治!
“如果你通过值传递一个数组,你真正在做的是复制一个指针”这没有意义,因为数组不能通过值传递,句号。跨度>
【参考方案2】:
数组与 C/C++ 中的指针基本相同,但不完全一样。转换数组后:
const int a[] = 2, 3, 5, 7, 11 ;
到指针中(无需强制转换即可工作,因此在某些情况下可能会意外发生):
const int* p = a;
您将失去 sizeof
运算符对数组中元素进行计数的能力:
assert( sizeof(p) != sizeof(a) ); // sizes are not equal
这种失去的能力被称为“衰减”。
更多详情,请查看article about array decay。
【讨论】:
数组不与指针基本相同;它们是完全不同的动物。在大多数情况下,可以将数组视为它是一个指针,并且可以将指针视为它是一个数组,但这是最接近的. @John,请原谅我措辞不准确。我试图在不陷入冗长的背景故事的情况下找到答案,而“基本上……但不完全”是我在大学时得到的最好的解释。我相信任何感兴趣的人都可以从您的点赞评论中获得更准确的图片。 "works without cast" 与类型转换的"隐式发生"的含义相同 数组变量几乎像指针一样工作这一事实并不一定意味着它们是同一个东西。他们有不同的类型。这就是为什么操作符sizeof
作用于数组而不是指向数组的指针的原因,尽管它们都具有相同的地址。【参考方案3】:
这是标准所说的(C99 6.3.2.1/3 - 其他操作数 - 左值、数组和函数指示符):
除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是 用于初始化数组的字符串字面量,具有“类型数组”类型的表达式是 转换为类型为“pointer to type”的表达式,该表达式指向 数组对象,不是左值。
这意味着几乎任何时候在表达式中使用数组名称时,它都会自动转换为指向数组中第一项的指针。
请注意,函数名称的作用方式类似,但函数指针的使用要少得多,而且使用的方式更加专业,不会像将数组名称自动转换为指针那样引起混乱。
C++ 标准(4.2 数组到指针的转换)将转换要求放宽到(强调我的):
“N T 数组”或“T 未知边界数组”类型的左值或右值可以转换为右值 类型为“指向 T 的指针”。
所以转换不会像在 C 中几乎总是发生的那样发生(这让函数重载或模板匹配数组类型)。
这也是为什么在 C 语言中你应该避免在函数原型/定义中使用数组参数(在我看来——我不确定是否有任何普遍的协议)。它们会引起混淆并且无论如何都是虚构的 - 使用指针参数并且混淆可能不会完全消失,但至少参数声明没有说谎。
【讨论】:
什么是示例代码行,其中“类型为'array of type'的表达式”是“用于初始化数组的字符串文字”? @Garrettchar x[] = "Hello";
。 6 个元素的数组"Hello"
不衰减;取而代之的是 x
获取大小 6
并且它的元素是从 "Hello"
的元素初始化的。【参考方案4】:
“衰减”是指表达式从数组类型到指针类型的隐式转换。在大多数情况下,当编译器看到一个数组表达式时,它会将表达式的类型从“T 的 N 元素数组”转换为“指向 T 的指针”,并将表达式的值设置为数组第一个元素的地址.此规则的例外情况是数组是 sizeof
或 &
运算符的操作数,或者数组是在声明中用作初始值设定项的字符串文字。
假设以下代码:
char a[80];
strcpy(a, "This is a test");
表达式a
的类型是“80 个字符的数组”,而表达式“这是一个测试”的类型是“15 个字符的数组”(在 C 中;在 C++ 中,字符串字面量是常量字符)。但是,在对strcpy()
的调用中,两个表达式都不是sizeof
或&
的操作数,因此它们的类型被隐式转换为“指向char 的指针”,并且它们的值被设置为第一个元素的地址每个。 strcpy()
接收的不是数组,而是指针,如其原型所示:
char *strcpy(char *dest, const char *src);
这与数组指针不同。例如:
char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;
ptr_to_first_element
和ptr_to_array
具有相同的值; a的基地址。但是它们是不同的类型,区别对待,如下图:
a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]
请记住,表达式a[i]
被解释为*(a+i)
(仅当数组类型转换为指针类型时才有效),因此a[i]
和ptr_to_first_element[i]
的工作方式相同。表达式(*ptr_to_array)[i]
被解释为*(*a+i)
。表达式*ptr_to_array[i]
和ptr_to_array[i]
可能会根据上下文导致编译器警告或错误;如果您期望他们评估为a[i]
,他们肯定会做错事。
sizeof a == sizeof *ptr_to_array == 80
同样,当数组是sizeof
的操作数时,它不会转换为指针类型。
sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
is on your platform
ptr_to_first_element
是一个指向 char 的简单指针。
【讨论】:
"This is a test" is of type "16-element array of char"
不是 "15-element array of char"
吗? (长度 14 + 1 为 \0)
请注意,*ptr_to_array
评估为数组值,然后立即衰减为指向数组第一个元素的指针,除非它是 sizeof
的操作数或一元 &
【参考方案5】:
C 中的数组没有价值。
如果需要对象的值但对象是数组,则使用其第一个元素的地址,类型为pointer to (type of array elements)
。
在函数中,所有参数都是按值传递的(数组也不例外)。当您在函数中传递数组时,它“衰减为指针”(原文如此);当您将数组与其他东西进行比较时,它再次“衰减为指针”(原文如此); ...
void foo(int arr[]);
函数 foo 需要一个数组的值。但是,在 C 语言中,数组没有价值!所以foo
得到的是数组第一个元素的地址。
int arr[5];
int *ip = &(arr[1]);
if (arr == ip) /* something; */
在上面的比较中,arr
没有值,所以它变成了一个指针。它变成了一个指向 int 的指针。该指针可以与变量ip
进行比较。
在您习惯看到的数组索引语法中,arr 再次“衰减为指针”
arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */
数组不衰减为指针的唯一情况是它是 sizeof 运算符或 & 运算符(运算符的“地址”)的操作数,或作为用于初始化字符数组的字符串文字.
【讨论】:
“数组没有价值”——这是什么意思?当然,数组是有价值的……它们是对象,你可以有指针,在 C++ 中,还有对它们的引用等等。 我相信,严格来说,“值”在 C 中定义为根据类型对对象位的解释。我很难用数组类型弄清楚它的有用含义。相反,您可以说您转换为指针,但这不是解释数组的内容,它只是获取其位置。你得到的是一个指针的值(它是一个地址),而不是一个数组的值(这将是“包含的项目的值的序列”,如“字符串”的定义中所使用的)。也就是说,我认为当一个表示指针得到时说“数组的值”是公平的。 无论如何,我认为有一点歧义:对象的值和表达式的值(如“rvalue”)。如果以后一种方式解释,那么数组表达式肯定有一个值:它是将其衰减为右值的结果,并且是指针表达式。但是如果按前一种方式解释,那么数组对象当然没有任何意义。 +1 用于小修复的短语;对于数组,它甚至不是一个三元组,只是一个对联[位置,类型]。对于阵列案例中的第三个位置,您是否还有其他想法?我想不出来。 @legends2k:我想我在数组中使用了第三个位置以避免使它们成为只有对联的特殊情况。也许 [location, type, void] 会更好。【参考方案6】:当数组腐烂并被指向时;-)
其实,只是如果你想在某个地方传递一个数组,但是传递的是指针(因为谁他妈会为你传递整个数组),人们说糟糕的数组衰减为指针。
【讨论】:
说得好。什么是一个不会衰减到指针或防止衰减的好数组?你能举一个C语言的例子吗?谢谢。 @Unheilig,当然,可以将数组真空包装到结构中并传递结构。 我不确定您所说的“工作”是什么意思。不允许通过数组访问,但如果您期望真正发生的事情,它会按预期工作。这种行为(虽然,同样,官方未定义)被保留。 衰减也发生在许多没有在任何地方传递数组的情况下(如其他答案所述)。例如,a + 1
.【参考方案7】:
数组衰减意味着,当数组作为参数传递给函数时,它的处理方式与(“衰减到”)指针相同。
void do_something(int *array)
// We don't know how big array is here, because it's decayed to a pointer.
printf("%i\n", sizeof(array)); // always prints 4 on a 32-bit machine
int main (int argc, char **argv)
int a[10];
int b[20];
int *c;
printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
do_something(a);
do_something(b);
do_something(c);
上述情况有两种复杂情况或例外情况。
首先,在 C 和 C++ 中处理多维数组时,只会丢失第一个维度。这是因为数组在内存中是连续布局的,因此编译器必须知道除了第一个维度之外的所有维度,才能计算到该内存块的偏移量。
void do_something(int array[][10])
// We don't know how big the first dimension is.
int main(int argc, char *argv[])
int a[5][10];
int b[20][10];
do_something(a);
do_something(b);
return 0;
其次,在 C++ 中,您可以使用模板来推断数组的大小。 Microsoft 将此用于 C++ 版本的 Secure CRT 函数,例如 strcpy_s,您可以使用类似的技巧来可靠地使用 get the number of elements in an array。
【讨论】:
衰减发生在许多其他情况下,而不仅仅是将数组传递给函数。【参考方案8】:tl;dr:当您使用已定义的数组时,您实际上将使用指向其第一个元素的指针。
因此:
当您写arr[idx]
时,您实际上只是在说 *(arr + idx)
。
函数从不真正将数组作为参数,只有指针 - 直接,当您指定数组参数时,或间接,如果您传递对数组的引用。
此规则的一些例外情况:
您可以将固定长度的数组传递给struct
中的函数。
sizeof()
给出了数组占用的大小,而不是指针的大小。
【讨论】:
可以通过引用函数来传递数组。而且我不明白sizeof
给出数组的大小而不是指针是不将数组作为参数的函数的一个例外。常见的问题是 sizeof
在用于源自将数组传递给函数的指针时确实返回指针的大小
@largest_prime_is_463035818 :我的 TL;DR 谈到了一般使用数组,而不仅仅是将其传递给函数。此外,经过编辑以阐明您可以通过引用传递数组。
谢谢,知道了。 “排序异常”指的是第一行而不是我第一次误读的“因此”【参考方案9】:
我可能会大胆地认为有四 (4) 种方法可以将数组作为函数参数传递。这里还有简短但有效的代码供您阅读。
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
using namespace std;
// test data
// notice native array init with no copy aka "="
// not possible in C
const char* specimen[] __TIME__, __DATE__, __TIMESTAMP__ ;
// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array)
// a pointer
assert(array != nullptr);
;
// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF])
// decayed to a pointer
assert( array != nullptr );
// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
// dealing with native pointer
assert( array != nullptr );
// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
// array is not a pointer here
// it is (almost) a container
// most of the std:: lib algorithms
// do work on array reference, for example
// range for requires std::begin() and std::end()
// on the type passed as range to iterate over
for (auto && elem : array )
cout << endl << elem ;
int main()
// ONE
as_pointer(specimen);
// TWO
by_value_no_size(specimen);
// THREE
pointer_to_array(&specimen);
// FOUR
reference_to_array( specimen ) ;
我也可能认为这显示了 C++ 与 C 的优势。至少在通过引用传递数组的引用(双关语)方面。
当然也有非常严格的项目,没有堆分配,没有异常,也没有 std::lib。有人可能会说,C++ 原生数组处理是任务关键型语言功能。
【讨论】:
【参考方案10】:数组在 C 中通过指针自动传递。The rationale behind it can only be speculated。
int a[5]
、int *a
和 int (*a)[5]
都是美化的地址,这意味着编译器根据类型对它们的算术和推导运算符进行不同的处理,因此当它们引用相同的地址时,它们不会被相同的处理编译器。 int a[5]
与其他 2 个不同之处在于地址是隐式的,并且不会作为数组本身的一部分显示在堆栈或可执行文件上,它仅由编译器用于解析某些算术运算,例如获取其地址或指针算术。 int a[5]
因此是一个数组也是一个隐式地址,但是一旦你谈到地址本身并把它放在堆栈上,地址本身就不再是一个数组,而只能是一个指向数组的指针或衰减数组,即指向数组第一个成员的指针。
例如,在int (*a)[5]
上,a
上的第一次解引用将产生一个int *
(所以地址相同,只是类型不同,注意不是int a[5]
),以及a
上的指针运算即a+1
或*(a+1)
将根据5 个整数数组的大小(这是它指向的数据类型),第二个取消引用将产生int
。然而,在int a[5]
上,第一次取消引用将产生int
,并且指针算法将根据int
的大小来计算。
对于函数,您只能传递int *
和int (*)[5]
,并且该函数将其转换为任何参数类型,因此在函数中您可以选择是否将传递的地址视为衰减数组或指向数组的指针(函数必须指定要传递的数组的大小)。如果您将a
传递给函数并且a
定义为int a[5]
,那么当a
解析为地址时,您传递的是地址,并且地址只能是指针类型。在函数中,它访问的参数是堆栈或寄存器中的地址,它只能是指针类型而不是数组类型 - 这是因为它是堆栈上的实际地址,因此显然不是数组本身。
您会丢失数组的大小,因为参数的类型是地址,是指针而不是数组,它没有数组大小,正如使用 sizeof
时可以看到的那样,它适用于传递给它的值的类型。参数类型int a[5]
而不是int *a
是允许的,但被视为int *
而不是彻底禁止它,尽管它应该被禁止,因为它具有误导性,因为它使您认为可以使用大小信息,但是你只能通过将它转换为int (*a)[5]
来做到这一点,当然,函数必须指定数组的大小,因为没有办法传递数组的大小,因为数组的大小需要是编译时常量。
【讨论】:
【参考方案11】:试试这个代码
void f(double a[10])
printf("in function: %d", sizeof(a));
printf("pointer size: %d\n", sizeof(double *));
int main()
double a[10];
printf("in main: %d", sizeof(a));
f(a);
你会看到函数内部数组的大小不等于main中数组的大小,而是等于指针的大小。
您可能听说过“数组是指针”,但这并不完全正确(main
中的sizeof
打印正确的大小)。然而,当通过时,数组衰减为指针。也就是说,不管语法显示什么,你实际上传递了一个指针,而函数实际上接收了一个指针。
在这种情况下,定义void f(double a[10]
被编译器隐式转换为void f(double *a)
。您可以等效地将函数参数直接声明为*a
。你甚至可以写a[100]
或a[1]
,而不是a[10]
,因为它实际上从来没有这样编译(但是,你不应该这样做,这会让读者感到困惑)。
【讨论】:
以上是关于什么是数组到指针衰减?的主要内容,如果未能解决你的问题,请参考以下文章