指向固定大小数组的指针数组

40

我试图将两个固定大小的数组分配给一个指向它们的指针数组,但编译器发出警告,我不明白为什么。

int A[5][5];
int B[5][5];
int*** C = {&A, &B};

这段代码编译时出现以下警告:

警告:不兼容的指针类型初始化[默认启用]

如果我运行这段代码,它会引发一个分段错误。但是,如果我动态分配AB,它就可以正常工作。为什么呢?


5
Hint: check the data types - Sourav Ghosh
3
如果我把A和B声明为动态数组......C语言没有这个功能。你不能“声明A和B为动态数组”。你只能将它们声明为指针,然后手动分配内存,使它们像动态多维数组一样行为。这种“手动构建”的数组与内置的多维数组不兼容——它们是完全不同的两种东西。 - AnT stands with Russia
这段代码产生了未定义行为。 "标量类型的初始化器应该是一个单独的表达式,可以用大括号括起来。" GCC 和 clang 接受很多糟糕的代码(作为扩展?)。使用 MSVC 编译会产生错误(生动例子)。 - AnArrayOfFunctions
只需说 var C={&A, &B},哦,等等,这是什么C? - FastAl
9个回答

25

如果你想声明一个符合 AB 的现有声明的 C ,你需要这样做:

int A[5][5];
int B[5][5];
int (*C[])[5][5] = {&A, &B};

变量C的类型被称为“C是一个指向包含int [5][5]数组的指针数组”。由于不能直接赋整个数组,所以需要分配一个指向该数组的指针。

通过这个声明,(*C[0])[1][2]访问的是与A[1][2]相同的内存地址。

如果你想要像C[0][1][2]这样更清晰的语法,那么你需要像其他人所说的那样动态地分配内存:

int **A;
int **B;
// allocate memory for A and each A[i]
// allocate memory for B and each B[i]
int **C[] = {A, B};

您也可以使用莫斯科的 Vlad 建议的语法来完成这个操作:

int A[5][5];
int B[5][5];
int (*C[])[5] = {A, B};

这个声明中的C应该被理解为"C是指向int [5]数组的指针数组"。在这种情况下,C的每个数组元素都是类型为int (*)[5]的数组,而类型为int [5][5]的数组可以转换为此类型。

现在,您可以使用C[0][1][2]来访问与A[1][2]相同的内存位置。

这种逻辑也可以扩展到更高维度:

int A[5][5][3];
int B[5][5][3];
int (*C[])[5][3] = {A, B};

5
好的!但如果可能的话,我宁愿不碰这个问题。 ;) - sjsam

19

不幸的是,市面上有很多烂书/教程/老师会教你错误的东西......

忘记指向指针,它们与数组没有任何关系。就这样。

还有一个经验法则:每当你发现自己使用超过2级间接引用时,这很可能意味着你的程序设计根本有缺陷,需要从头开始重做。


要正确地做到这一点,你应该这样做:

指向数组 int [5][5] 的指针被称为数组指针,并声明为 int(*)[5][5]。例如:

int A[5][5];
int (*ptr)[5][5] = &A;

如果你想要一个指向数组的数组指针,那么它的类型应该是int(*[])[5][5]。例如:

int A[5][5];
int B[5][5];
int (*arr[2])[5][5] = {&A, &B};

你可以看出这段代码看起来过于复杂 - 实际上它确实如此。访问单个元素会很麻烦,因为你需要输入 (*arr[x])[y][z]。意思是:“在数组指针的数组中,取编号为x的数组指针,获取它所指向的内容 - 这是一个2D数组 - 然后再取该数组中索引为[y][z]的项目。”

发明这样的结构只是一种疯狂的做法,我不会推荐使用。我想通过使用普通的数组指针来简化代码:

int A[5][5];
int B[5][5];
int (*arr[2])[5][5] = {&A, &B};
int (*ptr)[5][5] = arr[0];
...
ptr[x][y][z] = 0;

