复杂的C语言声明

70

我刚刚在互联网上查看一些代码,发现了这个:

float * (*(*foo())[SIZE][SIZE])()

我该如何阅读这个声明?是否有特定的规则来阅读这样复杂的声明?


22
螺旋法则(spiral rule),也称为“安德逊规则”,是一种用于解析C语言声明的方式。它指导我们从变量名称开始,按照顺序阅读声明,并确定每个标识符的类型和作用。可以通过对声明进行拆分并遵循此规则来简化复杂的声明。 - Luchian Grigore
16
http://cdecl.org/ - juanchopanza
24
你需要找到编写此代码的程序员,并让他告诉你代码的含义。然后你要告诉你的老板解雇他,并坚持说你绝对不会再接手他编写的代码。 - Pete Becker
@Andalur float * (*(*foo())[SIZE][SIZE])() 上的 cdecl.org,其中 SIZE 是 16。 - Alvin Wong
@LuchianGrigore 因为螺旋规则是错误的。它无法解析int a[5][6],因为不遵循优先顺序。 - kotlomoy
显示剩余4条评论
8个回答

119

我有一段时间没有做这个了!

foo开始,向右走。

float * (*(*foo())[SIZE][SIZE])()

foo是一个没有参数的函数...

由于有一个闭合括号,所以无法向右走。向左走:

float * (*(* foo())[SIZE][SIZE])()

foo是一个没有参数的函数,返回一个指针

无法再向左走,所以让我们穿过括号,再次向右走

float * (*(* foo())[SIZE][SIZE])() float * (*(* foo())[SIZE][SIZE])() float * (*(* foo())[SIZE][SIZE])()

foo是一个没有参数的函数,返回一个指向大小为SIZE的数组的SIZE个数组的指针...

Closing parenthesis reached, left again to reach a pointer symbol: float * (*(* foo())[SIZE][SIZE])() foo是一个不带参数的函数,返回值为指向SIZE个指向SIZE个指向...的指针的数组的指针。
再次遇到左括号,所以我们跨过它并向右移动: float *( *(* foo())[SIZE][SIZE])()float *( *(* foo())[SIZE][SIZE])() foo是一个不带参数的函数,返回值为指向SIZE个指向SIZE个没有参数的函数的指针的数组的指针。
最后向左移动到结尾处: float * ( *(* foo())[SIZE][SIZE])() foo是一个不带参数的函数,返回值为指向SIZE个指向SIZE个不带参数且返回指向float的指针的函数的指针的数组的指针。

无论是谁写的,请教他如何使用typedef

// Function that returns a pointer to float
typedef float* PFloatFunc ();

// Array of pointers to PFloatFunc functions
typedef PFloatFunc* PFloatFuncArray2D[SIZE][SIZE];

// Function that returns a pointer to a PFloatFuncArray2D
PFloatFuncArray2D* foo();

58
"+1"代表赞同,意为“支持”、“赞成”。“谁写的,请教他使用typedef”,是对代码中某处缺少了类型定义的建议。 - Dan Is Fiddling By Firelight
3
请注意,“无参数”部分仅适用于C ++;对于C语言,它意味着“未指定的参数列表”(但它不能是可变参数函数,因为即使在C中,这些函数也必须在作用域中具有完整的原型)。 - Jonathan Leffler

100

标准规则:找到最左边的标识符,然后向外扩展,记住[]()*之前绑定:

            foo                      -- foo
            foo()                    -- is a function
           *foo()                    -- returning a pointer
          (*foo())[SIZE]             -- to a SIZE-element array
          (*foo())[SIZE][SIZE]       -- of SIZE-element arrays
         *(*foo())[SIZE][SIZE]       -- of pointers
        (*(*foo())[SIZE][SIZE])()    -- to functions
      * (*(*foo())[SIZE][SIZE])()    -- returning pointers
float * (*(*foo())[SIZE][SIZE])();   -- to float

假设你有一堆返回指向float的指针的函数:

float *quux();
float *bar();
float *bletch();
float *blurga();

假设您想将它们存储在2x2表中:

float *(*tab[SIZE][SIZE])() = {quux, bar, bletch, blurga};

