数组声明如何在C++中工作

How does array declaration work in C++?

本文关键字:C++ 工作 声明 数组      更新时间:2023-10-16

我试图理解在C++中声明数组(一维或二维(的不同方法以及它们究竟返回的内容(指针,指针指向指针等(

以下是一些示例:

int A[2][2] = {0,1,2,3};
int A[2][2] = {{0,1},{2,3}};
int **A = new int*[2];
int *A = new int[2][2];

在每种情况下,究竟是什么A?是指针,双指针吗?当我做A+1时会发生什么?这些都是声明矩阵的有效方法吗?

另外,为什么第一个选项不需要第二组大括号来定义"列"?

看起来你在我写我的答案时得到了很多答案,但无论如何我都不妨发布我的答案,这样我就不会觉得这一切都是白费的......

(所有sizeof结果均取自VC2012 - 32位构建,指针大小当然会比64位构建翻倍(

size_t f0(int* I);
size_t f1(int I[]);
size_t f2(int I[2]);
int main(int argc, char** argv)
{
    // A0, A1, and A2 are local (on the stack) two-by-two integer arrays
    // (they are technically not pointers)
    // nested braces not needed because the array dimensions are explicit [2][2]
    int A0[2][2] = {0,1,2,3};
    // nested braces needed because the array dimensions are not explicit,
    //so the braces let the compiler deduce that the missing dimension is 2
    int A1[][2] = {{0,1},{2,3}};
    // this still works, of course. Very explicit.
    int A2[2][2] = {{0,1},{2,3}};
    // A3 is a pointer to an integer pointer. New constructs an array of two
    // integer pointers (on the heap) and returns a pointer to the first one.
    int **A3 = new int*[2];
    // if you wanted to access A3 with a double subscript, you would have to
    // make the 2 int pointers in the array point to something valid as well
    A3[0] = new int[2];
    A3[1] = new int[2];
    A3[0][0] = 7;
    // this one doesn't compile because new doesn't return "pointer to int"
    // when it is called like this
    int *A4_1 = new int[2][2];
    // this edit of the above works but can be confusing
    int (*A4_2)[2] = new int[2][2];
    // it allocates a two-by-two array of integers and returns a pointer to
    // where the first integer is, however the type of the pointer that it
    // returns is "pointer to integer array"
    // now it works like the 2by2 arrays from earlier,
    // but A4_2 is a pointer to the **heap**
    A4_2[0][0] = 6;
    A4_2[0][1] = 7;
    A4_2[1][0] = 8;
    A4_2[1][1] = 9;

    // looking at the sizes can shed some light on subtle differences here
    // between pointers and arrays
    A0[0][0] = sizeof(A0);        // 16 // typeof(A0) is int[2][2] (2by2 int array, 4 ints total, 16 bytes)
    A0[0][1] = sizeof(A0[0]);     // 8  // typeof(A0[0]) is int[2] (array of 2 ints)
    A1[0][0] = sizeof(A1);        // 16 // typeof(A1) is int[2][2]
    A1[0][1] = sizeof(A1[0]);     // 8  // typeof(A1[0]) is int[2]
    A2[0][0] = sizeof(A2);        // 16 // typeof(A2) is int[2][2]
    A2[0][1] = sizeof(A2[0]);     // 8  // typeof(A1[0]) is int[2]
    A3[0][0] = sizeof(A3);        // 4 // typeof(A3) is int**
    A3[0][1] = sizeof(A3[0]);     // 4 // typeof(A3[0]) is int*
    A4_2[0][0] = sizeof(A4_2);    // 4 // typeof(A4_2) is int(*)[2] (pointer to array of 2 ints)
    A4_2[0][1] = sizeof(A4_2[0]); // 8 // typeof(A4_2[0]) is int[2] (the first array of 2 ints)
    A4_2[1][0] = sizeof(A4_2[1]); // 8 // typeof(A4_2[1]) is int[2] (the second array of 2 ints)
    A4_2[1][1] = sizeof(*A4_2);   // 8 // typeof(*A4_2) is int[2] (different way to reference the first array of 2 ints)
// confusion between pointers and arrays often arises from the common practice of
// allowing arrays to transparently decay (implicitly convert) to pointers
    A0[1][0] = f0(A0[0]); // f0 returns 4.
    // Not surprising because declaration of f0 demands int*
    A0[1][1] = f1(A0[0]); // f1 returns 4.
    // Still not too surprising because declaration of f1 doesn't
    // explicitly specify array size
    A2[1][0] = f2(A2[0]); // f2 returns 4.
    // Much more surprising because declaration of f2 explicitly says
    // it takes "int I[2]"
    int B0[25];
    B0[0] = sizeof(B0); // 100 == (sizeof(int)*25)
    B0[1] = f2(B0); // also compiles and returns 4.
    // Don't do this! just be aware that this kind of thing can
    // happen when arrays decay.
    return 0;
}
// these are always returning 4 above because, when compiled,
// all of these functions actually take int* as an argument
size_t f0(int* I)
{
    return sizeof(I);
}
size_t f1(int I[])
{
    return sizeof(I);
}
size_t f2(int I[2])
{
    return sizeof(I);
}
// indeed, if I try to overload f0 like this, it will not compile.
// it will complain that, "function 'size_t f0(int *)' already has a body"
size_t f0(int I[2])
{
    return sizeof(I);
}
是的,

此示例有大量有符号/无符号的 int 不匹配,但该部分与问题无关。另外,不要忘记delete使用new创建的所有内容,并delete[]使用new[]创建的所有内容

编辑:

"当我做A+1时会发生什么?"——我之前错过了这个。

像这样的操作将被称为"指针算术"(即使我在答案的顶部喊出其中一些不是指针,但它们可以变成指针(。

如果我有一个指向someType数组P的指针,那么下标访问P[n]与使用此语法*(P + n)完全相同。编译器将考虑在这两种情况下指向的类型的大小。因此,生成的操作码实际上会为您执行类似的事情*(P + n*sizeof(someType))或等效*(P + n*sizeof(*P))因为物理 CPU 不知道或关心我们所有构成的"类型"。最后,所有指针偏移量都必须是字节计数。为了保持一致性,使用指针等数组名称在这里的工作方式相同。

回到上面的示例:A0A1A2A4_2 的行为都与指针算法相同。

A0[0]与引用

A0的前int[2] *(A0+0)相同

同样地:

A0[1] 与将"指针"偏移 sizeof(A0[0]) *(A0+1) 相同(即 8,见上文(,它最终引用了A0的第二个int[2]

A3行为略有不同。这是因为A3是唯一一个不连续存储 2 x 2 数组的所有 4 个整数的数组。在我的示例中,A3指向一个由 2 个 int 指针组成的数组,每个指针都指向两个整数的完全独立的数组。使用 A3[1]*(A3+1) 最终仍然会将您定向到两个 int 数组中的第二个,但它可以通过仅偏移 A3 开头的 4 个字节来做到这一点(使用 32 位指针用于我的目的(,这为您提供了一个指针,告诉您在哪里可以找到第二个 two-int 数组。我希望这是有道理的。

对于数组声明,第一个指定的维度是最外层的维度,即包含其他数组的数组。

对于指针声明,每个*都会添加另一个间接级别。

该语法是为 C 设计的,目的是让声明模拟用法。C 创建者和C++创建者 (Bjarne Stroustrup( 都将语法描述为失败的实验。主要问题是它不遵循数学中通常的替换规则。

在 C++11 中,您可以使用std::array而不是方括号声明。

您还可以定义类似的ptr类型生成器,例如

template< class T >
using ptr = T*;

然后写

ptr<int> p;
ptr<ptr<int>> q;
int A[2][2] = {0,1,2,3};
int A[2][2] = {{0,1},{2,3}};

这些声明Aarray of size 2 of array of size 2 of int 。声明是完全相同的。

int **A = new int*[2];

这声明了一个使用两个指针数组初始化的pointer to pointer to int。如果要将其用作二维数组,则还应为这两个指针分配内存。

int *A = new int[2][2];

这不会编译,因为右侧部分的类型pointer to array of size 2 of int无法转换为pointer to int

在所有有效情况下,A + 1&A[1] 相同,这意味着它指向数组的第二个元素,也就是说,在int A[2][2]的情况下指向两个整数的第二个数组,在int **A的情况下指向数组中的第二个指针。

其他答案已经涵盖了其他声明,但我会解释为什么在前两个初始化中不需要大括号。这两个初始化相同的原因:

int A[2][2] = {0,1,2,3};
int A[2][2] = {{0,1},{2,3}};

是因为它被聚合初始化所涵盖。在这种情况下,允许"省略"(省略(大括号。

C++标准在§ 8.5.1中提供了一个示例:

[...]

float y[4][3] = {
  { 1, 3, 5 },
  { 2, 4, 6 },
  { 3, 5, 7 },
};

[...]

在下面的示例中,省略了初始值设定项列表中的大括号; 但是,初始值设定项列表与 上面示例的完全支撑的初始值设定项列表,

float y[4][3] = {
  1, 3, 5, 2, 4, 6, 3, 5, 7
};
y 的初始值设定项

以左大括号开头,但 y[0] 的初始值设定项 不会,因此使用列表中的三个元素。同样 接下来的三个依次取为 y[1] 和 y[2]。

好的,

我会尝试向您解释:

  1. 这是一个初始化。使用以下值创建一个二维数组:
      A[0
    • ][0] -> 0
    • A[0][1] -> 1
    • A[1][0] -> 2
    • A[1
    • ][1] -> 3
  2. 这与上面完全相同,但在这里您使用大括号。总是这样做,更适合阅读。
  3. int **A 表示您有一个指向整数指针的指针。当你做新的int*[2]时,你将为2个整数指针保留内存。
  4. 这不会被编译。
int A[2][2] = {0,1,2,3};
int A[2][2] = {{0,1},{2,3}};

这两者是等价的。
两者都意味着:"我声明一个整数的二维数组。阵列的大小为 2 x 2"。

然而,记忆不是二维的,它不是以网格形式排列的,而是(在概念上(在一条长线上。在多维数组中,每一行都只在内存中紧跟在前一行之后。正因为如此,我们可以转到 A 指向的内存地址,要么存储两行长度为 2 的行,要么存储一行长度为 4,最终在内存中的结果将是相同的。

int **A = new int*[2];

声明指向名为 A 的指针的指针。
A 存储指向大小为 2 的数组的指针的地址,该数组包含 int s。此数组在堆上分配。

int *A = new int[2][2];

A 是指向int的指针。
该 int 是在堆中分配的 2x2 int数组的开头。

当然,这是无效的:

prog.cpp:5:23: error: cannot convert ‘int (*)[2]’ to ‘int*’ in initialization
  int *A = new int[2][2];

但是由于我们在前两个中看到的情况,这将起作用(并且是 100% 等效的(:

int *A new int[4];
int A[2][2] = {0,1,2,3};

A 是一个由 4 个整数组成的数组。为了程序员的方便,他决定将其声明为二维数组,以便编译器允许编码人员将其作为二维数组进行访问。Coder 在将所有元素放入内存时线性初始化它们。像往常一样,由于 A 是一个数组,A 本身就是数组的地址,因此 A + 1(应用指针数学后(将 A 偏移 2 个整数指针的大小。由于数组的地址指向该数组的第一个元素,因此 A 将指向数组第二行的第一个元素,值 2。

编辑:使用单个数组运算符访问二维数组将沿第一维操作,将第二维视为 0。所以 A[1] 等价于 A[1][0]。A + 1 导致等效指针相加。

int A[2][2] = {{0,1},{2,3}};

A 是一个由 4 个整数组成的数组。为了程序员的方便,他决定将其声明为二维数组,以便编译器允许编码人员将其作为二维数组进行访问。编码器已按行初始化元素。出于上述相同原因,A + 1 指向值 2。

int **A = new int*[2];
A 是指向 int 指针的指针

,该指针已初始化为指向指向 int 指针的 2 个指针数组。由于 A 是一个指针,A + 1 取 A 的值,这是指针数组的地址(因此,数组的第一个元素(并加 1(指针数学(,它现在将指向数组的第二个元素。由于数组没有初始化,实际上用 A + 1 做一些事情(比如读取或写入它(将是危险的(谁知道那里有什么值以及它实际上指向什么,如果它甚至是一个有效的地址(。

int *A = new int[2][2];

编辑:正如Jarod42指出的那样,这是无效的。我认为这可能更接近你的意思。如果没有,我们可以在评论中澄清。

int *A = new int[4];

A 是指向 int 的指针,该指针已初始化为指向 4 个整数的匿名数组。由于 A 是一个指针,A + 1 取 A 的值,这是指针数组的地址(因此,数组的第一个元素(并加 1(指针数学(,它现在将指向数组的第二个元素。

一些要点:

  1. 在前两种情况下,A 是数组的地址,而在后两种情况下,A 是恰好初始化为数组地址的指针的值。
  2. 在前两个中,A 一旦初始化就无法更改。在后两者中,A 可以在初始化后更改并指向其他内存。
  3. 也就是说,您需要小心如何将指针与数组元素一起使用。请考虑以下事项:

    int *a = new int(5);
    int *b = new int(6);
    int c[2] = {*a, *b};
    int *d = a;
    

c+1d+1不同。事实上,访问d+1是非常危险的。为什么?因为 c 是一个 int 数组,它已通过取消引用 ab 进行初始化。这意味着c 是内存块a地址,其中在该内存位置是已设置为变量a指向的值的值,而在下一个内存位置是变量b固定的值。另一方面,d只是a的地址。所以你可以看到,c != d因此,没有理由c + 1 == d + 1.