指向二维数组的指针的类型是什么?

What is the type of a pointer to a 2D array?

我知道以下是不正确的:

int arr[2][3] = {}; //some array initialization here
int** ptr;
ptr = arr;

但令我惊讶的是以下几行实际上有效

int arr[2][3] = {}; //some array initialization here
auto ptr = arr;
int another_arr[2][3] = {}; //some array initialization here
ptr = another_arr;

谁能解释一下在第二个代码块中分配给 ptr 的类型是什么,下面发生了什么?

好吧,当几乎无处不在时,数组会退化为指针。因此,您的代码片段中自然也会发生衰退。

但只有 "outer-most" 数组维度衰减为指针。由于数组是行优先的,因此您最终得到 int (*)[3] 作为指针类型,它是指向一维数组的指针,而不是二维数组。它指向第一个 "row".

如果您希望 ptr 的推导成为指向数组 的指针 ,则使用寻址运算符:

auto ptr = &arr;

现在 ptrint(*)[2][3]

auto ptr = arr;

arr 以正常方式衰减为指向其第一个元素的指针;相当于

auto ptr = &arr[0];

因为 arr[0] 是一个包含三个 int 的数组,所以 ptr 是一个 int (*)[3] - 指向 int[3].[=28= 的指针]

another_arr 以完全相同的方式衰减,所以在

ptr = another_arr;

赋值的两边都是int (*)[3]类型,对于任何类型T.

,您可以将T*赋值给T*

指向 arr 的指针本身具有类型 int(*)[2][3]

如果你想要一个指向数组的指针而不是指向数组第一个元素的指针,你需要使用&:

auto ptr = &arr; 

What is the type of [...]

您是否已经尝试让编译器告诉您表达式的类型?

int main()
{
    int arr[2][3] = {{0,1,2}, {3,4,5}};  // <-- direct complete initialized here

    auto ptr = arr;                     // <-- address assignment only

    cout << "arr: " << typeid(arr).name() << endl;
    cout << "ptr: " << typeid(ptr).name() << endl;
    return 0;
}

我不得不承认输出

arr: A2_A3_i
ptr: PA3_i

乍一看似乎可读性不是很好(与其他一些语言相比),但如果有疑问,它可能会有所帮助。它非常紧凑,但可能很快就会习惯。编码是依赖于编译器的,如果你使用的是 gcc,你可以阅读 Chapter 29. Demangling 来了解如何。

编辑:

对某些 simple_cpp_name 功能进行一些实验,例如这种基本的 hack

#include <typeinfo>
#include <cxxabi.h>
#include <stdlib.h>
#include <string>

std::string simple_cpp_name(const std::type_info& ti)
{
    /// simplified code extracted from "Chapter 29. Demangling"
    /// https://gcc.gnu.org/onlinedocs/libstdc++/manual/ext_demangling.html
    char* realname = abi::__cxa_demangle(ti.name(), 0, 0, 0);
    std::string name = realname;
    free(realname);
    return name;
}

将向您展示 auto &rfa = arr; 使 rfa 具有与 arr 相同的类型,即 int [2][3].

首先,让我们看看为什么不能将int arr[2][3]分配给int **。为了更容易可视化,我们将用一个序列初始化你的数组,并考虑它在内存中的样子:

int arr[2][3] = {{1,2,3},{4,5,6}};

在内存中,数组数据存储为单个块,就像常规的一维数组一样:

arr: [ 1, 2, 3, 4, 5, 6 ]

变量arr包含这个块的开始地址,并且从它的类型(int[2][3])编译器知道将像arr[1][0]这样的索引解释为"take the value that is at position (1*2 + 0) in the array".

但是对于指向指针的指针(int**),预期指向指针的指针包含单个内存地址或内存地址数组,并且this/these地址指向(一个)其他单个整数值或整数数组。假设我们将数组 arr 复制到 int **ptrptr。在记忆中,它看起来像这样:

ptrptr:     [0x203F0B20, 0x203F17D4]
0x203F0B20: [ 1, 2, 3 ]
0x203F17D4: [ 4, 5, 6 ]

因此除了实际的 int 数据外,还必须为数组的每一行存储一个额外的指针。不是将两个索引转换为单个数组查找,而是必须通过进行第一个数组查找 ("take the second value in ptrptr to get an int*"),然后是另一个数组查找 ("take the first value in the array at the address held by the previously obtained int*").

来执行访问。

这是一个说明这一点的程序:

#include <iostream>

int main()
{
    int arr[2][3] = {{1,2,3},{4,5,6}};

    std::cout << "Memory addresses for int arr[2][3]:" << std::endl;
    for (int i=0; i<2; i++)
    {
        for (int j=0; j<3; j++)
        {
            std::cout << reinterpret_cast<void*>(&arr[i][j]) << ": " << arr[i][j] << std::endl;
        }
    }

    std::cout << std::endl << "Memory addresses for int **ptrptr:" << std::endl;
    int **ptrptr = new int*[2];
    for (int i=0; i<2; i++)
    {
        ptrptr[i] = new int[3];
        for (int j=0; j<3; j++)
        {
            ptrptr[i][j] = arr[i][j];
            std::cout << reinterpret_cast<void*>(&ptrptr[i][j]) << ": " << ptrptr[i][j] << std::endl;
        }
    }

    // Cleanup
    for (int i=0; i<2; i++)
    {
        delete[] ptrptr[i];
        ptrptr[i] = nullptr;
    }
    delete[] ptrptr;
    ptrptr = nullptr;

    return 0;
}

输出:

Memory addresses for int arr[2][3]:
0x7ecd3ccc0260: 1
0x7ecd3ccc0264: 2
0x7ecd3ccc0268: 3
0x7ecd3ccc026c: 4
0x7ecd3ccc0270: 5
0x7ecd3ccc0274: 6

Memory addresses for int **ptrptr:
0x38a1a70: 1
0x38a1a74: 2
0x38a1a78: 3
0x38a1a90: 4
0x38a1a94: 5
0x38a1a98: 6

注意 arr 的内存地址总是增加 4 个字节,但是 ptrptr 在值 3 和 4 之间有 24 个字节的跳跃。

一个简单的赋值不能创建类型 int ** 所需的指针到指针结构,这就是为什么在上面的程序中需要循环的原因。它能做的最好的事情就是将 int[2][3] 类型衰减为指向该数组一行的指针,即 int (*)[3]。这就是您的 auto ptr = arr; 最终的结果。