然而,这仍然是相当复杂的代码。考虑完全不同的设计!例如:

  • 制作一个三维数组。
  • 制作包含2D数组的结构体,然后创建此类结构体的数组。

我认为你的经验法则非常有价值。C++并不“禁止”这样的结构,但它确实可以让你避免使用它们。 - Drew Dormann

14

这行代码存在很多问题。

int*** C = {&A, &B};

你声明了一个单指针C,但是却告诉它指向多个对象;这样是行不通的。你需要将C声明为指向那些数组的指针的数组。
A和B的类型都是int (*)[5][5],即"指向5个元素的int数组的5个元素的数组的指针";因此,C的类型需要是"指针数组,指向5个元素的int数组的5个元素的数组"。
int (*C[2])[5][5] = { &A, &B };

这段文字的意思是

      C           -- C is a
      C[2]        -- 2-element array of
     *C[2]        -- pointers to
    (*C[2])[5]    -- 5-element arrays of
    (*C[2])[5][5] -- 5-element arrays of
int (*C[2])[5][5] -- int

哎呀,这太丑陋了。如果你想通过 C 访问 AB 中的任何一个元素,情况会变得更加丑陋:

int x = (*C[0])[i][j]; // x = A[i][j]
int y = (*C[1])[i][j]; // y = B[i][j]

在我们可以索引指向数组的指针 C[i] 之前,我们必须明确地取消引用它,由于下标运算符 [] 的优先级高于一元运算符 *,因此我们需要将 *C[0] 分组放在括号中。
我们可以稍微整理一下。除非它是 sizeof 或一元 & 运算符的操作数(或者是用于声明中初始化另一个数组的字符串字面量),类型为 "N 元素数组 of T" 的表达式将被转换("衰减")为类型为 "指向 T 的指针" 的表达式,并且表达式的值将是数组的第一个元素的地址。
表达式 AB 的类型为 int [5][5],或者是 "5 元素数组 of 5 元素数组 of int"。根据上述规则,两个表达式都会 "衰减" 为类型为 "指向 5 元素数组 of int 的指针" 的表达式,即 int (*)[5]。如果我们使用 AB 来初始化数组,而不是使用 &A&B,那么我们需要一个指向 5 元素数组 of int 的指针数组。
int (*C[2])[5] = { A, B };

好的,这仍然很难理解,但这已经是没有typedefs的最清晰的表达方式了。
那么我们如何通过C访问A和B的元素呢?
记住数组下标操作a[i]被定义为*(a+i);也就是说,给定一个基地址a,从该地址偏移i个元素(而不是字节),然后对结果进行解引用。这意味着:
*a == *(a + 0) == a[0]

因此,
*C[i] == *(C[i] + 0) == C[i][0]

把这些结合起来:
C[0] == A                      // int [5][5], decays to int (*)[5]
C[1] == B                      // int [5][5], decays to int (*)[5]

*C[0] == C[0][0] == A[0]       // int [5], decays to int *
*C[1] == C[1][0] == B[0]       // int [5], decays to int *

C[0][i] == A[i]                // int [5], decays to int *
C[1][i] == B[i]                // int [5], decays to int *

C[0][i][j] == A[i][j]          // int
C[1][i][j] == B[i][j]          // int

我们可以将 C 索引,就好像它是一个由int元素组成的3D数组,这比使用(*C[i])[j][k]更清晰。

这个表格也可能会有帮助:

Expression        Type                "Decays" to       Value
----------        ----                -----------       -----
         A        int [5][5]           int (*)[5]       Address of A[0]
        &A        int (*)[5][5]                         Address of A
        *A        int [5]              int *            Value of A[0] (address of A[0][0])
      A[i]        int [5]              int *            Value of A[i] (address of A[i][0])
     &A[i]        int (*)[5]                            Address of A[i]
     *A[i]        int                                   Value of A[i][0]   
   A[i][j]        int                                   Value of A[i][j]   

