什么是数组到指针转换又名。衰变?

What is array-to-pointer conversion aka. decay?

提问人:Vamsi 提问时间:9/23/2009 最后编辑:Jan SchultkeVamsi 更新时间:9/26/2023 访问量:103358

问:

什么是数组到指针转换又名。衰变?与数组指针有什么关系吗?

C++ C 数组 指针

评论

85赞 Johannes Schaub - litb 9/23/2009
鲜为人知:一元加运算符可以用作“衰减运算符”:给定 ,则是一个 int 指针,并且是一个函数指针。如果要将其传递给接受引用的模板,则很有用。int a[10]; int b(void);+a+b
3赞 Michael Burr 9/23/2009
@litb - parens 也会做同样的事情(例如,(a) 应该是一个计算指针的表达式),对吧?
27赞 legends2k 3/25/2015
std::decay从 C++14 中可以减少数组在一元 + 上的衰减,这是一种不那么晦涩的方法。
30赞 M.M 5/31/2015
@JohannesSchaub litb 由于这个问题同时被标记为 C 和 C++,我想澄清一下,尽管在 C++ 中是合法的,但在 C 中是非法的(C11 6.5.3.3/1 “一元或运算符的操作数应具有算术类型”)+a+b+-
5赞 Johannes Schaub - litb 6/1/2015
@lege 对。但我想这并不像一元+的把戏那么鲜为人知。我之所以提到它,不仅仅是因为它会腐烂,还因为它是一些有趣的东西,可以玩;)

答:

10赞 Michael Krelin - hacker 9/23/2009 #1

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

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

评论

0赞 Unheilig 1/10/2014
说得好。什么是不会衰减为指针或防止衰减的好数组?你能举一个C的例子吗?谢谢。
0赞 Michael Krelin - hacker 1/10/2014
@Unheilig,当然,可以将数组真空打包到结构中并传递结构。
0赞 Michael Krelin - hacker 1/11/2014
我不确定你说的“工作”是什么意思。它不允许通过数组进行访问,但如果您期望实际发生的情况,它会按预期工作。这种行为(尽管官方再次未定义)被保留下来。
0赞 M.M 3/17/2015
衰减也发生在许多没有将数组传递到任何地方的情况下(如其他答案所述)。例如。a + 1
356赞 phoebus 9/23/2009 #2

据说数组会“衰减”为指针。声明为 的 C++ 数组不能重新指向,即你不能说 .更重要的是,术语衰变表示类型和维度的丧失; 通过丢失维度信息(计数 5)而衰减,并且类型不再存在。请在此处查找不会发生衰变的情况int numbers [5]numbers = 0x5a5aff23numbersint*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 在编译时应该是已知的。

评论

10赞 rlbond 9/23/2009
第一次传递价值如何?
12赞 John Bode 9/23/2009
by_value 将指针传递到数组的第一个元素;在函数参数的上下文中,等同于 。by_pointer传递相同的内容,只是指针值现在是限定的。如果要传递指向数组的指针(而不是指向数组的第一个元素指针),则语法为 。T a[]T *aconstT (*array)[U]
4赞 Pavel Minaev 9/23/2009
“使用指向该数组的显式指针” - 这是不正确的。如果 是 的数组,则 的类型为 ,并将衰减为 ;但属于类型,不会腐烂acharachar[N]char*&achar(*)[N]
6赞 Lightness Races in Orbit 6/5/2014
@FredOverflow:因此,如果进行了更改,您不必记住在两个地方进行更改,否则可能会出现无声的错误......自治!U
5赞 juanchopanza 9/20/2015
“如果你按值传递数组,你真正要做的就是复制一个指针”这没有意义,因为数组不能按值、句点传递。
130赞 system PAUSE 9/23/2009 #3

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

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

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

const int* p = a;

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

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

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

有关更多详细信息,请查看这篇关于阵列衰减的文章

评论

