什么是数组指针衰减?

什么是数组指针衰减?它和数组指针有关系吗?

90814 次浏览

当数组腐烂并被指向时;-)

实际上,如果你想传递一个数组到某个地方,但却传递了指针(因为谁会他妈的为你传递整个数组),人们会说这个可怜的数组衰减为指针。

据说数组会“衰减”成指针。声明为int numbers [5]的c++数组不能被重指向,即不能说numbers = 0x5a5aff23。更重要的是,衰减一词意味着类型和维度的丧失;numbers通过丢失维数信息(计数5)而衰减为int*,并且类型不再是int [5]。在这里查找不发生衰变的情况

如果你是按值传递一个数组,你实际上是在复制一个指针——指向数组第一个元素的指针被复制到形参(形参的类型也应该是数组元素的类型的指针)。这是由于数组的衰减性质;一旦衰变,sizeof就不再给出整个数组的大小,因为它本质上变成了一个指针。这就是为什么首选通过引用或指针传递的原因(以及其他原因)。

传递array1的三种方法:

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信息,而第一个将不会,因为数组参数已经衰减为赋值给形参。

常量U应该在编译时已知。

数组与C/ c++中的指针基本相同,但又不完全相同。一旦你转换一个数组:

const int a[] = { 2, 3, 5, 7, 11 };

转换为指针(不需要强制转换,因此在某些情况下可能会意外发生):

const int* p = a;

你将失去sizeof操作符计算数组中元素的能力:

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

这种丧失的能力被称为“衰退”。

有关更多详细信息,请查看这个关于数组衰减的文章

数组衰减意味着,当数组作为参数传递给函数时,它被视为(“衰减为”)指针。

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++中,您可以使用模板来推断数组的大小。微软将此用于c++版本的Secure CRT函数,如strcpy_s,并且您可以使用类似的技巧来可靠地获取数组中元素的个数

在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操作符或&操作符('address of'操作符),或作为用于初始化字符数组的字符串字面值。

以下是该标准的内容(C99 6.3.3.1 /3 -其他操作数-左值、数组和函数指示符):

除非它是sizeof操作符或一元&运算符,或者是a 字符串字面值用于初始化数组,具有类型“数组类型”的表达式为 转换为类型为“指针指向类型”的表达式,该表达式指向的初始元素 数组对象和不是左值

这意味着无论何时在表达式中使用数组名称,它都会自动转换为指向数组中第一项的指针。

请注意,函数名的作用与此类似,但函数指针的使用要少得多,而且使用的方式要专门得多,因此不会像将数组名自动转换为指针那样引起混乱。

c++标准(4.2数组到指针转换)将转换要求放宽为(强调我的):

类型为“N T数组”或“T未知界数组”可以的左值或右值被转换为右值

因此转换不会发生,就像它在C中经常发生的那样(这让函数重载或模板匹配数组类型)。

这也是为什么在C语言中你应该避免在函数原型/定义中使用数组形参(在我看来-我不确定是否有普遍的共识)。它们会引起混乱,而且无论如何都是虚构的——使用指针形参,混乱可能不会完全消失,但至少形参声明没有说谎。

“Decay"引用表达式从数组类型到指针类型的隐式转换。在大多数情况下,当编译器看到一个数组表达式时,它会将表达式的类型从&;n元素数组的t &;指向T"并将表达式的值设置为数组第一个元素的地址。此规则的例外情况是当数组是sizeof&操作符的操作数,或者数组是在声明中用作初始化式的字符串字面值时。

假设有以下代码:

char a[80];
strcpy(a, "This is a test");

表达式a的类型为“80-element array of char”。而表达“这是一个测试”;是char的“15元素数组”类型;(在C;在c++中,字符串字面值是const char数组)。然而,在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_elementptr_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指针。

tl;dr:当您使用已定义的数组时,实际上是在使用指向其第一个元素的指针。

因此:

  • 当你写arr[idx]时,你实际上只是在说*(arr + idx)
  • 函数从来不会真正将数组作为参数,只接受指针——当指定数组参数时可以直接接受,如果将引用传递给数组则可以间接接受。

这条规则的例外情况:

  • 可以将固定长度的数组传递给struct中的函数。
  • sizeof()给出了数组所占用的大小,而不是指针的大小。

我可能会大胆地认为有四(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++原生数组处理是关键任务语言特性。

数组由C. 其背后的原理只能推测中的指针自动传递。

int a[5]int *aint (*a)[5]都是美化的地址,这意味着编译器会根据不同的类型对其上的算术运算符和差分运算符进行不同的处理,因此当它们引用相同的地址时,编译器不会对它们进行相同的处理。int a[5]与其他两个不同之处在于,它的地址是隐式的,不作为数组本身的一部分显示在堆栈或可执行文件中,它仅被编译器用于解析某些算术操作,如获取其地址或指针算术。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],当然,该函数必须指定数组的大小,因为没有办法通过数组的大小,因为数组的大小必须是一个编译时常量。

试试这段代码


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],因为它实际上从来没有这样编译过(然而,你显然不应该这样做,这会让读者感到困惑)。