请注意,A&AA[0]&A[0]&A[0][0]都产生相同的(数组的地址和数组第一个元素的地址总是相同的),但它们的类型是不同的,如上表所示。
指针算术运算会考虑所指向类型的大小;如果p包含一个int对象的地址,则p+1将产生下一个int对象的地址,该地址可能相距2到4个字节。

非常好,特别是您关于如何声明 C 以便可以像三维数组一样进行索引的建议。 - John Bollinger

9
一个常见的C语言初学者误解是他们认为指针和数组是等价的,这完全是错误的。
当初学者看到下面的代码时,就会感到困惑:
int a1[] = {1,2,3,4,5};
int *p1 = a1;            // Beginners intuition: If 'p1' is a pointer and 'a1' can be assigned
                         // to it then arrays are pointers and pointers are arrays.

p1[1] = 0;               // Oh! I was right
a1[3] = 0;               // Bruce Wayne is the Batman! Yeah.

现在,初学者已经证实数组是指针,指针是数组,因此他们进行了这样的实验:
int a2[][5] = {{0}};
int **p2 = a2;

然后出现了一个关于不兼容指针赋值的警告,然后他们想:“哦天啊!为什么这个数组变成了Harvey Dent?”

有些人甚至更进一步。

int a3[][5][10] = {{{0}}};
int ***p3 = a3;             // "?"

然后Riddler就会涉及到他们关于数组指针等价性的噩梦。

enter image description here

请记住,数组不是指针,反之亦然。数组是一种数据类型,而指针是另一种数据类型(它不是数组类型)。几年前就在C-FAQ中讨论过这个问题:

说数组和指针是“等价的”并不意味着它们是相同的,甚至可以互换。它的意思是数组和指针算术被定义为指针可以方便地用于访问数组或模拟数组。换句话说,正如Wayne Throop所说的那样,在C语言中,“指针算术和数组索引是等价的,指针和数组是不同的。”

现在,请始终记住一些重要的数组规则,以避免这种混淆:
  • 数组不是指针。指针也不是数组。
  • 在表达式中使用数组时,除了sizeof&运算符的操作数外,数组会转换为指向其第一个元素的指针。
  • 指针算术和数组索引是相同的。
  • 指针和数组是不同的。
  • 我说过“指针不是数组,反之亦然”吗?

现在你知道了这些规则,可以得出结论:

int a1[] = {1,2,3,4,5};
int *p1 = a1;

a1是一个数组,在声明语句int *p1 = a1;中,它被转换为指向其第一个元素的指针。它的元素类型是int,那么指向其第一个元素的指针将是int *类型,与p1兼容。

int a2[][5] = {{0}};
int **p2 = a2;

a2是一个数组,在int **p2 = a2;中它会退化为指向其第一个元素的指针。它的元素是int[5]类型(2D数组是1D数组的数组),所以它的第一个元素的指针类型应该是int(*)[5](指向数组的指针),与int **类型不兼容。正确的写法应该是

int (*p2)[5] = a2;

同样适用于
int a3[][5][10] = {{{0}}};
int ***p3 = a3;

a3的元素是int [5][10]类型,指向其第一个元素的指针应该是int (*)[5][10]类型,但是p3int ***类型,为了使它们兼容,应该

int (*p3)[5][10] = a3;

现在来看你的代码片段。
int A[5][5];
int B[5][5];
int*** C = {&A, &B};

&A&B的类型是int(*)[5][5]C的类型是int***,它不是一个数组。由于您想让C保存AB数组的地址,因此需要将C声明为包含两个int(*)[5][5]类型元素的数组。应该这样做:

int (*C[2])[5][5] = {&A, &B};

