C 没那么难:void ( *( *f[] ) () ) ()

C isn't that hard: void ( *( *f[] ) () ) ()

我今天刚看到一张照片,希望得到解释。所以这是图片:

我发现这令人困惑,想知道这样的代码是否实用。我用谷歌搜索了这张图片,在 this reddit 条目中找到了另一张图片,这是那张图片:

所以这个 "reading spirally" 有效吗?这是 C 编译器解析的方式吗?
如果对这个奇怪的代码有更简单的解释就太好了。
除此之外,这些代码能有用吗?如果有,何时何地?

a question 关于 "spiral rule",但我不只是询问它是如何应用的或如何使用该规则读取表达式。我也在质疑此类表达式的用法和螺旋规则的有效性。关于这些,已经发布了一些不错的答案。

有一个名为 "Clockwise/Spiral Rule" 的规则可以帮助找到复杂声明的含义。

来自 c-faq:

There are three simple steps to follow:

  1. Starting with the unknown element, move in a spiral/clockwise direction; when ecountering the following elements replace them with the corresponding english statements:

    [X] or []
    => Array X size of... or Array undefined size of...

    (type1, type2)
    => function passing type1 and type2 returning...

    *
    => pointer(s) to...

  2. Keep doing this in a spiral/clockwise direction until all tokens have been covered.

  3. Always resolve anything in parenthesis first!

您可以查看上面的 link 示例。

另请注意,还有一个网站可以帮助您:

http://www.cdecl.org

你可以输入一个C声明,它会给出它的英文含义。对于

void (*(*f[])())()

它输出:

declare f as array of pointer to function returning pointer to function returning void

编辑:

正如 Random832 在评论中指出的那样,螺旋规则不涉及数组的数组,并且会导致(大部分)这些声明中的错误结果。例如,对于 int **x[1][2];,螺旋规则忽略了 []*.

具有更高优先级的事实

在array of arrays的前面,可以先加显式的括号,再应用spiral rule。例如:int **x[1][2];int **(x[1][2]);(也是有效的 C)相同,因为优先级和螺旋规则然后将其正确读为 "x is an array 1 of array 2 of pointer to pointer to int",这是正确的英文声明。