tab是一个大小为SIZE x SIZE的指针数组,其中每个指针指向一个返回指向float类型指针的函数。

现在让我们决定要编写一个函数来返回该表的指针:

float *(*(*foo())[SIZE][SIZE])()
{
  static float *(*tab[SIZE][SIZE])() = {quux, bar, bletch, blurga};
  return &tab;
}

请注意,您可以有多个函数构建不同功能的表格,或以不同方式组织相同的函数:
float *(*(*qwerbl())[SIZE][SIZE])()
{
  static float *(*tab[SIZE][SIZE])() = {blurga, bletch, bar, quux};
  return tab;
}

我认为这是唯一的原因,才会有人这样做。在野外很少见到这种类型(虽然偶尔也会出现,而且我自己也写过类似的东西),所以你不应该看到它们。


1
qwerbl?你差点用尽了通用变量名,是吗 :-) 为理由加1。我相信“深度相关”的类型经常出现,但通常也涉及结构或类,这使得命名问题自然而然地消失了 - 当引入一些typedef时,它也会在这里发挥作用。 - Kos
@Kos:是的。我还没有摄入足够的咖啡因,所以想不出更好的东西来。 - John Bode
1
维基百科有一个元语法变量列表,这样你就不会用完:foo,bar,baz,qux,quux,corge,grault,garply,waldo,fred,plugh,xyzzy,thud。 - Waleed Khan
1
@WaleedKhan 深入研究参考资料,你最终会进入《术语档案》。http://www.catb.org/jargon/html/M/metasyntactic-variable.html - Kos

6

根据cdecl.org的说明:

声明foo为一个函数,返回大小为SIZE的指向数组的指针,该数组的每个元素都是指向返回指向浮点数的指针的函数的指针。

如果您想手动解码,请使用Luchian Grigore提供的螺旋规则。


4
最好的做法是将其转换为一系列的typedef。
typedef float * fnReturningPointerToFloat();
typedef fnReturningPointerToFloat* fnArray[SIZE][SIZE];
fnArray* foo();

2
我花了一分钟以上才完成这个。 - QuentinUK

3
通常,您可以尝试使用cdecl.org,但您需要将SIZE替换为相应的数字。
例如,如果您将SIZE替换为12,则会得到以下结果:
声明foo为返回指向数组12的指针的函数,该数组包含指向返回指向浮点数的指针的函数的数组12。
两个观察点:
1. 我猜这段代码旁边没有注释来解释它的目的(即不是技术上的解释,而是从功能/业务角度解释它实现了什么)。如果程序员需要使用像这样复杂的东西,他们应该足够聪明,能够向未来的维护者解释它的目的。
2. 在C++中,肯定有更明显且可能更安全的方法来实现同样的事情。

3
这是由于"SIZE"的缘故,你需要使用文字而不是变量(在之后自己用常量替换)。 - Clement J.
请用某个数字替换文本中的SIZE!! - Kaunteya

2
虽然上面的大部分答案都足够好,但缺少解码复杂C声明的完整规则集。我提供了下面的完整规则集来解码任何复杂的C声明。这个规则集实际上是基于运算符的优先级。右手螺旋规则等规则可以看作是这些规则的快捷方式。
在任何事情之前,我们需要知道一些东西来解码声明。
声明的“基本类型”
C声明始终只有一个基本声明类型。这是在声明的最左边位置。 例如 -
int a - 基本类型为'int' float *p - 基本类型为'float' char (*p)[3] - 基本类型为'char'
优先级和结合性
接下来,我们需要知道(),[]和* - 取消引用运算符的优先级顺序
(),[] - 结合性从左到右 * - 结合性从右到左
与每个运算符对应的短语
接下来,我们需要知道与每个运算符对应的解码短语。后面的示例将说明这一点。
() - 返回函数 [SIZE] - 大小为SIZE的数组 * - 指向
现在按照以下规则解码声明
1.始终首先写变量名,然后是'is'。
例如 -
int a - a is ... float *p - p is ... char (*p)[3] - p is ...
2.始终以基本类型结束
例如 -
int a - a is ... int float *p - p is ... float char (*p)[3] - p is ... char
  1. 现在,使用以下子步骤填写中间部分:

    • 从名称开始,遵循运算符优先级和结合性选择下一个最高优先级运算符,并将对应短语附加到解码字符串的中间部分。

    • 对于剩余声明重复上述子步骤,直到解码过程完成。

