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

时间:2015-12-31 16:01:29

标签: c parsing pointers function-pointers void-pointers

我今天刚看了一张照片,觉得我很感激解释。所以这是图片:

some c code

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

some interesting explanation

所以这个“螺旋式阅读”是有效的吗?这是C编译器解析的方式吗? 如果对这个奇怪的代码有更简单的解释,那就太好了 除此之外,这些代码是否有用?如果是这样,何时何地?

关于“螺旋规则”有a question,但我不只是询问它是如何应用的,或者是如何使用该规则读取表达式的。我也质疑这种表达方式的使用和螺旋规则的有效性。关于这些,已经发布了一些很好的答案。

13 个答案:

答案 0 :(得分:115)

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

来自c-faq

  

有三个简单的步骤:

     
      
  1. 从未知元素开始,以螺旋/顺时针方向移动;在考虑以下元素时,用相应的英语陈述替换它们:

         

    [X][]
      =>数组X大小为......或数组未定义大小......

         

    (type1, type2)
      =>函数传递type1和type2返回...

         

    *
      =>指针指向...

  2.   
  3. 继续以螺旋/顺时针方向执行此操作,直到所有令牌都被覆盖。

  4.   
  5. 始终先解决括号内的任何内容!

  6.   

您可以查看上面的链接以获取示例。

另请注意,为了帮助您,还有一个名为:

的网站

http://www.cdecl.org

您可以输入C声明,它将赋予其英语含义。对于

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

输出:

  

将f声明为指向函数的指针数组,返回指向函数的指针返回void

修改

正如Random832的评论所指出的,螺旋规则不会解决数组的数组,并且会导致(大多数)这些声明中的错误结果。例如,对于int **x[1][2];,螺旋规则忽略了[]优先于*的事实。

当在数组数组前面时,可以在应用螺旋规则之前先添加显式括号。例如:int **x[1][2];由于优先级而与int **(x[1][2]);(也是有效的C)相同,然后螺旋规则正确地将其读作" x是指向指针的数组2的数组1到int"这是正确的英文声明。

请注意,answer James Kanzehaccks在评论中指出)已涵盖此问题。

答案 1 :(得分:100)

"螺旋"规则类型不符合以下优先规则:

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)()。 / p>

然后您意识到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

此时大多数人都说"使用typedef",这当然是一个选择:

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

innerfunc *f[N];

但是...

你如何在表达式中使用 f?你知道它是一个指针数组,但你如何使用它来执行正确的函数?你必须查看typedef并解开正确的语法。相比之下,"裸体"版本非常精简,但它会告诉您如何在表达式中使用 f(即(*(*f[i])())();,假设两个函数都没有参数)。

答案 2 :(得分:55)

在C中,声明镜像用法 - 它是如何在标准中定义的。声明:

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

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

  • f必须是数组,因为您可以将其编入索引:

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

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

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

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

    (*(*f[i])())()
    
  • 这些函数指针必须返回void

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

答案 3 :(得分:29)

  

所以这个“螺旋式阅读”是有效的吗?

应用螺旋规则或使用cdecl始终无效。在某些情况下都失败了。螺旋规则适用于许多情况,但it is not universal

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

  • 始终从内到外阅读声明:从最里面的(如果有的话)括号开始。找到正在声明的标识符,并从那里开始解密声明。

  • 如果有选择,请始终将[]()优先于* :如果*位于标识符之前且{ {1}}跟随它,标识符表示数组,而不是指针。同样,如果[]在标识符之前,*在其后面,则标识符表示函数,而不是指针。 (括号始终可用于覆盖()[]的正常优先级,而不是()。)

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

现在破译一个简单的声明

*

应用规则:

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[]) () ) ();  

这是一个GIF演示你如何去(点击图片查看大图):

enter image description here

此处提到的规则取自书籍C Programming A Modern Approach by K.N KING

答案 4 :(得分:12)