然而,如果我动态分配A和B,它就可以正常工作。为什么会这样呢?
在这种情况下,您必须将A和B声明为int **。在这种情况下,两者都是指针,而不是数组。C的类型是int ***,因此它可以保存int **类型数据的地址。请注意,在这种情况下,声明int *** C = {&A, &B}; 应该是
  int*** C = &A;

int*** C = {&A, &B};的情况下,程序的行为可能是未定义的或实现定义的。
C11:5.1.1.3(P1):
引用: 规范的实现应生成至少一条诊断消息(以实现定义的方式标识),如果预处理翻译单元或翻译单元包含任何语法规则或约束的违规,则即使行为也明确指定为未定义或实现定义,也应如此。
阅读this post以获得进一步的解释。

@GauravJain;使用C99编译该代码,您将会得到警告 - haccks
好的...但是为什么它能工作呢?当我使用gcc编译它时发生了什么巧合? - Flying disk
@GauravJain; 两者都是使用GCC编译的,唯一的区别是您没有使用C99标志,因此默认情况下您的代码以C89模式编译。这个较旧版本的C可能不会产生任何警告。 - haccks

8
数组在C语言中并不等同于多维指针。通常情况下,数组的名称被解释为包含它的缓冲区的地址,而不管你如何对其进行索引。如果将A声明为int A[5][5],则A通常表示第一个元素的地址,即它被有效地解释为int *(实际上是int *[5]),而不是int **。地址的计算恰好需要两个元素:A[x][y] = A + x + 5 * y。这是一个方便的做法,可以执行A[x + 5 * y],但它并不使A提升为多维缓冲区。
如果您想在C语言中使用多维指针,那么也可以这样做。语法非常相似,但需要进行更多的设置。有几种常见的方法可以实现。
用单个缓冲区:
int **A = malloc(5 * sizeof(int *));
A[0] = malloc(5 * 5 * sizeof(int));
int i;
for(i = 1; i < 5; i++) {
    A[i] = A[0] + 5 * i;
}

使用每行单独的缓冲区:

int **A = malloc(5 * sizeof(int *));
int i;
for(i = 0; i < 5; i++) {
    A[i] = malloc(5 * sizeof(int));
}

3
数组和指针完全不是同一回事,无论是多维还是其他情况。此外,数组的名称表示该数组本身,而不是任何指针。重要的是不要将此与一个事实混淆,即在大多数情况下(但并非全部情况),数组值会转变为指针。 - John Bollinger
3
另外,对于给定的int A[5][5],当A退化为指针时,该指针的类型是int (*)[5],而不是int * - John Bollinger
1
当您想要2D数组时,请不要像这样分配数据,这只是一种坏习惯,有很多缺点而没有好处。您想要创建这样的结构的唯一原因是每个维度都需要单独的大小。但在这种情况下并非如此。 - Lundin
2
(actually int *[5]) 不正确。这是指针数组而不是数组指针。您计算偏移量的方法不正确。A[x + 5 * y]A[x][y] 不相同。 - 2501
2
这个回答不仅没有解答问题,而且像@JohnBollinger和@2501指出的那样误导人。 另外,如果A被声明为int A [5] [5],那么A通常会意味着第一个元素的地址:不是的。它总是表示A是5个数组中每个数组都包含有5个整数的5个数组的数组。 没有更多,也没有更少。 - haccks
显示剩余4条评论

7

您可能会被数组和指针的等价性所困惑。

当您声明一个像A[5][5]这样的数组时,因为您已经声明了两个维度,C语言将会连续地分配25个对象的内存。也就是说,内存将会按照以下方式分配:

A00, A01, ... A04, A10, A11, ..., A14, A20, ..., A24, ...

产生的对象A是指向这块内存起始位置的指针。它的类型是int *而不是int **

如果你想要一个指向数组的指针向量,你需要将变量声明为:

int   *A[5], *B[5];

这将为您提供:
A0, A1, A2, A3, A4

所有类型为int*的内容,您需要使用malloc()或其他方法进行填充。