注意1:为简单起见,本文忽略了函数参数,但可以在对应()短语之后包含它们。

注意2:括号(())会改变运算符的优先级顺序,就像在任何算术表达式中一样。

注意3:您可以在已解码的声明中使用括号以增加可读性(我在下面的某些示例中已经这样做)。将每组括号视为单个单位。

注意4: n维数组实际上是一个数组(n-1次)的数组。例如 - int A[2][3] - A是包含3个整数的数组的2元素数组,即A是包含2个元素的数组,每个元素都是包含3个整数的数组

例子

  • int a - a是int类型的变量
  • float *p - p是指向float类型的指针
  • char (*p)[3] - p是指向包含3个字符的数组的指针

一些复杂声明的示例

  1. int **p[10] - p是包含10个指向指针的数组,这些指针指向int类型的变量
  2. int (*p)[10] - p是指向包含10个整数的数组的指针
  3. int *p(char *a) - p是返回指向int类型变量的指针的函数
  4. int (*p(char*a))[10] - p是返回(指向包含10个整数的数组的指针)的函数
  5. int *(*p)() - p是指向(返回指向int类型变量的指针的函数)的指针
  6. int (*p()[20])[10] - p是返回(包含20个(指向包含10个整数的数组的指针)的数组)的函数

此规则集也可以与const一起使用 - const修饰符会修改其左侧的术语(如果存在),否则会修改其右侧的术语。

  1. const int *p[10] - p 是一个包含 10 个指向 const int 的指针的数组
  2. int const *p[10] - p 是一个包含 10 个指向 const int 的指针的数组(与第 7 个例子相同)
  3. int *const p[10] - p 是一个包含 10 个指向 int 的常量指针的数组

现在是一个非常复杂的例子,实际上在实践中不会使用,但仍然可以用来演示解码过程。

  1. char *(*(**foo[][8])())[] - foo 是一个包含 (包含 8 个指向 (指向 (返回指向 (包含指向 char 的指针的数组) 的函数的指针))) 的数组的数组

现在最后一步是对问题中给出的声明进行解码。

float * (*(*foo())[SIZE][SIZE])() - foo 是一个返回 (指向大小为 SIZE 的数组的指针,其中每个元素都是大小为 SIZE 的数组的指针,这些指针又指向一个返回指向 float 的指针的函数) 的函数的指针。

以下是我阅读此解码过程的文章链接

例 10 取自本文

http://www.unixwiz.net/techtips/reading-cdecl.html


2

这篇文章给了我关于如何轻松地阅读任何C语言声明的最好线索:

http://c-faq.com/decl/spiral.anderson.html

有三个简单的步骤需要遵循:

  • 从未知元素开始,以螺旋式/顺时针方向移动;当遇到以下元素时,将其替换为相应的英文语句:

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

    • (type1, type2) => 函数传递类型1和类型2,返回...

    • * => 指向...

  • 一直以这种螺旋式/顺时针的方式进行,直到所有标记都被覆盖。

  • 始终首先解析括号中的内容!

例如:

             +-------+
             | +-+   |
             | ^ |   |
        char *str[10];
         ^   ^   |   |
         |   +---+   |
         +-----------+

Question we ask ourselves: What is str?

``str is an...

- We move in a spiral clockwise direction starting with `str' and the first character we see is a `[' so, that means we have an array, so...
  ``str is an array 10 of...

- Continue in a spiral clockwise direction, and the next thing we encounter is the `*' so, that means we have pointers, so...
  ``str is an array 10 of pointers to...

- Continue in a spiral direction and we see the end of the line (the `;'), so keep going and we get to the type `char', so...
``str is an array 10 of pointers to char''

We have now ``visited'' every token; therefore we are done!

1

来自 http://cdecl.org/

声明 foo 为一个返回指向大小为 SIZE 的指针数组的函数,该数组的每个元素都是指向返回指向浮点数的指针的函数的指针。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接