【问题标题】:C declarator understandingC 声明符理解
【发布时间】:2020-09-25 14:22:35
【问题描述】:

我正在阅读 C99 ISO 标准中的声明符,但我很难理解以下段落:

5 如果在声明“T D1”中,D1 的形式为

    标识符

那么为ident指定的类型是T

6 如果在声明“T D1”中,D1 的形式为

    ( D )

然后 ident 具有声明“T D”指定的类型。因此,括号中的声明符与未加括号的声明符相同,但复杂声明符的绑定可能会被括号改变。

【问题讨论】:

  • C99 6.7.5p4 -- C11 6.7.6p4 的文字几乎相同
  • 答案可能与 int *foo[]int (*foo)[]int *(foo[]) 等内容之间的差异有关

标签: c standards


【解决方案1】:

您省略了前面的重要段落:

4 在以下小节中,考虑一个声明

    T D1

其中T 包含指定类型T 的声明说明符(例如int),D1 是包含标识符ident 的声明符。在各种形式的声明符中为标识符 ident 指定的类型使用此表示法进行归纳描述。

所以,当我们到达第 5 和第 6 段时,我们知道我们正在考虑的声明中包含一些我们标记为 ident 的标识符。例如,在 int foo(void) 中,identfoo

第 5 段说,如果声明“TD1”只是“Tident”,它声明 ident 为 @987654330 类型@。

第 6 段说如果声明“TD1”只是“T(ident)”,它也声明了ident 类型为T

这些只是为声明的递归规范建立基本情况。第 6.7.5.1 条继续说,如果声明“TD1”是“T*some-qualifiersD”和没有@987654342 的相同声明@ 和限定符“TD”将声明 ident 为“some-derived-type T”(如“T 的数组”或“指向T”的指针),然后带有*和限定符的声明将ident*声明为“some-derived-type 一些限定符指向T”的指针。

例如,int x[3]x 声明为“3 个 int 的数组”,因此 6.7.5.1 中的这条规则告诉我们“int * const x[3] 声明 x 为”3 个 const 指针的数组到int”——它采用之前必须派生的“3 的数组”,并将“const 指针”附加到它。

类似地,第 6.7.5.2 和 6.7.5.3 条告诉我们将数组和函数类型附加到带有括号(用于下标)和后缀括号的声明符。

【讨论】:

  • 什么是“派生声明符类型列表”?
  • @MichaelMunta:这是来自声明者内部的东西。给定一些声明符,比如*(*x[3])(void),你可以通过剥离部分来计算它——*、后缀参数、括号、第二个指针和下标大小。然后以相反的顺序构建派生声明器类型列表:三个数组,然后是三个指针数组,然后是三个指向函数的指针数组,该函数采用 void 并返回,然后是三个指向函数的指针数组,该函数采用 void 并返回指针到。最后,坚持输入int,就完成了。
  • 文档没有在任何地方指定派生声明器类型列表的定义。一个人应该怎么知道?
  • @MichaelMunta: C 2018 6.2.5 20 说可以派生类型。每个子条款 6.7.6.1 到 6.7.6.3 都隐含地定义了一个 derived-declarator-type-list 作为前提;他们说“If [内部声明符 D 使得指定的类型是] 'derived-declarator-type-list T', then ...” 6.7.6 以空的derived-declarator-type-list 开始基本表单,通过说裸或带括号的 ident 指定类型“T”,没有声明符类型。在解析类中熟悉这些内容。
  • 你是怎么学到这些东西的,你以前做过解析器吗?我觉得阅读这样的抽象规范很有趣,但是当文档本身没有解释某些事情时遇到诸如此类的问题会扼杀我的动力。
【解决方案2】:

在引用的定义中,T 是一个类型(例如 intdoublestruct foo 或任何 typedef 名称),而 D1 是一个声明符.

声明器可以任意复杂,但由少量简单步骤构成。他们提到它是归纳定义的,这是另一种说法,它具有递归定义。

最简单的声明符只是一个标识符名称,例如xfoo。所以 T 可能是 intD1 可能是 x

然后继续说声明符可以用括号括起来,例如(x)((x)) 等。这些是退化的情况,都等同于 x,但有时需要括号来生成所需的分组。例如,声明符*x[10](*x)[10] 表示完全不同的东西。前者等价于*(x[10]),是一个指针数组,而后者是一个指向数组的指针。

还有更多内容(数组、指针、函数等),但这涵盖了问题中引用的部分。

