我最近学习了反混淆复杂声明的螺旋规则,它应该用一系列 typedef 编写。但是,以下评论使我感到震惊:
我没有找到void (*signal(int, void (*fp)(int)))(int);
“简单案例”。顺便说一句,这更令人担忧。
所以,我的问题是,在哪些情况下应用该规则是正确的,在哪些情况下会出错?
我最近学习了反混淆复杂声明的螺旋规则,它应该用一系列 typedef 编写。但是,以下评论使我感到震惊:
我没有找到void (*signal(int, void (*fp)(int)))(int);
“简单案例”。顺便说一句,这更令人担忧。
所以,我的问题是,在哪些情况下应用该规则是正确的,在哪些情况下会出错?
基本上,该规则根本不起作用,或者它通过重新定义螺旋的含义来起作用(在这种情况下,它没有意义。考虑,例如:
int* a[10][15];
螺旋规则会给出一个数组[10],它是一个指向 int 数组 [15] 的指针,这是错误的。你引用的情况,它也不起作用;事实上,在 的情况下signal
,甚至不清楚应该从哪里开始螺旋。
一般来说,找到规则失败的示例比找到规则有效的示例更容易。
我经常想说解析 C++ 声明很简单,但没有尝试过复杂声明的人会相信我。另一方面,它并不像有时想象的那么难。秘诀是将声明完全视为表达式,但运算符要少得多,并且有一个非常简单的优先规则:右侧的所有运算符都优先于左侧的所有运算符。在没有括号的情况下,这意味着首先处理右边的所有内容,然后处理左边的所有内容,并且完全像在任何其他表达式中一样处理括号。实际难度不是语法本身,但它会导致一些非常复杂且违反直觉的声明,特别是在涉及函数返回值和函数指针的情况下:先右后左规则意味着特定级别的运算符通常是分开的,例如:
int (*f( /* lots of parameters */ ))[10];
此处展开的最后一个术语是int[10]
,但将 放在[10]
完整的函数规范之后(至少对我而言)是非常不自然的,我每次都必须停下来解决它。(这可能是逻辑上相邻的部分展开的这种趋势导致了螺旋规则。问题当然是,在没有括号的情况下,它们并不总是展开——任何时候你看到[i][j]
,规则都是正确的,然后再向右走,而不是螺旋形。)
既然我们现在正在考虑表达式的声明:当表达式变得太复杂而无法阅读时,你会怎么做?您引入中间变量是为了使其更易于阅读。在声明的情况下,“中间变量”是typedef
. 特别是,我认为任何时候返回类型的一部分在函数参数之后结束(以及很多其他时间),您应该使用 atypedef
来简化声明。(不过,这是一个“照我说的做,而不是照我做的”的规则。恐怕我偶尔会使用一些非常复杂的声明。)
规则是正确的。但是,在应用它时应该非常小心。
我建议以更正式的方式将其应用于 C99+ 声明。
这里最重要的是识别所有声明的以下递归结构(为简单起见,从图片中删除了const
, volatile
, static
, extern
, inline
, struct
, union
,typedef
但可以轻松添加回来):
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];
你得到:
base-type
=char
derived-part1
= *
, object
= (**(*foo[3][5])(void))
, derived-part2
=[7][9]
derived-part1
= **
, object
= (*foo[3][5])
, derived-part2
=(void)
derived-part1
= *
, object
= foo
, derived-part2
=[3][5]
从那里:
*
[3][5]
foo
**
(void)
*
[3][5]
foo
*
[7][9]
**
(void)
*
[3][5]
foo
char
*
[7][9]
**
(void)
*
[3][5]
foo
现在,从右到左阅读:
foo
是一个由 3 个数组组成的数组,其中包含 5 个指向函数的指针(不带参数),返回一个指针,该指针指向一个由 7 个数组组成的数组的指针,该数组由 9 个指向 char 的指针组成。
您也可以在每个过程中反转数组维度derived-part2
。
这就是你的螺旋法则。
而且很容易看到螺旋。你从左边潜入更深的嵌套[object]
,然后在右边重新浮出水面,只是注意到在上层还有另一对左右,依此类推。
螺旋规则实际上是一种过于复杂的看待它的方式。实际的规则要简单得多:
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”
如果我们开始 a signal
,第一级有一个后缀(函数)和一个前缀(返回指针)。需要下一个级别来查看它指向的内容(函数返回 void)。所以我们最终得到“具有两个参数(int 和指向函数的指针)的函数,返回指向具有一个(int)参数的函数的指针,返回 void”
例如:
int * a[][5];
这不是指向数组的指针数组int
。