【问题标题】:Square brackets after pointer declaration/reference指针声明/引用后的方括号
【发布时间】:2021-10-17 20:05:14
【问题描述】:

我正在将代码从 C 移植到 Go,并且在 C 中遇到了一些我以前从未见过的东西。在寻找和学习这样的新概念时,我喜欢认为自己非常有能力和自给自足,但在这种情况下,我绝对无法做到。

我要移植的代码是声明和引用(我认为)是指针,但它们被括号包围,然后用方括号索引。一些例子:

结构定义

typedef struct {
    uint32_t *S;
    uint32_t (*S0)[2], (*S1)[2], (*S2)[2];
} example;

赋值:

ex.S0 = (uint32_t (*)[2])ex.S;
ex.S1 = ex.S0 + (1 << someInt1) * someInt2;
ex.S2 = ex.S1 + (1 << someInt1) * someInt2;

就目前而言,命名、声明和使用不足以让我制定一个能告诉我更多信息的搜索。那么,对于 S0、S1 和 S2,它们是什么,我在哪里可以了解有关声明和使用它们的更多信息?

【问题讨论】:

  • 这些是指向数组的指针
  • 指针指向2个元素的数组
  • 我宁愿说它们是指针数组。所以变量 S 是一个指向一个或多个 uint32_t 的指针,但 S0、S1 和 S2 都是两个指针的数组。指针通常指向某事物的可能更多元素中的第一个。
  • @Henrik Carlqvist: uint32_t (*S0)[2] 使 S0 成为一个指向两个 uint32_t 的数组的指针。 uint32_t *(*S0)[2] 是指向指针数组的指针。
  • 是的,@sj95126,但更重要的可能是指向uint32_t 的两个指针的数组将拼写为uint32_t *S0[2]。在示例声明中,正是括号的功能产生了前者(指向数组的指针)而不是后者(指针数组)。

标签: c pointers multidimensional-array declaration implicit-conversion


【解决方案1】:

括号是声明符语法的一部分。它们表示数组。

在 C 中,declaration 是一个 declaration-specifiers 列表,后跟一个带有可选初始化的 declarators 列表。

declaration-specifiers 指定一个类型,如 intdoublelong longstruct foo 或 typedef 名称。 (它们还包括其他类型的说明符,例如externinline,这在此答案中不重要。)无论<i>T</i> 是什么类型,声明符都会指定具有<i>T</i> 类型的事物。

声明符有多种形式:

  • 一个普通的名称<i>D</i> 表示<i>D</i> 的类型为<i>T</i>
  • 括号中的声明符(<i>D</i>)表示表达式(<i>D</i>)的类型为<i>T</i>,这意味着<i>D</i>也有类型<i>T</i>,因此这与声明符中的普通名称具有相同的含义.但是,它将声明符分组以供进一步解析。
  • 带括号的声明符<i>D</i>[<em>expression</em>] 表示表达式<i>D</i>[i] 将具有<i>T</i> 类型,这意味着<i>D</i> 必须是该类型的数组。
  • 带有星号*<i>D</i> 的声明符表示表达式*<i>D</i> 的类型为<i>T</i>,因此<i>D</i> 必须是指向<i>T</i> 的指针。
  • 带括号的声明符<i>D</i>() 表示表达式<i>D</i>() 将具有<i>T</i> 类型,这意味着<i>D</i> 必须是返回<i>T</i> 的函数。

这些形式可以组合。声明符(*<i>D</i>)[] 表示(*<i>D</i>)[] 的类型为<i>T</i>,所以(*<i>D</i>)<i>T</i> 的数组,所以*<i>D</i> 也是<i>T</i> 的数组,所以<i>D</i> 是指向数组的指针<i>T</i>.

本质上,声明符是一个名称将如何在表达式中使用的图片,名称的实际类型是通过从该表达式的类型向后推算出来的。

(上面没有讨论的声明器的其他部分,例如函数的参数列表和声明数组时括号内容的一些选项。)