【讨论】:

    【解决方案3】:

    long int *ident[4]; (数组 (4) of pointer to long int) long int 是说明符列表,*ident[4] 是声明符。 您可以将声明符放在括号中而不更改语义: long int (*ident[4]);,但在更复杂的声明中,例如 long int (*ident[4])[5]; (array (4) of pointer to array (5) of long int),括号影响绑定,因为没有它们,long int *ident[4][5]; 将被解释为 array (4) of指向 long int 的指针的数组 (5).

    之所以这样工作,是因为声明器部分可以递归地包含另一个声明器,依此类推。

    来自旧的 C 手册(在后来的标准化 Cs 中它变得更间接,但基本原理是相同的):

    declarator:
       identifier
       * declarator
       declarator ( )
       declarator [ constant-expression opt ]
       ( declarator )
    

    换句话说,C 声明符是一种将 pointer-to/function-returning/array-of 前缀到一些说明符列表(例如,long int),这些可以组合起来创建一个链。

    通常,在创建该链时,后缀(()=函数返回,[]=array of)比 */pointer-to 前缀绑定得更紧密。您可以使用括号覆盖它并强制*(或其中几个)绑定now,而不会被[]/() 后缀压倒。

    例如:

     long int *ident[4][5]; //array (4) of array (5) of pointer to long int
     //the suffixes win over the `*` prefix
    
    
     long int (*ident[4])[5]; //array (4) of pointer to array (5) of long int
     //the parens force `pointer to` right after `array (4)` 
     //without letting the `[]` suffix overpower the pointer-to/`*` declarator prefix
    

    附:

    1. C 不允许函数数组和函数返回函数或数组。这些都可以在语法中很好地表达,但是 C 的语义检查会希望你这样做,但需要一个指向链接来防止这些。
    2. cdecl.org 如果你想玩这些可能会很有帮助,也许更有用,因为它不做语义检查,因此允许你甚至像 int foo[]()(); (函数返回函数的数组返回 int),很好地展示了语法是如何工作的,即使 C 编译器会拒绝它们。

    【讨论】:

      【解决方案4】:

      ...复杂声明符的绑定

      这是规范中非常好的提示。

      规则本身真的很难分析,因为它是递归的。 declarationdeclarator 的关系和部分也是相关的。

      结果是:

      • ()[] 是最里面的直接声明符部分,在左边声明(直接通过符号)函数和数组名称
      • * 将名称声明为右侧的指针
      • (...) 对于...复杂的情况需要更改默认关联。

      分组括号会引导您从 inside(标识符)到 outside(左侧的类型说明符,例如“int”)。

      最后是指针符号* 及其所指的内容。重新制定的(括号表示可选,这里不是数组!)语法是:

      declarator: [* [qual]] direct-declarator
      direct-declarator: (declarator)
      

      foo() 是 DD(直接声明符)

      *foo 是一个声明符。 (“间接”演绎)

      *foo() 是 *(foo())。 foo 保持一个函数, () 和 [] 绑定最强。 * 是返回类型。

      (*foo)() 使 foo 成为指针。一对一功能。

      顺便说一句,这也解释了为什么在声明符列表中。

      int const * a, b
      

      都是 const int,但只有 a 是指针

      const 属于 int,star 只属于 a。这样就更清楚了,

      const int x, *pi
      

      但这已经是边缘混淆了。就像现代诗歌一样。适合某些场合。


      即使没有括号,解析也会有轻微的掉头。但这是很自然的:

      3   2 0  1
      int *foo()
      

      这种标准情况(和类似情况)必须很简单。还有著名的多维数组,如 int a[10][10][10]。

      3    1 0  2
      int (*foo)()
      

      这里的括号强制“foo”是左侧的内容(指针)。


      复杂的声明在 K&R C 书中有自己的章节。

      这是最简单复杂的声明:

      int (*(*foo)[])() 
      

      它调试到抽象类型:

      int (*(*)[])() 
      

      替换(*)

      int (*F[])() 
      

      缺少的数组大小会发出编译器警告 - “假设一个元素”。

      作为抽象类型:

      int (*[])()
      

      但是:

      int *G[]()
      

      --> 错误:将“G”声明为函数数组

      是的,你可以,甚至递归,但使用 * indirection 和括号。这样就形成了一个洋葱,中间是标识符,左边是星号,右边是 [] 和 ()。


      C11 规格有这个怪物。 ... 声明可变参数:

      int (*fpfi(int (*)(long), int))(int, ...)
      

      移除所有参数:

      int (*fpfi())()
      

      只是一个返回指针的函数。一个返回 int 的函数。

      但是 fpfi 的第一个参数是一个函数本身——一个指向函数的指针,返回类型和它自己的参数:

      int (*)(long)
      

      非抽象的:

      int (*foo)(long)
      

      一个指向函数的指针,该函数将 long 形式“转换”为 int。

      那是参数。仅有的。返回值也是一个函数指针,指向函数的参数和返回类型在最外层。删除整个内部函数(int (*)(long), int)

      int (*pfi)(int, ...)
      

      或更通用/不完整:

      int (*pfi)()
      

      “T (D)”洋葱规则

      所以这个洋葱游戏重演了。 []()* 之间的由内而外和左右左右。语法不是问题,而是语义。

      【讨论】:

        猜你喜欢
        • 2022-01-25
        • 2019-01-20
        • 1970-01-01
        • 2019-10-30
        • 2022-07-21
        • 2018-01-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多