int (*a)[3] 的解释是什么?

26

在 C 语言中,当涉及到数组和指针时,我们很快就会发现它们并不完全等同,尽管乍一看可能会这样认为。我了解 L 值和 R 值之间的区别。但最近我尝试找出一种可以与二维数组一起使用的指针类型。

int foo[2][3];
int (*a)[3] = foo;

然而,我无法理解编译器是如何“理解”变量a的类型定义的,尽管有*[]的正常运算符优先级规则。如果我使用typedef,问题会变得简单得多:

int foo[2][3];
typedef int my_t[3];
my_t *a = foo;
在底线处,有人能告诉我编译器如何读取术语int (*a)[3]吗?int a[3]很简单,int *a[3]也很简单。但是,为什么不是int *(a[3])呢?
编辑:当然,我指的是“typedef”而不是“typecast”(这只是笔误)。

你可能会对阅读http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/BitOp/pointer.html感兴趣。 - sand
提供的链接中只有很小一部分与我的原始问题相关,而这个问题已经被下面的答案详细回答了。无论如何,感谢您的回答! - fotNelton
1
请参阅阅读C类型声明阅读C声明:一个指南,以获取更多信息。 - outis
8个回答

53

每当你对于复杂的声明感到困惑时,你可以在类Unix操作系统中使用 cdecl 工具:

[/tmp]$ cdecl
Type `help' or `?' for help
cdecl> explain int (*a)[10];
declare a as pointer to array 10 of int

编辑:

这个工具也有一个在线版本,可以在这里找到。

感谢Tiberiu Ana和gf。


很好的建议,尽管据我所知cdecl是Linux特有的。它绝对不在Unix或Posix规范中。 - Stephen Canon
我很高兴偶然得到了这个答案。我刚刚学到了一些非常有价值的东西! - Varun Madiath

23

首先,在你的问题中,你的意思是“typedef”,而不是“typecast”。

在C语言中,指向类型为T的指针可以指向类型为T的对象:

int *pi;
int i;
pi = &i;

上述内容很容易理解。现在,让我们稍微复杂一些。你似乎知道数组和指针之间的区别(即,你知道数组不是指针,但有时它们的行为类似)。因此,你应该能够理解:

int a[3];
int *pa = a;

但为了完整起见,关于这个赋值语句,变量名 a 等同于 &a[0] ,也就是数组 a 的第一个元素的指针。如果您不确定它是如何工作的以及为什么能够这样工作,有很多答案可以解释关于数组名“衰减”为指针的时机和原因,具体请查看:

我相信在 Stack Overflow 上还有许多类似的问题和回答,我只是列举了一些我从搜索中发现的。

回到主题:当我们有:

int foo[2][3];

foo是类型为“array [2] of array [3] of int”的数组。这意味着foo[0]是一个包含3个int的数组,而foo[1]也是一个包含3个int的数组。

现在假设我们想声明一个指针,并将其赋值给foo[0]。也就是说,我们想执行以下操作:

/* declare p somehow */
p = foo[0];

上述代码与 int *pa = a; 行在形式上没有区别,因为 afoo[0] 的类型相同。因此,我们需要使用 int *p; 来声明 p
现在,关于数组的主要事情是要记住,“规则”关于数组名称衰减到指向其第一个元素的指针仅适用一次。如果您有一个数组的数组,则在值上下文中,数组的名称不会衰减为类型“指向指针”,而是衰减为“指向数组”的指针。回到 foo
/* What should be the type of q? */
q = foo;

上面的foo名称是指向foo第一个元素的指针,也就是说,我们可以将上面的内容写成:
q = &foo[0];
foo[0]的类型是"数组[3],其中包含int类型的元素"。因此,我们需要q成为指向"数组[3],其中包含int类型的元素"的指针:
int (*q)[3];
q周围的括号是必需的,因为在C语言中[]的优先级比*高,所以int *q[3]声明q为指针数组,而我们需要一个指向数组的指针。从上面可以看出,int *(q[3])等同于int *q[3],即一个包含3个指向int的指针的数组。

希望这能帮到您。您还应该阅读聪明人的C语言:数组和指针,这是一个非常好的教程。

关于阅读声明的一般方法:您需要“自内向外”阅读,从“变量”的名称(如果有)开始。尽可能向左移动,除非右侧紧接着一个[],并且始终遵守括号。 cdecl应该能够在某种程度上帮助您:

$ cdecl
cdecl> declare p as  pointer to array 3 of int
int (*p)[3]
cdecl> explain int (*p)[3]
declare p as pointer to array 3 of int

阅读

int (*a)[3];

      a            # "a is"
    (* )           # parentheses, so precedence changes.
                   # "a pointer to"
        [3]        # "an array [3] of"
int        ;       # "int".

为了

int *a[3];

     a             # "a is"
      [3]          # "an array [3] of"
    *              # can't go right, so go left.
                   # "pointer to"