或者,您可以将C声明为int **C


从OP的角度来看,int **C 的问题在于它丢失了二维索引信息。无论如何,这是一个很好的答案。 - Mad Physicist
4
数组和指针之间不存在等价关系。它们有联系,但并不相同。对于int A[5][5]A所衰减的指针类型是int (*)[5],而不是int * - John Bollinger
你可以考虑重新表述“指向数组的指针向量”,因为在后续语句中,你有两个指针数组。这样做不会产生预期的效果。 - sjsam
1
很抱歉要说,但是你的回答是误导性的。你说:“结果对象A是指向这个内存块开头的指针。它的类型是int *,而不是int **。” 这完全是错误的。对象A是一个数组类型。在某些情况下,它将转换为指向其第一个元素的指针。A的类型是int[5][5]。转换后,它的类型为int (*)[5]。再次强调:“如果您想要一个指向数组的指针向量,您需要将变量声明为:int *A [5],*B [5];”:不是的。这将声明指针数组,而不是指向数组的指针向量 - haccks
int *A[5], *B[5]; 为什么你说这些是指向数组的向量(在C语言中没有这个意思,但我们假设你的意思是数组),当它们显然不是? - 2501

7
尽管数组和指针密切相关,但它们并不是完全相同的东西。人们有时会对此感到困惑,因为在大多数情况下,数组值会衰减为指针,并且因为数组符号可以用于函数原型中声明实际上是指针的参数。此外,许多人认为的数组索引符号实际上执行指针算术和解引用的组合,因此它既适用于指针值,也适用于数组值(因为数组值会衰减为指针)。
给定声明:
int A[5][5];

变量A表示一个由五个包含五个int元素的数组组成的数组。它会自动转换为指向由5个int元素组成的数组的指针,即int (*)[5]类型的指针。而指向整个多维数组的指针则具有不同的类型:int (*)[5][5](指向由5个包含5个int元素的数组组成的数组的指针),与int***(指向int的指针,再指向指针,再指向指针)完全不同。如果你想声明一个指向这种多维数组的指针,可以像这样进行:
int A[5][5];
int B[5][5];
int (*C)[5][5] = &A;

如果您想声明一个指向这些指针的数组,则可以这样做:

int (*D[2])[5][5] = { &A, &B };

新增:

这些区别在各种方式中发挥作用,其中一些更重要的是数组值不会在某些情况下衰减为指针,并与这些情况相关的上下文。最显著的一个情况是当一个值是sizeof运算符的操作数时。根据以上声明,所有以下关系表达式求值结果均为1 (true):

sizeof(A)       == 5 * 5 * sizeof(int)
sizeof(A[0])    == 5 * sizeof(int)
sizeof(A[0][4]) == sizeof(int)
sizeof(D[1])    == sizeof(C)
sizeof(*C)      == sizeof(A)

此外,这些关系表达式可能会计算出1,但不能保证:
sizeof(C)       == sizeof(void *)
sizeof(D)       == 2 * sizeof(void *)

这对数组索引的工作方式至关重要,而在分配内存时理解它是必不可少的。

3

你应该像这样声明第三个数组

int A[5][5];
int B[5][5];
int ( *C[] )[N][N] = { &A, &B };

这意味着它是指向二维数组的指针数组。

例如:

#include <stdio.h>

#define N   5

void output( int ( *a )[N][N] )
{
    for ( size_t i = 0; i < N; i++ )
    {
        for ( size_t j = 0; j < N; j++ ) printf( "%2d ", ( *a )[i][j] );
        printf( "\n" );
    }
}

