C不是那么难:void(*(* f )())()
我今天刚看了一张照片,觉得我会很感激的解释。 所以这里是图片:
我发现这是令人困惑的,并想知道这样的代码是否可行。 我GOOGLE了图片,发现在这个 reddit条目中的另一张图片,这里是图片:
那么这个“螺旋式阅读”是有效的? 这是如何C编译器parsing?
如果对这个奇怪的代码有更简单的解释,那将会很棒。
除此之外,这些代码可以用吗? 如果是的话,何时何地?
有一个关于“螺旋规则”的问题,但我不只是问如何应用它,或者如何用这个规则来读取expression式。 我质疑这样的expression式和螺旋规则的有效性的使用。 关于这些,一些很好的答案已经发布。
有一条规则叫“顺时针/螺旋规则”来帮助find一个复杂的声明的含义。
从c-faq :
有三个简单的步骤:
以未知元素开始,以螺旋/顺时针方向移动; 当生成以下元素时,用相应的英语语句replace它们:
[X]
或[]
=>数组X的大小…或数组未定义的大小…
(type1, type2)
=>函数传递type1和type2返回…
*
=>指向…的指针继续以螺旋/顺时针方向进行操作,直到所有的令牌都被覆盖。
总是先解决任何括号内的问题!
您可以检查上面的链接的例子。
另外请注意,为了帮助您,还有一个网站叫做:
你可以input一个C声明,它会给出它的英文含义。 对于
void (*(*f[])())()
它输出:
声明f为指向函数的数组返回指向返回void的函数的指针
编辑:
正如Random832的评论所指出的那样 ,螺旋规则并不能解决数组的数组问题,并且会导致这些声明(大部分)的错误结果。 例如对于int **x[1][2];
螺旋规则忽略了[]
优先于*
的事实。
当位于数组数组的前面时,可以在应用螺旋规则之前先添加明确的括号。 例如: int **x[1][2];
与int **(x[1][2]);
(也是有效的C)由于优先级和螺旋规则,然后正确地读取它为“x是指向int指针的数组2的数组1”,这是正确的英文声明。
请注意, 詹姆斯·坎泽 ( 詹姆斯· 坎泽在评论中指出)的这个问题也包含了这个问题 。
“螺旋”规则types脱离了以下优先规则:
T *a[] -- a is an array of pointer to T T (*a)[] -- a is a pointer to an array of T T *f() -- f is a function returning a pointer to T T (*f)() -- f is a pointer to a function returning T
下标[]
和函数调用()
运算符的优先级高于一元的*
,所以*f()
被parsing为*(f())
, *a[]
被parsing为*(a[])
。
所以如果你需要一个指向数组的指针或指向函数的指针,那么你需要明确地将*
与标识符分组,如(*a)[]
或(*f)()
。
然后你意识到a
和f
可以是比标识符更复杂的expression式; 在T (*a)[N]
, a
可以是一个简单的标识符,或者可以是像(*f())[N]
( a
– > f()
)这样的函数调用,也可以是像(*p[M])[N]
,( a
– > p[M]
),或者它可以是一个指向数组的指针数组,如(*(*p[M])())[N]
> (*p[M])()
)等
如果间接运算符*
是后缀而不是一元的,这将是很好的,这将使声明从左到右读取更容易( void f[]*()*();
肯定比void (*(*f[])())()
),但事实并非如此。
当你遇到这样的毛茸茸的声明时,首先find最左边的标识符并应用上面的优先级规则,recursion地将它们应用于任何函数参数:
f -- f f[] -- is an array *f[] -- of pointers ([] has higher precedence than *) (*f[])() -- to functions *(*f[])() -- returning pointers (*(*f[])())() -- to functions void (*(*f[])())(); -- returning void
标准库中的signal
函数可能是这种疯狂的types标本:
signal -- signal signal( ) -- is a function with parameters signal( sig, ) -- sig signal(int sig, ) -- which is an int and signal(int sig, func ) -- func signal(int sig, *func ) -- which is a pointer signal(int sig, (*func)(int)) -- to a function taking an int signal(int sig, void (*func)(int)) -- returning void *signal(int sig, void (*func)(int)) -- returning a pointer (*signal(int sig, void (*func)(int)))(int) -- to a function taking an int void (*signal(int sig, void (*func)(int)))(int); -- and returning void
在这一点上大多数人都说“使用typedef”,这当然是一个select:
typedef void outerfunc(void); typedef outerfunc *innerfunc(void); innerfunc *f[N];
但…
你如何在expression式中使用 f
? 你知道这是一个指针数组,但你如何使用它来执行正确的function? 你必须通过typedefs来解决正确的语法。 相比之下,“裸”版本是相当眼睛,但它告诉你如何在expression式中使用 f
(即(*(*f[i])())();
;,假设这两个函数都不带参数)。
在C语言中,声明反映了用法 – 这就是它在标准中的定义。 声明:
void (*(*f[])())()
是expression式(*(*f[i])())()
产生void
types的结果的断言。 意思是:
-
f
必须是一个数组,因为你可以索引它:f[i]
-
f
的元素必须是指针,因为你可以解引用它们:*f[i]
-
这些指针必须是指向没有参数的函数的指针,因为你可以调用它们:
(*f[i])()
-
这些函数的结果也必须是指针,因为你可以解引用它们:
*(*f[i])()
-
这些指针也必须是指向不带任何参数的函数的指针,因为你可以调用它们:
(*(*f[i])())()
-
这些函数指针必须返回
void
“螺旋规则”只是提供了一种理解同一事物的不同方式的助记符。
那么这个“螺旋式阅读”是有效的?
应用螺旋规则或使用cdecl总是无效的。 在某些情况下都失败了。 螺旋规则适用于许多情况,但并不普遍 。
要破译复杂的声明,请记住这两条简单的规则:
-
始终从内向外读取声明 :从最内层(如果有的话)括号开始。 find正在声明的标识符,并从那里开始解密声明。
-
当有select的时候,总是用
[]
和()
来代替*
:如果*
在标识符之前,并且[]
在其之后,标识符代表一个数组,而不是一个指针。 同样,如果*
在标识符之前,并且()
跟在标识符之后,则标识符表示一个函数,而不是一个指针。 (括号总是可以用来覆盖[]
和()
的正常优先级,而不是*
。)
这个规则实际上涉及从标识符的一侧到另一侧的之字形 。
现在破译一个简单的声明
int *a[10];
适用规则:
int *a[10]; "a is" ^ int *a[10]; "a is an array" ^^^^ int *a[10]; "a is an array of pointers" ^ int *a[10]; "a is an array of pointers to `int`". ^^^
让我们来解释这个复杂的声明吧
void ( *(*f[]) () ) ();
通过应用上述规则:
void ( *(*f[]) () ) (); "f is" ^ void ( *(*f[]) () ) (); "f is an array" ^^ void ( *(*f[]) () ) (); "f is an array of pointers" ^ void ( *(*f[]) () ) (); "f is an array of pointers to function" ^^ void ( *(*f[]) () ) (); "f is an array of pointers to function returning pointer" ^ void ( *(*f[]) () ) (); "f is an array of pointers to function returning pointer to function" ^^ void ( *(*f[]) () ) (); "f is an array of pointers to function returning pointer to function returning `void`" ^^^^
这是一个GIF演示如何去(点击图片查看大图):
这里提到的规则取自KN KING的“ C Programming A Modern Approach ”一书。
这只是一个“螺旋”,因为在这个声明中,每一级括号内只有一个操作员。 声称你在“螺旋式”进行的时候,通常会build议你在声明int ***foo[][][]
交替使用数组和指针,实际上所有的数组级别都在指针级别之前。
我怀疑这样的结构可以在现实生活中有任何用处。 我甚至憎恨他们作为常规开发者的面试问题(对于编译器作者来说可能行)。 应该使用typedefs来代替。
作为一个随机的琐事事实,你可能会发现有一个真正的英文单词来描述C语言是如何被读出来的 ,这可能是有趣的: Boustrophedonically ,也就是说,从左到右交替地从右到左。
参考: Van der Linden,1994 – 第76页
关于这个的用处,在使用shellcode的时候,你会看到这个构造很多:
int (*ret)() = (int(*)())code; ret();
虽然不是很复杂,但是这个特殊的模式出现了很多。
在这个 SO问题中更完整的例子。
所以虽然原始图片的用处是有问题的(我build议任何产品代码都应该大大简化),但是有一些语法结构确实出现了很多。
声明
void (*(*f[])())()
只是一个晦涩的说法
Function f[]
同
typedef void (*ResultFunction)(); typedef ResultFunction (*Function)();
实际上,将需要更多的描述性名称而不是ResultFunction和Function 。 如果可能的话,我也将参数列表指定为void
。
我发现Bruce Eckel描述的方法是有帮助和容易的:
定义一个函数指针
要定义一个指向没有参数和返回值的函数的指针,你可以这样说:
void (*funcPtr)();
当你正在看这样一个复杂的定义时,攻击它的最好方法就是从中间开始,一路走下去。 “从中间开始”是指从variables名称funcPtr开始。 “走出去”意味着向右看最近的项目(在这种情况下没有任何东西,右边的括号使你短),然后向左看(用星号表示的指针),然后向右看空参数列表指示不带参数的函数),然后向左看(void,表示该函数没有返回值)。 这个右 – 左 – 右运动适用于大多数声明。
要回顾一下,“从中间开始”(“funcPtr是一个…”),向右(没有任何 – 你被右括号停下来),向左走,find“*”(“ …指向…“),向右移动并find空的参数列表(”…不带参数的函数…“),向左边findvoid(”funcPtr is指向函数的指针,不带参数并返回void“)。
你可能想知道为什么* funcPtr需要括号。 如果你没有使用它们,编译器会看到:
void *funcPtr();
你会声明一个函数(返回一个void *)而不是定义一个variables。 你可以把编译器看作是当你计算出声明或定义应该是什么的时候经历的同一个过程。 它需要这些括号“碰撞”,所以它回到左边,find“*”,而不是继续向右,find空的参数列表。
复杂的声明和定义
另外,一旦你弄清楚C和C ++声明的语法是如何工作的,你可以创build更复杂的项目。 例如:
//: C03:ComplicatedDefinitions.cpp /* 1. */ void * (*(*fp1)(int))[10]; /* 2. */ float (*(*fp2)(int,int,float))(int); /* 3. */ typedef double (*(*(*fp3)())[10])(); fp3 a; /* 4. */ int (*(*f4())[10])(); int main() {} ///:~
穿过每一个,并使用左右指南来弄清楚。 数字1表示“fp1是一个函数的指针,它接受一个整型参数并返回一个指向10个无效指针数组的指针。
数字2表示“fp2是一个函数指针,它接受三个参数(int,int和float),并返回一个指向一个函数的指针,该函数接受一个整数参数并返回一个浮点数。
如果你正在创build很多复杂的定义,你可能需要使用typedef。 数字3显示了typedef如何保存每次input复杂的描述。 它说:“一个fp3是一个函数的指针,它不接受任何参数,并返回一个指向10个指针数组的指针,这个指针指向不带任何参数和返回双精度的函数。”然后它说“a是这些fp3types之一”typedef通常用于从简单的描述中构build复杂的描述。
数字4是一个函数声明,而不是一个variables定义。 它说:“f4是一个函数,它返回一个指向返回整数的函数的10个指针数组的指针。
你将很less需要如此复杂的声明和定义。 但是,如果你仔细研究它们,你将不会受到现实生活中可能遇到的稍微复杂的问题的轻微干扰。
摘自:Thinking in C ++第1卷,第2版,第3章,Bruce Eckel的“函数地址”部分。
记住C声明的这些规则
优先从不会被怀疑:
从后缀开始,继续前缀,
从里面读出两套。
– 我,八十年代中期
当然,除了括号修改外。 请注意,声明这些语法的语法正好反映了使用该variables获取基类实例的语法。
说真的,这一眼就不难学, 你只需要愿意花一些时间来练习这个技巧。 如果你要维护或改编其他人编写的C代码,那肯定值得投资。 吓跑其他没有学过它的程序员也是一个有趣的派对伎俩。
对于你自己的代码:和往常一样,事情可以被写成一行代码并不意味着它应该是,除非它是一个非常普通的模式已经成为一个标准的习惯用法(如string复制循环) 。 如果你使用分层的typedef和逐步的引用来build立复杂的types,而不是依靠你的能力来产生和parsing这些“一举成名”,那么你和跟随你的人将会更加快乐。 性能将会一样好,代码的可读性和可维护性将会大大提高。
这可能会更糟,你知道的。 有一个合法的PL / I声明开始于类似于:
if if if = then then then = else else else = if then ...
- void
(*(*f[]) ()) ()
解决void
>>
-
(*(*f[]) ())
()= void
Resoiving ()
>>
- (*
(*f[]) ()
)=函数返回(void)
解决*
>>
-
(*f[])
()=指向(函数返回(void))
解决()
>>
- (*
f[]
)=函数返回(指向(函数返回(void)))
解决*
>>
-
f
[] =指向(函数返回(指向(函数返回(void))))
解决[ ]
>>
- f =数组指针(返回函数(指向函数返回(void)))))
我碰巧是这么多年前(当我有很多头发的时候)编写的螺旋规则的原始作者,并且当它被添加到cfaq时被授予了荣誉。
我写了螺旋规则,使我的学生和同事们更容易阅读“头脑中”的C语言声明。 也就是说,不必使用像cdecl.org这样的软件工具等。我从来没有意图宣称螺旋规则是parsingCexpression式的规范方法。 我很高兴地看到这个规则多年来帮助成千上万的C程序devise学生和从业者!
作为logging,
在很多地方,包括由林纳斯·托瓦兹(Linus Torvalds)(我非常尊重的人),已经“正确地”认出了这一点,即我的螺旋规则“崩溃”了。 最常见的是:
char *ar[10][10];
正如在这个线程中的其他人所指出的那样,规则可以被更新来说,当你遇到数组时,简单地使用所有的索引就好像写成这样:
char *(ar[10][10]);
现在,遵循螺旋规则,我会得到:
“ar是指向char的10×10二维数组”
我希望这个螺旋法则能够在学习C的过程中发挥它的作用!
PS:
我喜欢“C不难”的形象:)