请注意,此问题也已在本answer by James Kanze (pointed out by haccks评论中提及。

我怀疑这样的结构在现实生活中有任何用处。我什至讨厌它们作为普通开发人员的面试问题(对于编译器编写者来说可能没问题)。应该改用 typedef。

关于这个的用处,在使用 shellcode 时你经常看到这个结构:

int (*ret)() = (int(*)())code;
ret();

虽然在语法上没有那么复杂,但这种特殊模式经常出现。

this SO 问题中有更完整的示例。

因此,虽然原始图片中的实用性值得怀疑(我建议任何生产代码都应该大大简化),但确实有一些句法结构出现了很多。

"spiral" 规则不符合以下优先规则:

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()被解析为*(f())*a[] 被解析为 *(a[])

所以如果你想要一个指向数组的指针或一个指向函数的指针,那么你需要明确地将 * 与标识符分组,如 (*a)[](*f)() .

然后你意识到 af 可以是比标识符更复杂的表达式;在 T (*a)[N] 中,a 可以是一个简单的标识符,也可以是像 (*f())[N] (a -> f()) 这样的函数调用,也可以是像 (*p[M])[N]、(a -> p[M]) 这样的数组,或者它可以是像 (*(*p[M])())[N] 这样的函数指针数组 (a -> (*p[M])()), 等等

如果间接运算符 * 是后缀而不是一元就好了,这将使声明更容易从左到右阅读(void f[]*()*(); 肯定比 void (*(*f[])())()), 但事实并非如此。

当你遇到这样一个毛茸茸的声明时,首先找到 最左边的 标识符并应用上面的优先规则,递归地将它们应用于任何函数参数:

         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函数大概就是这种疯狂的典型:

       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

此时大多数人会说"use typedefs",这当然是一个选项:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

但是...

如何在表达式中使用 f?你知道它是一个指针数组,但你如何使用它来执行正确的函数呢?您必须检查 typedef 并找出正确的语法。相比之下,"naked" 版本相当难看,但它告诉您如何在表达式中 使用 f(即 (*(*f[i])())();,假设两个函数都不接受参数)。

Remember these rules for C declares
And precedence never will be in doubt:
Start with the suffix, proceed with the prefix,
And read both sets from the inside, out.
-- me, mid-1980's

当然,括号修饰的除外。请注意,声明这些的语法完全反映了使用该变量获取基 class.

实例的语法。

说真的,乍一看并不难学;你只需要愿意花一些时间练习这项技能。如果您要维护或改编其他人编写的 C 代码,那么 绝对 值得投入时间。这也是一个有趣的派对技巧,可以吓坏其他没有学过它的程序员。

对于您自己的代码:一如既往,某些东西 可以 写成 one-liner 的事实并不意味着它应该是,除非它是一个非常已成为标准习语的常见模式(例如 string-copy 循环)。如果您使用分层类型定义和 step-by-step 解引用构建复杂类型,而不是依赖于您生成和解析这些 的能力,您和那些追随您的人将会 快乐 "at one swell foop." 性能会一样好,代码的可读性和可维护性也会大大提高。

情况可能会更糟,你知道的。有一个合法的 PL/I 语句以这样的开头:

if if if = then then then = else else else = if then ...

在 C 语言中,声明反映了用法——这就是它在标准中的定义方式。宣言:

void (*(*f[])())()

是表达式(*(*f[i])())()产生void类型结果的断言。这意味着:

  • f 必须是一个数组,因为你可以索引它:

    f[i]
    
  • f 的元素必须是指针,因为您可以取消引用它们:

    *f[i]
    
  • 这些指针必须是指向不带参数的函数的指针,因为您可以调用它们:

    (*f[i])()
    
  • 这些函数的结果也必须是指针,因为您可以取消引用它们:

    *(*f[i])()
    
  • 这些指针必须是指向不带参数的函数的指针,因为您可以调用它们:

    (*(*f[i])())()
    
  • 那些函数指针必须returnvoid

“螺旋法则”只是一个助记符,提供了理解同一事物的不同方式。

它只是一个"spiral",因为在这个声明中,每一级括号内的每一侧恰好只有一个运算符。声称你继续 "in a spiral" 通常会建议你在声明 int ***foo[][][] 中交替使用数组和指针,而实际上所有数组级别都在任何指针级别之前。

作为一个随机的琐事类事实,您可能会发现英语中有一个实际的词来描述如何读取 C 声明:Boustrophedonically,即 right-to-left 与 left-to-right.

参考:Van der Linden, 1994 - Page 76

So this "reading spirally" is something valid?

应用螺旋法则或使用cdecl are not valid always. Both fails in some cases. Spiral rule works for many cases, but it is not universal

要破译复杂的声明,请记住这两个简单的规则:

  • 总是从里到外阅读声明:从最里面的括号开始,如果有的话。找到正在声明的标识符,并从那里开始破译声明。

  • 当有选择的时候,总是偏爱[]()而不是*:如果*在标识符之前,[]在它之后,标识符表示一个数组,而不是一个指针。同样,如果 * 位于标识符之前并且 () 位于标识符之后,则标识符代表一个函数,而不是一个指针。 (括号总是可以用来覆盖 []() 相对于 * 的正常优先级。)

此规则实际上涉及从标识符的一侧到另一侧的之字形

现在破译一个简单的声明

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(单击图片可查看大图):


这里说的规则摘自书C Programming A Modern Approach by K.N KING.

我发现 Bruce Eckel 描述的方法很有帮助且易于遵循:

Defining a function pointer

To define a pointer to a function that has no arguments and no return value, you say:

void (*funcPtr)();

When you are looking at a complex definition like this, the best way to attack it is to start in the middle and work your way out. “Starting in the middle” means starting at the variable name, which is funcPtr. “Working your way out” means looking to the right for the nearest item (nothing in this case; the right parenthesis stops you short), then looking to the left (a pointer denoted by the asterisk), then looking to the right (an empty argument list indicating a function that takes no arguments), then looking to the left (void, which indicates the function has no return value). This right-left-right motion works with most declarations.

To review, “start in the middle” (“funcPtr is a ...”), go to the right (nothing there – you're stopped by the right parenthesis), go to the left and find the ‘*’ (“... pointer to a ...”), go to the right and find the empty argument list (“... function that takes no arguments ... ”), go to the left and find the void (“funcPtr is a pointer to a function that takes no arguments and returns void”).

You may wonder why *funcPtr requires parentheses. If you didn't use them, the compiler would see:

void *funcPtr();

You would be declaring a function (that returns a void*) rather than defining a variable. You can think of the compiler as going through the same process you do when it figures out what a declaration or definition is supposed to be. It needs those parentheses to “bump up against” so it goes back to the left and finds the ‘*’, instead of continuing to the right and finding the empty argument list.

Complicated declarations & definitions

As an aside, once you figure out how the C and C++ declaration syntax works you can create much more complicated items. For instance:

//: 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() {} ///:~ 

Walk through each one and use the right-left guideline to figure it out. Number 1 says “fp1 is a pointer to a function that takes an integer argument and returns a pointer to an array of 10 void pointers.”

Number 2 says “fp2 is a pointer to a function that takes three arguments (int, int, and float) and returns a pointer to a function that takes an integer argument and returns a float.”

If you are creating a lot of complicated definitions, you might want to use a typedef. Number 3 shows how a typedef saves typing the complicated description every time. It says “An fp3 is a pointer to a function that takes no arguments and returns a pointer to an array of 10 pointers to functions that take no arguments and return doubles.” Then it says “a is one of these fp3 types.” typedef is generally useful for building complicated descriptions from simple ones.

Number 4 is a function declaration instead of a variable definition. It says “f4 is a function that returns a pointer to an array of 10 pointers to functions that return integers.”

You will rarely if ever need such complicated declarations and definitions as these. However, if you go through the exercise of figuring them out you will not even be mildly disturbed with the slightly complicated ones you may encounter in real life.

Taken from: Thinking in C++ Volume 1, second edition, chapter 3, section "Function Addresses" by Bruce Eckel.

声明

void (*(*f[])())()

只是一种晦涩的说法

Function f[]

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

在实践中,需要更具描述性的名称,而不是 ResultFunctionFunction。如果可能的话,我还将参数列表指定为 void.

  • 无效(*(*f[]) ()) ()

正在解析void>>

  • (*(*f[]) ())()=无效

正在解决 () >>

  • (*(*f[]) ()) = 函数返回(无效)

正在解析*>>

  • (*f[]) () = 指向(函数返回 (void) )的指针

正在解析()>>

  • (*f[]) = 函数返回(指向(函数返回(void))的指针)

正在解析*>>

  • f[] = 指向(函数返回的指针(指向(函数返回的指针 (无效))))

正在解析[ ]>>

  • f = (指向(函数的指针返回(指向(函数的指针 返回(无效)))))

我正好是我多年前写的螺旋法则的原作者哦(当时头发还很多:) 很荣幸被加进cfaq

我编写螺旋规则是为了让我的学生和同事更容易阅读 C 声明 "in their head";即,无需使用 cdecl.org 等软件工具。我从来没有打算宣布螺旋规则是解析 C 表达式的规范方法。不过,我很高兴看到该规则多年来帮助了数以千计的 C 编程学生和从业者!

郑重声明,

在很多网站上,包括 Linus Torvalds(我非常尊重的人),"correctly" 已经多次指出,在某些情况下我的螺旋规则 "breaks down"。最常见的是:

char *ar[10][10];

正如该线程中其他人所指出的,规则可以更新为当您遇到数组时,只需消耗所有索引 就好像 写成这样:

char *(ar[10][10]);

现在,按照螺旋法则,我会得到:

"ar is a 10x10 two-dimensional array of pointers to char"

希望螺旋法则在C语言学习中继续发挥作用!

P.S.:

我喜欢 "C isn't hard" 图片 :)