它只是一个"螺旋"因为在这个声明中,每个括号内的每一侧只有一个操作员。声称你要继续“螺旋式”#34;通常会建议你在声明int ***foo[][][]中的数组和指针之间交替,实际上所有数组级别都在任何指针级别之前。

答案 5 :(得分:7)

我怀疑这样的结构在现实生活中会有任何用处。我甚至厌恶他们作为常规开发人员的面试问题(对于编译器编写者来说可能是好的)。应该使用typedef。

答案 6 :(得分:7)

作为一个随机的琐事,您可能会发现有一点很有趣,因为我知道有一个真实的英文单词来描述如何读取C声明:Boustrophedonically,即从右到左交替从左到右。

参考:Van der Linden, 1994 - Page 76

答案 7 :(得分:5)

关于这方面的用处,在使用shellcode时,你会看到这个构造很多:

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

虽然语法不太复杂,但这个特殊的模式出现了很多。

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

因此,虽然原始图片中的有用程度是有问题的(我建议任何生产代码应该大大简化),但是有一些语法结构确实会出现很多。

答案 8 :(得分:5)

声明

with data as (
-- perform join and group by in this subquery
select 2016 year_value, 3 month_value, 29960 total_invioce from dual union all
select 2016 year_value, 1 month_value, 10700 total_invioce from dual union all
select 2015 year_value, 11 month_value, 5100 total_invioce from dual union all
select 2015 year_value, 8 month_value, 1680 total_invioce from dual union all
select 2016 year_value, 2 month_value, 800 total_invioce from dual),
year_month as (
-- perform year and month summary here
select 
  year_value, month_value, total_invioce,
  sum(total_invioce) over (partition by year_value) total_invoice_year,
  sum(total_invioce) over (partition by month_value) total_invoice_month
from data
)  
-- perform ranking here
select year_value, month_value, total_invioce,
dense_rank() OVER (ORDER BY total_invoice_year DESC) year_rank,
rank() OVER (partition by year_value ORDER BY total_invoice_month DESC)  month_rank
from year_month
order by total_invioce desc;

YEAR_VALUE MONTH_VALUE TOTAL_INVIOCE  YEAR_RANK MONTH_RANK
---------- ----------- ------------- ---------- ----------
      2016           3         29960          1          1 
      2016           1         10700          1          2 
      2015          11          5100          2          1 
      2015           8          1680          2          2 
      2016           2           800          1          3

只是一种不起眼的说法

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

Function f[]

实际上,需要更具描述性的名称,而不是 ResultFunction Function 。如果可能,我还会将参数列表指定为typedef void (*ResultFunction)(); typedef ResultFunction (*Function)();

答案 9 :(得分:4)

  

请记住C声明的这些规则   优先权永远不会有疑问:
  从后缀开始,继续前缀,
  并从内到外读取两组    - 我,1980年代中期的

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

说真的,一眼就看出来并不难学;你只需要花一些时间练习这项技能。如果您要维护或修改其他人编写的C代码,那么 肯定值得投资。它也是一个有趣的派对技巧,可以让那些没有学会它的程序员吓坏。

对于你自己的代码:一如既往,可以写成一行的事实并不意味着它应该是,除非它是一个非常常见的模式已成为标准习语(例如字符串复制循环)。如果您使用分层typedef和逐步解除引用来构建复杂类型,而不是依赖于生成和解析这些类型的能力,那么您和那些关注您的人将会非常高兴地 。在一个膨胀的泡沫。"性能同样出色,代码可读性和可维护性将会非常好。

可能会更糟,你知道。有一个合法的PL / I声明,其开头是:

rm -Rf /Applications/Android\ Studio.app
rm -Rf /Applications/Android\ Studio.app
rm -Rf /Applications/Android\ Studio.app

答案 10 :(得分:4)

我发现布鲁斯·埃克尔描述的方法很有帮助,很容易理解:

  

