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

22

我知道以下内容是不正确的:

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被分配的类型是什么,以及发生了什么吗?

4个回答

27

嗯,当几乎在任何地方使用时,数组都会衰变为指针。因此,在您的代码片段中也发生了衰变。

但是,只有“最外层”的数组维度会衰变为指针。由于数组是行主序,您最终得到的是int (*)[3]作为指针类型,它是指向一维数组而不是二维数组的指针。它指向第一个“行”。

如果您希望ptr的推断成为指向数组而不是指针,则使用取地址运算符:

auto ptr = &arr;

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


抱歉如果这是一个愚蠢的问题,但如果我不使用关键字auto,我应该如何在开头声明ptr的类型?谢谢。 - R.Xin
1
@R.Xin - 对于稍微复杂的语法 int (* ptr )[3] = arr;,如果需要经常重复这样的声明,大多数人都会使用typedef来简化。 - StoryTeller - Unslander Monica
1
@R.Xin,如果你想像StoryTeller建议的那样使用typedef,我建议你使用typedef int MyArr[3]; MyArr* ptr; - eerorika
1
@user2079303 - 关于隐藏指针语义的恶劣影响,你说得很好! - StoryTeller - Unslander Monica
@StoryTeller 我修复了注释,谢谢。数组大小与类型分离的事实对我来说很反直觉。我认为 using MyArr = int[3] 会更好。 - eerorika

12

In

auto ptr = arr;

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

auto ptr = &arr[0];

由于arr[0]是一个包含三个int的数组,这使得ptr成为了int (*)[3]——指向int[3]的指针。

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

ptr = another_arr;

被赋值的两边都是类型为int (*)[3]的,对于任何类型T,你可以将T*分配给T*

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

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

auto ptr = &arr; 

7

首先,让我们看看为什么不能将 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]解释为“获取数组中位置为(1 * 2 + 0)的值”。但是对于指向指针(int **),预期指向指针的指针包含单个内存地址或内存地址数组,该/这些地址指向另一个单个int值或int数组。假设我们将数组arr复制到int **ptrptr中,那么在内存中它看起来像这样:
ptrptr:     [0x203F0B20, 0x203F17D4]
0x203F0B20: [ 1, 2, 3 ]
0x203F17D4: [ 4, 5, 6 ]

所以除了实际的int数据外,每行数组还必须存储一个额外的指针。不是将两个索引转换为单个数组查找,而是通过进行第一次数组查找(“取ptrptr中的第二个值以获取int *”),然后不再进行另一个数组查找(“取先前获得的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; 最终得到的结果。

啊,现在我才看到你已经展示了arr内存的初始化,很高兴看到这个深入的解释 :-) - Wolf

3

什么是[...]类型?

你是否已经尝试向编译器询问表达式的类型?

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,可以阅读第29章. 模块化来了解。

编辑:

尝试使用像这样的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]


1
是的,我尝试过这个,但我无法理解运行时特定的代码。 - R.Xin
@R.Xin 事实上,正如StorryTeller指出的auto ptr相对于原始声明的间接级别似乎很容易看到。 - Wolf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接