繁体   English   中英

什么是数组到指针衰减?

[英]What is array to pointer decay?

什么是数组到指针衰减? 与数组指针有什么关系吗?

据说数组“衰减”成指针。 声明为int numbers [5]的 C++ 数组不能重新指向,即不能说numbers = 0x5a5aff23 更重要的是,衰减一词表示类型和维度的损失; numbers通过丢失维度信息(计数 5)衰减为int* ,并且类型不再是int [5] 在这里寻找没有发生衰变的情况

如果您按值传递数组,那么您真正要做的是复制指针 - 指向数组第一个元素的指针被复制到参数(其类型也应该是数组元素类型的指针)。 这是由于阵列的衰减性质而起作用的; 一旦衰减, 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 应该在编译时知道。

数组与 C/C++ 中的指针基本相同,但不完全一样。 转换数组后:

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

放入一个指针(无需强制转换即可工作,因此在某些情况下可能会意外发生):

const int* p = a;

您失去了sizeof运算符对数组中元素进行计数的能力:

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

这种失去的能力被称为“衰减”。

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

这是标准所说的(C99 6.3.2.1/3 - 其他操作数 - 左值、数组和函数指示符):

除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串文字,否则类型为 ''array of type'' 的表达式将转换为类型为 ''pointer to type'' 指向数组对象的初始元素并且不是左值。

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

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

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

“NT数组”或“T的未知边界数组”类型的左值或右值可以转换为“指向T的指针”类型的右值。

所以转换不必像在 C 中总是那样发生(这让函数重载或模板匹配数组类型)。

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

“衰减”是指表达式从数组类型到指针类型的隐式转换。 在大多数情况下,当编译器看到一个数组表达式时,它会将表达式的类型从“T 的 N 元素数组”转换为“指向 T 的指针”,并将表达式的值设置为数组第一个元素的地址. 此规则的例外情况是,当数组是sizeof&运算符的操作数,或者数组是在声明中用作初始值设定项的字符串文字时。

假设以下代码:

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

表达式a的类型是“80 元素的 char 数组”,而表达式“这是一个测试”的类型是“15 元素的 char 数组”(在 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 的简单指针。

在 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 运算符或 & 运算符(运算符的“地址”)的操作数,或作为用于初始化字符数组的字符串文字。

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

实际上,只是如果你想在某个地方传递一个数组,但传递的是指针(因为谁会为你传递整个数组),人们说糟糕的数组衰减为指针。

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

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 ),您可以使用类似的技巧可靠地获取数组中的元素数

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

因此:

  • 当您编写arr[idx]时,您实际上只是在说*(arr + idx)
  • 函数从不真正将数组作为参数,只有指针——直接地,当你指定一个数组参数时,或者间接地,如果你传递一个数组的引用。

此规则的一些例外情况:

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

数组在 C 中通过指针自动传递。其背后的基本原理只能推测

int a[5]int *aint (*a)[5]都是美化的地址,这意味着编译器根据类型对它们的算术和遵从运算符进行不同的处理,因此当它们引用相同的地址时,它们不是编译器同样处理。 int a[5]与其他 2 的不同之处在于地址是隐式的,并且不会作为数组本身的一部分显示在堆栈或可执行文件上,它仅由编译器用于解析某些算术运算,例如将其地址或指针算术。 因此int a[5]既是一个数组也是一个隐式地址,但是一旦你谈到地址本身并将其放入堆栈中,地址本身就不再是一个数组,而只能是一个指向数组或衰减数组,即指向数组第一个成员的指针。

例如,在int (*a)[5]上,第一次取消引用a将产生一个int * (所以相同的地址,只是不同的类型,注意不是int a[5] ),以及 a 上a指针运算,即a+1*(a+1)将根据 5 个 int 数组的大小(这是它指向的数据类型),第二次取消引用将产生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]来做到这一点,当然,函数必须指定数组的大小,因为无法传递数组的大小,因为该数组需要是编译时常量。

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

试试这个代码


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] ,因为它实际上从未以这种方式编译(但是,您显然不应该这样做,这会使读者感到困惑)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM