【问题标题】:The spiral rule about declarations — when is it in error?关于声明的螺旋规则——它什么时候出错了?
【发布时间】:2013-04-22 01:02:15
【问题描述】:

我最近学习了 the spiral rule 用于对复杂声明进行反混淆,这应该是用一系列 typedef 编写的。但是,下面的评论让我感到震惊:

A frequently cited simplification, which only works for a few simple cases.

我没有发现void (*signal(int, void (*fp)(int)))(int); 是一个“简单案例”。顺便说一句,这更令人担忧。

所以,我的问题是,在哪些情况下应用该规则是正确的,在哪些情况下会出错?

【问题讨论】:

  • 出错的时候建议你问Vorac;我不知道在什么情况下会出错。
  • @Jonathan Leffler ,所以没有已知问题,螺旋规则永远不会出错?
  • 好的——所以是James Kanze 在 1994 年发表的文章中有views(不是你;为误读归因而道歉)。我不知道詹姆斯对此的反对意见是什么;我不知道它失败的情况(但我没有认真研究过)。这与说没有这种情况不同,但我认为像这样的无法解释、未经证实的评论就像烟雾一样。
  • @JonathanLeffler 即使在一些简单的声明中也会出错,例如int* a[][10];。否则,你重新定义“螺旋”,最终得到比实际定义更糟糕的结果。
  • @JamesKanze 在上述情况下int* a[][10] right-left rule 有效。你知道这条规则也像螺旋规则一样无效的情况吗?基本上我试图找到一个规则来统治他们所有人:)

标签: c declaration


【解决方案1】:

基本上,规则根本不起作用,否则它 通过重新定义螺旋的含义来工作(在这种情况下, 这没有任何意义。例如:

int* a[10][15];

螺旋规则将给出一个数组 [10] 的指针 int 的array[15],这是错误的。你引用的情况,它 也不行;其实在signal的情况下,并不是 甚至清楚应该从哪里开始螺旋。

通常,更容易找到规则失败的示例 比它有效的例子。

我经常想说解析 C++ 声明是 简单,但没有人尝试过复杂的声明 会相信我。另一方面,它并不像现在那么难 有时被证明是。秘诀就是想 声明与表达式完全一样,但有很多 更少的运算符,以及一个非常简单的优先规则:所有运算符 右侧的运算符优先于左侧的所有运算符。在 没有括号,这意味着将所有内容处理到 先向右,再向左,然后处理 括号与您在任何其他表达式中的完全一样。这 实际的困难不是语法本身,而是它 结果是一些非常复杂且违反直觉的声明, 特别是函数返回值和指针的地方 涉及功能:先右后左规则意思 特定级别的操作员通常分散很大, 例如:

int (*f( /* lots of parameters */ ))[10];

这里展开的最后一个词是int[10],但是把 完整功能规范后的[10] 是(在 至少对我来说)非常不自然,我必须停下来解决 每一次。 (这可能是逻辑相邻的趋势 部分展开,导致螺旋规则。问题 当然,在没有括号的情况下,它们不会 总是散开——只要你看到[i][j],规则就是去 对,然后再向右走,而不是螺旋形。)

因为我们现在正在考虑声明 表达式:当表达式变得太时你会怎么做 阅读复杂?你按顺序引入中间变量 使其更易于阅读。在声明的情况下, “中间变量”是typedef。特别是,我会 争辩说,任何时候返回类型的一部分都在 函数参数(以及很多其他时候),你 应该使用typedef 使声明更简单。 (这 然而,这是一个“照我说的做,而不是照我做”的规则。恐怕 我偶尔会使用一些非常复杂的声明。)

【讨论】:

  • “在没有括号的情况下,这意味着首先处理左边的所有内容,然后处理右边的所有内容”。不应该是反过来的吗?正如您自己所说,右侧的所有运算符都优先于左侧的所有运算符。所以它应该是:首先 - 向右,然后 - 向左。
  • @AnT 是的。我肯定打错了那句话。我会改正的,谢谢。 (令人惊讶的是,直到现在还没有人注意到这个错误。)
【解决方案2】:

规则是正确的。但是,使用它时应该非常小心。

我建议以更正式的方式将其应用于 C99+ 声明。

这里最重要的是识别所有声明的以下递归结构(constvolatilestaticexterninlinestructuniontypedef为简单起见已从图片中删除,但可以轻松添加回来):

base-type [derived-part1: *'s] [object] [derived-part2: []'s or ()]

是的,就是这样,四个部分。

where

  base-type is one of the following (I'm using a bit compressed notation):
    void
    [signed/unsigned] char
    [signed/unsigned] short [int]
    signed/unsigned [int]
    [signed/unsigned] long [long] [int]
    float
    [long] double
    etc

  object is
      an identifier
    OR
      ([derived-part1: *'s] [object] [derived-part2: []'s or ()])

  * is *, denotes a reference/pointer and can be repeated
  [] in derived-part2 denotes bracketed array dimensions and can be repeated
  () in derived-part2 denotes parenthesized function parameters delimited with ,'s
  [] elsewhere denotes an optional part
  () elsewhere denotes parentheses

一旦你解析了所有 4 个部分,

  [object] 是 [derived-part2(包含/返回)] [derived-part2(指向)] base-type 1

如果有递归,你会在递归堆栈的底部找到你的object(如果有的话),它将是最里面的一个,你将通过返回并收集并获得完整的声明在每个递归级别组合派生部分。

解析时,您可以将[object] 移动到[derived-part2] 之后(如果有)。这将为您提供一个线性化、易于理解的声明(参见上面的 1)。

因此,在

char* (**(*foo[3][5])(void))[7][9];

你得到:

  1. base-type = char
  2. 1级:derived-part1 = *, object = (**(*foo[3][5])(void)), derived-part2 = [7][9]
  3. 2级:derived-part1 = **, object = (*foo[3][5]), derived-part2 = (void)
  4. 3级:derived-part1 = *, object = foo, derived-part2 = [3][5]

从那里:

  1. 3级:*[3][5]foo
  2. 2级:**(void)*[3][5]foo
  3. 1级:*[7][9]**(void)*[3][5]foo
  4. 最后,char*[7][9]**(void)*[3][5]foo

现在,从右到左阅读:

foo 是一个由 3 个数组组成的数组,其中包含 5 个指向函数的指针(不带参数),返回一个指向由 9 个指向 char 的指针组成的 7 个数组的指针的指针。

您也可以在此过程中反转每个derived-part2 中的数组维度。

这是你的螺旋法则。

而且很容易看到螺旋。您从左侧潜入嵌套更深的[object],然后在右侧重新浮出水面,却发现上层还有另一对左右,依此类推。

【讨论】:

  • 有趣。首先你说这个规则是正确的,然后你解释如何阅读一个没有使用它的声明,实际上是矛盾的。
  • 您可能会提到,当您递归时(我会使用术语嵌套,但归根结底是同一件事),声明的嵌套部分不包含您的 base-part(除了函数参数,你有一个完整的声明嵌套在另一个声明中)。
  • @JamesKanze 您要提及的内容应该立即显而易见。注意object is... 下的任何地方都没有base-type
  • 嗯,这对您和我来说都很明显,但对于在解析声明时遇到问题的其他人来说可能值得一提。
  • @JamesKanze 是什么让您认为文本允许在声明中的某处添加额外的base-type's(除了在函数参数列表或sizeof(type) 的数组维度表达式中)?
【解决方案3】:

螺旋规则实际上是一种过于复杂的看待它的方式。实际的规则要简单得多:

postfix is higher precedence than prefix.

就是这样。这就是您需要记住的全部内容。 “复杂”的情况是当你有括号来覆盖后缀高于前缀的优先级时,但你真的只需要找到匹配的括号,然后先查看括号内的内容,如果那不完整, 拉入括号外的下一级,后缀优先。

所以看看你的复杂例子

void (*signal(int, void (*fp)(int)))(int);

我们可以从任何名字开始,然后弄清楚那个名字是什么。如果你从int 开始,你就完成了——int 是一个类型,你可以自己理解它。

如果你从fp 开始,fp 不是一个类型,它的名字被声明为某种东西。所以看看第一组括号括起来:

                        (*fp)

没有后缀(先处理后缀),然后前缀*表示指针。指向什么?尚未完成,请查看另一个级别

                   void (*fp)(int)

后缀首先是“函数采用 int 参数”,然后前缀是“返回 void”。所以我们有 fn 是“指向函数的指针,采用 int 参数,返回 void”

如果我们开始一个signal,第一层有一个后缀(函数)和一个前缀(返回指针)。需要下一个级别来查看它指向的内容(函数返回 void)。所以我们最终得到“具有两个参数(int 和指向函数的指针)的函数,返回指向具有一个(int)参数的函数的指针,返回 void”

【讨论】:

    【解决方案4】:

    例如:

    int * a[][5];
    

    这不是指向int 数组的指针数组。

    【讨论】:

    • 那么这是什么?
    猜你喜欢
    • 2011-04-12
    • 2021-05-01
    • 2016-10-06
    • 2012-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多