【讨论】:

    【解决方案2】:

    假设你有一个二维数组:

    uint32_t array[][2] = {
        {1, 2},
        {3, 4},
        {5, 6},
        {7, 8},
        {9, 10},
        {0, 0}
    };
    

    我们知道这真的不是一个“二维”数组。它实际上是一个数组数组。 array 是一些东西的数组,每个东西是两个uint32_t 的小数组。所以array[0] 是两个uint32_t 的数组,array[1] 是两个uint32_t 的数组,等等。

    假设,出于某种原因,您想要操作指向该数组中行的指针。也许您想使用如下代码向下移动数组:

    sometype p;
    p = array;
    while(p is not at the end of array) {
        do something with the array pointed to by p;
        p++;
    }
    

    或者假设您想再次通过指针跟踪数组中的某些行:

    sometype row2, row4;
    row2 = &array[2];
    row4 = &array[4];
    

    我们知道这是有道理的,因为array 是一些东西的数组,所以array[2] 是第二个(实际上是第三个,因为从0 开始)的东西,&amp;array[2] 是指向那个东西的指针。同样,在这种情况下,“某物”是两个 uint32_t 的数组。

    但问题是,sometype 是什么?我们希望prow2row4 具有类型“指向两个uint32_t 的数组的指针”。这是一种不寻常的类型,但它确实存在,而且是您一直在询问的类型:

    uint32_t (*p)[2];
    

    这表示p 正是一个指向两个uint32_t 的数组的指针。当然,一个大而明显的子问题是:(*p) 部分周围的括号到底是做什么的?答案是没有他们,我们会有

    uint32_t *p[2];        /* not what we want */
    

    这会将p 声明为一个由两个指向uint32_t 的指针组成的数组。在 C 中,声明中存在优先关系,就像表达式中存在优先关系一样,[] 的绑定比* 更紧密。所以如果你想说p首先是一个指针(它的指针是任何数组),你需要括号:

    uint32_t (*p)[2];
    

    所以现在我们可以重写我之前编写的尝试扫描数组的循环:

    uint32_t (*p)[2];
    p = array;
    while((*p)[0] != 0 || (*p)[1] != 0) {
        printf("next row: %d, %d\n", (*p)[0], (*p)[1]);
        p++;
    }
    

    这是可行的,我鼓励你编译并运行它。还有一些更“有趣”的括号,同样是因为优先级。我们的p 确实是一个指向两个uint32 的小数组的指针。所以(*p) 两个uint32 的数组。所以(*p)[0] 是指向数组的第一个元素,(*p)[1] 是第二个元素。但是如果没有括号,我们就会有*p[0]*p[1],它们基本上会尝试首先将p 视为一个数组,然后是一个指针,这不是我们想要的。 (实际上,正如@sj95126 和我在 cmets 中讨论的那样,*p[1] 之类的符号 确实 工作 - 起初我认为这是一个错误 - 但它做了一些不同的事情,即访问子数组的[0] 元素过去p,而不是p 的数组的[1] 元素。)

    最后,在这一点上观察“复杂”类型(例如指向数组的指针)可能很有用,C 的typedef 机制有时可以使事情变得更清晰。之前我写过,作为伪代码,

    sometype p;
    

    因为我们不太确定要使用什么类型。但是,现在我们知道了,如果我们想要更方便地声明变量,例如 prow2row4,我们可以编写

    typedef uint32_2 (*sometype)[2];
    

    这就像我们之前对p 的声明,只是它在前面多了一个关键字typedef。这改变了一些事情:我们没有将“sometype”声明为指向两个uint32_t的数组的指针;相反,我们将“sometype”声明为 别名 或类型的简写,“指向两个 uint32_t 的数组的指针”。然后我们可以字面上说像

    sometype p;
    

    sometype row2, row4;
    

    typedef 仅在声明 prow2row4 时有帮助。在我们使用它们的表达式中,我们仍然需要额外的括号。并且包含指针的 typedef 也可能令人困惑(也就是说,比它们更方便更令人困惑),因此许多程序员建议避免使用指针 typedef。 (当然“sometype”在实践中是一个可怕的名字。如果你真的写了这个,你会想要像typedef uint32_2 (*ptrary2)[2]; ptrary2 p;这样的东西。)

    有关数组指针的更多讨论,请参阅旧版 C FAQ list 中的 question 6.13

    【讨论】:

    • 那么真正的二维数组会是什么?
    • @JohnBollinger 这不是我坚持的观点,但理论上一个“真实的”二维数组将被声明为uint32_t arr[M, N],你会将它索引为arr[i, j],它会不一定可以操纵指向其行的指针。 (但是,正如我所说,我不是一个书呆子;大多数时候,我非常乐意将 C 中的数组数组称为“多维数组”,因为它的表现确实足够好。 )
    • 我只是在调试器下运行它以确认:(*p)[0] = 3*p[1] = 7 正确填充数组,没有任何编译器警告。在 gdb 中,print *p 显示 {3, 7}。在这种情况下,这两种语法似乎都有效,尽管对于更复杂的形式可能不是这样。
    • @sj95126 这听起来像是一个错误。 p 是一个指针,所以普通的指针运算适用,所以p[i] 基本上是访问另一行。由于该行是一个数组,并且由于数组衰减为指向其第一个元素的指针,*p[i] 最终访问了p 指向的数组的第一个元素(即[0])。特别是,*p[1] 应该访问p 之后的第一行的第一个元素,而不是p 的行的第二个元素。
    • @Steve Summit:我检查了两个环境:使用 gcc 10.3 的 Linux (x86_64) 和使用 gcc 10.2 的 Cygwin。 (*p)[0] = 3*p[0] = 3 都生成完全相同的汇编指令。
    【解决方案3】:

    这个

    uint32_t (*S0)[2], (*S1)[2], (*S2)[2];
    

    是指向数组类型uint32_t[2]的指针的声明。

    例如,如果你有一个像这样的二维数组

    uint32_t a[N][2];
    

    其中N 是某个(在这种情况下不重要)值,那么您可以声明一个指向数组第一个元素的指针,例如

    uint32_t ( *p )[2] = a;
    

    用作初始化器的数组指示符a 被隐式转换为指向其第一个元素的指针。数组的元素是uint32_t[2] 类型的一维数组。

    或者如果你有一个像这样的一维数组

    uint32_t a[2];
    

    那么指向数组的指针的声明可能看起来像

    uint32_t ( *p )[2] = &a;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-21
      • 1970-01-01
      • 2011-05-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多