C指向数组/指针数组的消歧义

503

以下声明之间有什么区别:

int* arr1[8];
int (*arr2)[8];
int *(arr3[8]);

理解更复杂的声明的一般规则是什么?


64
以下是有关于如何阅读C语言复杂声明的优秀文章:http://unixwiz.net/techtips/reading-cdecl.html。 - jesper
很遗憾,那篇文章中缺少了constvolatile这两个既重要又棘手的限定符。 - not-a-user
13个回答

473
int* arr[8]; // An array of int pointers.
int (*arr)[8]; // A pointer to an array of integers

第三个与第一个相同。

一般规则是运算符优先级。当函数指针出现时,它可能会变得更加复杂。


5
因此,对于32位系统: int* arr[8]; /* 为每个指针分配了8个4字节的空间 */ int (arr)[8]; / 只分配了4字节的空间,用于指向数组 */ - George
11
不行。int* arr[8]:总共分配了8个4字节,每个指针占用4字节。int (*arr)[8]是正确的,只占用4字节。 - Mehrdad Afshari
2
我应该重新阅读我写的内容。我的意思是每个指针都是4。感谢您的帮助! - George
5
第一个和最后一个相同的原因是可以始终在声明符周围添加括号。P[N] 是数组声明符,P(....) 是函数声明符,*P 是指针声明符。因此,在以下所有内容中,除了函数的 "()" 之外,所有内容都与没有任何括号的内容相同:int (((*p))); void ((g(void))); int (a[1]); void ((p())). - Johannes Schaub - litb
2
你的解释很好。关于运算符优先级和结合性的深入参考,请参阅Brian Kernighan和Dennis Ritchie的《C程序设计语言》(ANSI C第二版)第53页。操作符( ) [ ]从左到右结合,并且比*具有更高的优先级,因此将int* arr[8]读作大小为8的数组,其中每个元素指向一个整数,而int (*arr)[8]则表示指向包含整数的大小为8的数组的指针。 - Mushy
也许这与主题无关,但指针不应该是8个字节吗? 我猜9年前它们通常是4个字节,但现在它们通常是8个字节,除了旧的x32位处理器和x86位编译模式。一些实现甚至可能会说指针是12个字节。 - Edenia

275

使用 K&R 建议的 cdecl 程序。

$ cdecl
Type `help' or `?' for help
cdecl> explain int* arr1[8];
declare arr1 as array 8 of pointer to int
cdecl> explain int (*arr2)[8]
declare arr2 as pointer to array 8 of int
cdecl> explain int *(arr3[8])
declare arr3 as array 8 of pointer to int
cdecl>

它也可以反过来使用。

cdecl> declare x as pointer to function(void) returning pointer to float
float *(*x)(void )

@ankii 大多数Linux发行版应该都有一个软件包。你也可以自己构建二进制文件。 - sigjuice
啊,抱歉没有提到,我在使用macOS。如果有的话我会看一下,否则网站也可以。^^谢谢你让我知道这个。请随意标记NLN。 - user10063119
2
@ankii 你可以从Homebrew(或者MacPorts?)安装。如果这些不符合你的口味,那么从cdecl.org右上角的Github链接构建自己的二进制文件也是非常简单的(我刚在macOS Mojave上构建了它)。然后只需将cdecl二进制文件复制到你的PATH中。我建议使用$PATH/bin,因为在这么简单的事情上没有必要涉及root权限。 - sigjuice
哦,我没有在自述文件中阅读有关安装的小段落。只是一些处理依赖项的命令和标志.. 使用brew安装的。 :) - user10063119

133

我不知道它是否有官方名称,但我称之为Right-Left Thingy(TM)。

从变量开始,向右走,然后向左,再向右......如此循环。

int* arr1[8];

arr1 是包含 8 个指向整数的指针的数组。

int (*arr2)[8];

arr2是一个指向包含8个整数的数组的指针。(圆括号限制了从右到左的优先级。)

int *(arr3[8]);

arr3是一个包含8个指向整数的指针的数组。

这可以帮助您处理复杂的声明。


20
我听说过它被称为“螺旋法则”,您可以在这里找到相关内容。 - fouric
7
螺旋规则与右左规则是不同的。前者在类似于 int *a[][10] 的情况下会失败,而后者会成功。参考链接如下: 螺旋规则出错右左规则成功 - legends2k
1
正如InkBlend和legends2k所说,这是Spiral Rule,它更加复杂,并且并不适用于所有情况,因此没有理由使用它。 - kotlomoy
不要忘记 ( ) [ ] 的从左至右结合性以及 * & 的从右至左结合性。 - Mushy
@legends2k:int *a[][10] 的声明读法是什么? - beastboy
显示剩余2条评论

28
int *a[4]; // Array of 4 pointers to int

int (*a)[4]; //a is a pointer to an integer array of size 4

int (*a[8])[5]; //a is an array of pointers to integer array of size 5 

第三个应该是:a 是一个包含 8 个整型数组指针的数组,每个整型数组都有大小为 8 吧? - Rushil Paul
2
@Rushil:不,最后一个下标([5])代表内部维度。这意味着(*a[8])是第一维,因此是数组的外部表示。a中每个元素所指向的是大小为5的不同整数数组。 - zeboidlund
谢谢第三个。我正在寻找如何编写指向数组的指针数组。 - Deqing