int      ;         # "int".

char *(*(*a[])())()

          a         # "a is"
           []       # "an array of"
         *          # "pointer to"
        (    )()    # "function taking unspecified number of parameters"
      (*        )   # "and returning a pointer to"
                 () # "function"
char *              # "returning pointer to char"

(例子来自C语言常见问题第1.21问题。 实际上,如果您正在阅读这样一个复杂的声明,代码中一定有严重问题!)


1
+1 是指解释数组名称衰减为指向第一个元素的指针仅适用一次。 - Andy
非常感谢您详细的答复。事实上,我已经了解了数组和指针的相关内容,但是我并不知道如何阅读类型声明规则。非常棒的例子! - fotNelton

22

它声明了一个指向包含3个int的数组的指针。

必须加上括号,因为以下语句声明了一个包含3个指向int的指针的数组:

int* a[3];

使用 typedef 可以提高可读性:

typedef int threeInts[3];
threeInts* pointerToThreeInts;

抱歉,我不明白你的意思。这已经在 OP 中说过了? - fotNelton
@kapuzineralex:4月15日的所有答案都是被管理员从另一个问题合并到这里的,我想它们不太适合在这里 :/ - Georg Fritzsche

6

意思是

声明a为指向整型数组3的指针

参见cdecl以供参考。


谢谢,但我已经知道它的含义了。我的问题针对编译器,而不是表达式的最终含义。 - fotNelton

4

这对编译器可能比对你来说更容易解释。一种语言定义了什么构成有效表达式的规则,编译器使用这些规则来解释这样的表达式(听起来可能很复杂,但编译器已经被深入研究,并且有许多标准化的算法和工具可用,您可以阅读 parsing)。cdecl 是一个将 C 表达式翻译成英语的工具。源代码可在网站上查看。如果我没记错的话,《C++程序设计语言》一书包含了编写此类程序的示例代码。

至于自己解释这些表达式,我在《Thinking in C++》第一卷中找到了最好的技巧:

为了定义一个没有参数和返回值的函数指针,你需要这样写:void (*funcPtr)(); 当你看到这样复杂的定义时,最好的方法是从中间开始,向外扩展。 "从中间开始" 意味着从变量名 funcPtr 开始。 "向外扩展" 意味着向右查找最近的项(在这种情况下没有任何东西;右括号使您停止),然后向左查找(由星号表示的指针),然后向右查找(空参数列表表示不带参数的函数),然后向左查找(void 表示该函数没有返回值)。这种右-左-右运动适用于大多数声明。 回顾一下,“从中间开始”(“funcPtr 是一个...”),向右走(没有任何东西-右括号使你停下来),向左走并找到 '*'("...指向...的指针"),向右走并找到空的参数列表("...不带参数的函数..."),向左走并找到 void("funcPtr 是一个指向不带参数且返回 void 的函数的指针")。
您可以在Bruce Eckel的下载站点免费下载该书。让我们尝试将其应用于int (*a)[3]
  1. 从中间的名称开始,即a。到目前为止,它读作:“a是一个…”
  2. 向右移动,遇到一个关闭括号停止,因此向左移动并找到*。现在你读到了:“a是指向…”
  3. 向右移动,你会发现[3],意思是一个有3个元素的数组。表达式变成了:“a是指向一个由3个元素组成的数组…”
  4. 最后向左移动,找到int,所以最终你得到“a是指向一个由3个int组成的数组”。

谢谢,这也缩小了我想问的范围。 - fotNelton

2
为什么“非int *(a[3])”确切指的是什么(指你上一个问题)?“int *(a[3])”和简单的“int *a[3]”是相同的,花括号是多余的。它是一个包含3个指向“int”的指针的数组,并且您说您知道它的含义。
“int (*a)[3]”是指向包含3个“int”的数组的指针(即指向“int [3]”类型的指针)。在这种情况下,大括号很重要。您自己提供了通过中间“typedef”声明等效声明的正确示例。
在C语言中,常用的从内到外的简单流行的声明读取规则在这种情况下完美奏效。在“int *a[3]”声明中,我们必须从左边开始,即先去右边,才能得到“a[3]”,这意味着“a”是一个包含3个“something”的数组(等等)。在“int (*a)[3]”中,括号“()”改变了这一点,因此我们不能向右移动,而必须从“*a”开始:a是指向“something”的指针。继续解码声明,我们将得出结论:that“something”是一个包含3个“int”的数组。
无论如何,请找到一份好的教程,学习如何读取C语言声明,在网络上有很多这样的教程。

2

你可能会发现这篇文章很有帮助:

我已经将这个答案设为社区wiki(因为它只是一篇文章链接,而不是一个答案),如果有人想要添加更多资源,请随意编辑。


2
在你不知道声明是什么的情况下,我发现所谓的右左规则非常有帮助。
顺便说一句,一旦你学会了它,这样的声明就像小菜一碟 :)

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