61赞 John Bode 9/23/2009
数组与指针基本不同;它们是完全不同的动物。在大多数情况下,数组可以被视为指针,指针也可以被视为数组,但这与它们之间的距离非常接近。
26赞 system PAUSE 9/23/2009
@John,请原谅我不准确的语言。我试图在不陷入冗长的背景故事的情况下找到答案,“基本上......但不完全是“是我在大学里得到的最好的解释。我相信任何感兴趣的人都可以从您的点赞评论中获得更准确的画面。
1赞 M.M 3/17/2015
在谈论类型转换时,“无需强制转换即可工作”与“隐式发生”的意思相同
2赞 Wu Xiliang 1/14/2022
数组变量几乎像指针一样工作,这一事实并不一定意味着它们是一回事。它们有不同的类型。这就是为什么运算符在数组上工作,而不是在指向数组的指针上工作,尽管它们都具有相同的地址。sizeof
5赞 Josh Kelley 9/23/2009 #4

数组衰减意味着,当数组作为参数传递给函数时,它的处理方式与指针相同(“衰减到”)。

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将其用于安全 CRT 函数(如 strcpy_s)的C++版本,您可以使用类似的技巧来可靠地获取数组中的元素数

评论

1赞 M.M 3/17/2015
衰减发生在许多其他情况下,而不仅仅是将数组传递给函数。
18赞 pmg 9/23/2009 #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; */ }

在上面的比较中,没有值,所以它变成了一个指针。它成为指向 int 的指针。该指针可以与变量 进行比较。arrip

在你习惯看到的数组索引语法中,arr 再次“衰减为指针”

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

数组唯一不会衰减为指针的情况是,当它是 sizeof 运算符的操作数,或 & 运算符('address of' 运算符),或用作用于初始化字符数组的字符串文字时。

评论

7赞 Pavel Minaev 9/23/2009
“数组没有价值”——这是什么意思?当然,数组是有价值的......它们是对象,你可以有指针,在 C++ 中,可以有对它们的引用,等等。
2赞 Johannes Schaub - litb 9/23/2009
我相信,严格来说,“值”在 C 中被定义为根据类型对对象位的解释。我很难用数组类型弄清楚它的有用含义。相反,你可以说你转换为指针,但这不是解释数组的内容,它只是获取它的位置。你得到的是一个指针的值(它是一个地址),而不是一个数组的值(这将是“包含项的值序列”,如“字符串”的定义中所用)。也就是说,我认为当一个人表示一个人得到的指针时,说“数组的值”是公平的。
0赞 Johannes Schaub - litb 9/23/2009
无论如何,我认为有一点歧义:对象的值和表达式的值(如“rvalue”)。如果用后一种方式解释,那么数组表达式肯定有一个值:它是将它衰减为右值而产生的值,并且是指针表达式。但是,如果以前一种方式解释,那么数组对象当然没有有用的意义。
1赞 legends2k 7/8/2014
+1 表示带有小修复的短语;对于数组,它甚至不是三元组,只是一副对联 [location, type]。对于阵列案例中的第三个位置,您还有其他想法吗?我想不出任何一个。
1赞 pmg 7/8/2014
@legends2k:我想我使用了数组中的第三个位置,以避免使它们成为只有对联的特例。也许 [location, type, void] 会更好。
60赞 Michael Burr 9/23/2009 #6

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

除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是 字符串文字,类型为“数组类型”的表达式为 转换为类型为“指针指向类型”的表达式,该表达式指向 数组对象,而不是左值。

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

请注意,函数名称的作用方式与此类似,但函数指针的使用要少得多,并且使用方式更加专业,因此它不会像将数组名称自动转换为指针那样造成混淆。

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

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

因此,转换不必像在 C 中那样发生(这允许函数重载或模板在数组类型上匹配)。

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

评论

2赞 Garrett 7/8/2014
什么是示例代码行,其中“具有'数组类型'的表达式”是“用于初始化数组的字符串文字”?
5赞 M.M 3/17/2015
@Garrett .6 个元素的数组不会衰减;而是获取 size,并且其元素是从 的元素初始化的。char x[] = "Hello";"Hello"x6"Hello"
45赞 John Bode 9/23/2009 #7

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

假设代码如下:

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

表达式的类型为“80 元素字符数组”,表达式“This is a test”的类型为“15 元素字符数组”(在 C 中;在 C++ 中,字符串文字是常量字符数组)。但是,在对 的调用中,这两个表达式都不是 或 的操作数,因此它们的类型被隐式转换为“指向 char 的指针”,并且它们的值被设置为每个元素中第一个元素的地址。接收的不是数组,而是指针,如其原型所示:astrcpy()sizeof&strcpy()