16
最后两个问题的答案也可以从C语言的黄金法则中得出:
声明应在使用之前。
如果您解引用arr2会发生什么?您将得到一个由8个整数组成的数组。
如果您从arr3中取出一个元素会发生什么?您将得到一个指向整数的指针。
这在处理函数指针时也很有帮助。以sigjuice的示例为例:
float *(*x)(void)
当您解引用x时会发生什么?您将得到一个可以不带参数调用的函数。当您调用它时会发生什么?它将返回一个指向float类型的指针。
操作符优先级总是棘手的。然而,使用括号可能也会令人困惑,因为声明应在使用之前。至少对我来说,arr2看起来像是一个由8个指向int的指针组成的数组,但实际上是相反的。只需花些时间适应即可。如果您问我,这已经足够理由在这些声明中始终添加注释 :)。
顺便说一句,我刚刚偶然遇到了以下情况:一个具有静态矩阵并使用指针算术来查看行指针是否越界的函数。例如:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define NUM_ELEM(ar) (sizeof(ar) / sizeof((ar)[0]))

int *
put_off(const int newrow[2])
{
    static int mymatrix[3][2];
    static int (*rowp)[2] = mymatrix;
    int (* const border)[] = mymatrix + NUM_ELEM(mymatrix);

    memcpy(rowp, newrow, sizeof(*rowp));
    rowp += 1;
    if (rowp == border) {
        rowp = mymatrix;
    }

    return *rowp;
}

int
main(int argc, char *argv[])
{
    int i = 0;
    int row[2] = {0, 1};
    int *rout;

    for (i = 0; i &lt; 6; i++) {
        row[0] = i;
        row[1] += i;
        rout = put_off(row);
        printf("%d (%p): [%d, %d]\n", i, (void *) rout, rout[0], rout[1]);
    }

    return 0;
}

输出:

0 (0x804a02c): [0, 0]
1 (0x804a034): [0, 0]
2 (0x804a024): [0, 1]
3 (0x804a02c): [1, 2]
4 (0x804a034): [2, 4]
5 (0x804a024): [3, 7]

请注意,border的值永远不会改变,因此编译器可以优化它。这与您最初想使用的内容不同:const int (*border)[3]:这将border声明为指向包含3个整数的数组的指针,只要该变量存在,其值将不会更改。但是,该指针可以随时指向任何其他这样的数组。我们希望实现的是参数的这种行为(因为此函数不会更改这些整数中的任何一个)。声明在使用之后。

(附言:请随意改善此示例!)


5
typedef int (*PointerToIntArray)[];
typedef int *ArrayOfIntPointers[];

3
作为一个经验法则,右一元运算符(例如[]()等)比左一元运算符优先级高。所以,int *(*ptr)()[];将是一个指向返回指向int的指针数组的函数的指针(尽快使用右操作符,因为你一旦离开括号就获取到它们)。

这是正确的,但也是非法的。你不能有一个返回数组的函数。我尝试过并得到了这个错误:error: ‘foo’ declared as function returning an array int foo(int arr_2[5][5])[5]; 在GCC 8下使用 $ gcc -std=c11 -pedantic-errors test.c - alx - recommends codidact
1
编译器出现错误的原因是它将函数解释为返回一个数组,这是优先级规则的正确解释。虽然这种声明是非法的,但合法的声明 int *(*ptr)(); 允许稍后使用类似 p()[3](或 (*p)()[3])的表达式。 - Luis Colorado
好的,如果我理解正确的话,你是在讨论创建一个返回指向数组第一个元素的指针(不是数组本身)的函数,然后将该函数用作返回数组的函数?有趣的想法。我会试一下。int *foo(int arr_2[5][5]) { return &(arr_2[2][0]); } 并像这样调用:foo(arr)[4];,这应该包含 arr[2][4],对吗? - alx - recommends codidact
没错...但你也是对的,声明是非法的。 :) - Luis Colorado

2
以下是我的理解:

这是我的理解:

int *something[n];

优先级说明:数组下标操作符([])的优先级高于解引用操作符(*)。

因此,在此处我们将先应用[],然后再应用*,使该语句等价于:
int *(something[i]);

声明的意义: int num 表示num 是一个intint *ptrint (*ptr) 表示ptr的值为一个int,这使得ptr成为指向int的指针。

这可以理解为,(某个东西中第i个索引的值)是一个整数。因此,(某个东西中第i个索引的值)是一个(整数指针),这使得该东西成为整数指针数组。

在第二个例子中,

int (*something)[n];

要理解这个语句,您必须熟悉以下事实:
指针表示数组时的注意事项:`somethingElse[i]` 等同于 `*(somethingElse + i)`。
因此,将 `somethingElse` 替换为 `(*something)`,我们得到 `*(*something + i)`,根据声明,它是一个整数。因此,`(*something)` 给出了一个数组,使得 `something` 等价于(指向数组的指针)。

2

我认为我们可以使用简单的规则...

相关的IT技术内容。
example int * (*ptr)()[];
start from ptr 

"ptr是一个指向函数的指针。向右移动,现在是“)”,再向左移动,是“(”。走出来,向右走到“()”,然后向左移动,“它返回一个指向整数数组的指针”"。

我会稍微改进一下: "ptr是一个指向" 向右走... 是),现在向左走... 是* "一个指针,指向" 向右走... 是),现在向左走... 是一个( 出来,向右走(), 所以 "指向一个不带参数的函数" 向右走... 是[] "并返回一个" 向右走; 结束,所以向左走... 是* "指向" 向左走... 是int "整数"。 - alx - recommends codidact

2

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