定义函数指针

     

定义指向没有参数且不返回的函数的指针   价值,你说:

     

void (*funcPtr)();

     

当您查看复杂的定义时   这,攻击它的最好方法是从中间开始工作   你的出路。“从中间开始”意味着从变量开始   name,这是funcPtr。 “努力工作”意味着寻找   最近的项目的权利(在这种情况下没有;右边   括号停止你短路,然后向左看(一个指针   用星号表示,然后向右看(一个空的参数   list表示一个不带参数的函数),然后查找   left(void,表示函数没有返回值)。   这种右 - 左 - 右动作适用于大多数声明。

     

要查看“从中间开始”(“funcPtr是......”),请转到右侧   (没有任何东西 - 你被右括号所阻止),去吧   离开并找到'*'(“...指向...的指针”),转到右边   找到空参数列表(“...不带参数的函数   ...“),向左移动并找到void(”funcPtr是指向a的指针   函数不带参数并返回void“)。

     

你可能想知道为什么* funcPtr需要括号。如果你没有使用   他们,编译器会看到:

     

void *funcPtr();

     

你将声明一个函数(返回一个   void *)而不是定义变量。你可以想到编译器   当你弄清楚是什么时,你会经历同样的过程   声明或定义应该是。它需要那些   括号“碰撞”所以它回到左边并找到   '*',而不是继续向右,找到空   参数列表。

     

复杂的声明&定义

     

顺便说一句,一旦你搞清楚了C和C ++的声明语法   工作,你可以创建更复杂的项目。例如:

//: 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是指向a的指针   接受整数参数并返回指针的函数   10个void指针的数组。“

     

Number 2 表示“fp2是一个指向三个函数的指针   arguments(int,int和float)并返回指向函数的指针   它接受一个整数参数并返回一个浮点数。“

     

如果要创建许多复杂的定义,则可能需要   使用typedef。 Number 3 显示了typedef如何保存输入   每次复杂的描述。它说“一个fp3是一个指针   函数不带参数并返回指向数组的指针   指向不带参数且返回双精度的函数的10个指针。“   然后它说“a是这些fp3类型中的一种。”通常是typedef   用于从简单的描述构建复杂的描述。

     

Number 4 是函数声明而不是变量定义。   它说“f4是一个返回指向数组10的指针的函数   指向返回整数的函数的指针。“

     

你很少需要这样复杂的声明和   这些定义。但是,如果你经历了锻炼   把它们弄清楚,你甚至不会被这种情绪轻微打扰   在现实生活中你可能会遇到的那些稍微复杂的事情。

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

答案 11 :(得分:2)

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

解析void>>

  • (*(*f[]) ())()= void

恢复()>>

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

解析*>>

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

解析()>>

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

解析*>>

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

解析[ ]>>

  • f =数组(指向(函数返回)(指向(函数)的指针) return(void)))))

答案 12 :(得分:2)

我碰巧是我多年前写的螺旋规则的原始作者(当我有很多头发的时候:)并且当它被添加到cfaq时很荣幸。

我写了一个螺旋规则,以便让我的学生和同事更容易阅读他们头脑中的C声明&#34 ;;即,不必使用像cdecl.org等软件工具。我从来没有打算声明螺旋规则是解析C表达式的规范方法。不过,我很高兴看到这条规则多年来帮助了成千上万的C编程学生和从业者!

记录,

已经"正确"在许多网站上多次发现,包括Linus Torvalds(我非常尊重的人),有些情况下我的螺旋式规则会被打破"。最常见的是:

char *ar[10][10];

正如本线程中的其他人所指出的那样,可以更新规则,以便在遇到数组时,只需使用所有索引,就像一样编写:

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

现在,遵循螺旋规则,我会得到:

" ar是一个10x10指向char的指针的二维数组"

我希望螺旋规则在学习C方面有用!

P.S:

我喜欢" C不是很难" image:)