char *strcpy(char *dest, const char *src);

这与数组指针不是一回事。例如:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

两者具有相同的;A 的基址。但是,它们是不同的类型,处理方式也不同,如下所示:ptr_to_first_elementptr_to_array

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 的简单指针。

评论

2赞 chux - Reinstate Monica 12/8/2013
不是 ?(长度 14 + 1 表示 \0)"This is a test" is of type "16-element array of char""15-element array of char"
0赞 Chris Dodd 9/28/2021
请注意,计算结果为数组值,然后该值立即衰减为指向数组第一个元素的指针,除非它是 的操作数或一元数*ptr_to_arraysizeof&
2赞 einpoklum 3/13/2018 #8

tl;dr:当你使用你定义的数组时,你实际上会使用指向它的第一个元素的指针。

因此:

  • 当你写作时,你真的只是在说.arr[idx]*(arr + idx)
  • 函数从不真正将数组作为参数,而只将指针作为参数 - 在指定数组参数时直接接受,或者在传递对数组的引用时间接执行指针。

此规则的例外情况:

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

评论

0赞 463035818_is_not_an_ai 3/25/2021
数组可以通过引用函数来传递。而且我不明白给出数组的大小而不是指针是函数不将数组作为参数的例外。常见的问题是,当在源自将数组传递给函数的指针上使用时,确实会返回指针的大小sizeofsizeof
1赞 einpoklum 3/25/2021
@largest_prime_is_463035818 : 我的TL;DR 谈到了一般使用数组,而不仅仅是将其传递给函数。此外,经过编辑以阐明您可以通过引用传递数组。
0赞 463035818_is_not_an_ai 3/25/2021
谢谢,明白了。“Sort-of exception”指的是第一行,而不是我第一次误读的“因此”
1赞 Chef Gladiator 11/9/2019 #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++ 原生数组处理是关键任务语言功能。

1赞 Lewis Kelsey 4/13/2021 #10

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

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

例如,on ,第一个取消引用 on 将产生一个(所以相同的地址,只是不同的类型,请注意不是 ),以及指针算术 on 即 或者是 5 个整数数组的大小(这是它指向的数据类型),第二次取消引用将产生 .但是,第一次取消引用将产生 和 指针算术将根据 .int (*a)[5]aint *int a[5]aa+1*(a+1)intint a[5]intint

对于函数,您只能传递 和 ,并且该函数将其强制转换为任何参数类型,因此在函数中,您可以选择是将传递的地址视为衰减数组还是指向数组的指针(其中函数必须指定要传递的数组的大小)。如果传递给函数并被定义,则解析为地址,则传递的是地址,而地址只能是指针类型。在函数中,它访问的参数是堆栈或寄存器中的地址,它只能是指针类型,而不能是数组类型——这是因为它是堆栈上的实际地址,因此显然不是数组本身。int *int (*)[5]aaint a[5]a

您丢失了数组的大小,因为参数的类型(地址)是指针而不是数组,数组没有数组大小,这在使用 时可以看出,它适用于传递给它的值的类型。参数类型 instead of 是允许的,但被视为而不是完全禁止它,尽管它应该被禁止,因为它具有误导性,因为它让你认为可以使用大小信息,但你只能通过将其转换为 来做到这一点,当然,函数必须指定数组的大小,因为没有办法传递数组的大小,因为大小的数组必须是编译时常量。sizeofint a[5]int *aint *int (*a)[5]

5赞 blue_note 12/18/2021 #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 中数组的大小,但它等于指针的大小。

您可能听说过“数组是指针”,但是,这并不完全正确(内部打印正确的大小)。但是,当传递时,数组会衰减为指针。也就是说,无论语法显示什么,您实际上传递了一个指针,而函数实际上接收了一个指针。sizeofmain

在这种情况下,编译器将定义隐式转换为 。您可以等效地直接将函数参数声明为 。你甚至可以写 or 代替 ,因为它实际上从来没有以这种方式编译(但是,你不应该明显地这样做,它会让读者感到困惑)。void f(double a[10]void f(double *a)*aa[100]a[1]a[10]