int main( void )
{
    int A[N][N] =
    {
        {  1,  2,  3,  4,  5 },
        {  6,  7,  8,  9, 10 },
        { 11, 12, 13, 14, 15 },
        { 16, 17, 18, 19, 20 },
        { 21, 22, 23, 24, 25 }
    };
    int B[N][N] =
    {
        { 25, 24, 23, 22, 21 },
        { 20, 19, 18, 17, 16 },
        { 15, 14, 13, 12, 11 },
        { 10,  9,  8,  7,  6 },
        {  5,  4,  3,  2,  1 }
    };

/*
    typedef int ( *T )[N][N];
    T C[] = { &A, &B };
*/

    int ( *C[] )[N][N] = { &A, &B };

    output( C[0] );
    printf( "\n" );

    output( C[1] );
    printf( "\n" );
}        

程序输出如下:
 1  2  3  4  5 
 6  7  8  9 10 
11 12 13 14 15 
16 17 18 19 20 
21 22 23 24 25 

25 24 23 22 21 
20 19 18 17 16 
15 14 13 12 11 
10  9  8  7  6 
 5  4  3  2  1 

或者类似于
int A[5][5];
int B[5][5];
int ( *C[] )[N] = { A, B };

这意味着它是一个指向二维数组第一个元素的指针数组。

例如:

#include <stdio.h>

#define N   5

void output( int ( *a )[N] )
{
    for ( size_t i = 0; i < N; i++ )
    {
        for ( size_t j = 0; j < N; j++ ) printf( "%2d ", a[i][j] );
        printf( "\n" );
    }
}

int main( void )
{
    int A[N][N] =
    {
        {  1,  2,  3,  4,  5 },
        {  6,  7,  8,  9, 10 },
        { 11, 12, 13, 14, 15 },
        { 16, 17, 18, 19, 20 },
        { 21, 22, 23, 24, 25 }
    };
    int B[N][N] =
    {
        { 25, 24, 23, 22, 21 },
        { 20, 19, 18, 17, 16 },
        { 15, 14, 13, 12, 11 },
        { 10,  9,  8,  7,  6 },
        {  5,  4,  3,  2,  1 }
    };

/*
    typedef int ( *T )[N];
    T C[] = { A, B };
*/

    int ( *C[] )[N] = { A, B };

    output( C[0] );
    printf( "\n" );

    output( C[1] );
    printf( "\n" );
}        

程序输出与上述相同。
 1  2  3  4  5 
 6  7  8  9 10 
11 12 13 14 15 
16 17 18 19 20 
21 22 23 24 25 

25 24 23 22 21 
20 19 18 17 16 
15 14 13 12 11 
10  9  8  7  6 
 5  4  3  2  1 

根据你要如何使用第三个数组,可能需要不同的翻译方式。

使用typedef(在演示程序中以注释形式显示)可以简化数组的定义。

至于这个声明:

int*** C = {&A, &B};

然后在左侧声明了一个类型为int ***的指针,它是一个标量对象,而右侧有一个不同类型的初始化器列表,其类型为int ( * )[N][N]

因此,编译器会发出一条消息。


我认为这两个都没有涵盖到原帖作者实际想要做的事情(3D索引)。 - Mad Physicist
C只有两个元素,而不是五个。 - Mad Physicist

-1

我非常相信使用 typedef

#define SIZE 5

typedef int  OneD[SIZE]; // OneD is a one-dimensional array of ints
typedef OneD TwoD[SIZE]; // TwoD is a one-dimensional array of OneD's
                         // So it's a two-dimensional array of ints!

TwoD a;
TwoD b;

TwoD *c[] = { &a, &b, 0 }; // c is a one-dimensional array of pointers to TwoD's
                           // That does NOT make it a three-dimensional array!

int main() {
    for (int i = 0; c[i] != 0; ++i) { // Test contents of c to not go too far!
        for (int j = 0; j < SIZE; ++j) {
            for (int k = 0; k < SIZE; ++k) {
//              c[i][j][k] = 0;    // Error! This proves it's not a 3D array!
                (*c[i])[j][k] = 0; // You need to dereference the entry in c first
            } // for
        } // for
    } // for
    return 0;
} // main()

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