24fahed

博客园 首页 新随笔 联系 订阅 管理

说明

本文章是我尝试用优先级的思路来尝试解析声明时的结论,目的是将普通运算和声明的解析统一起来。该方法本质上是螺旋法则的野路子实现(非常野,一开始想这个问题时我甚至不知道螺旋法则是啥,且最终发现,使用优先级来解复杂声明的解析顺序实际上是和螺旋法则相同的)。因此,如果您是在搜索一种更正宗的解决方法,请阅读以下文章:

如果你想阅读螺旋法则的更原始的说明,可以查看:https://c-faq.com/decl/spiral.anderson.html

这位大佬的文章直接使用了螺旋法则:https://www.luozhiyun.com/archives/756

【推荐】这位大佬参结合了cdecl工具给出了更加具有实践意义的指导:https://www.cnblogs.com/idorax/p/7714963.html

【推荐】这篇文章给出了更加详细的应用螺旋法则的方法:https://blog.csdn.net/m0_60259116/article/details/153702024

如果你想做更多阅读复杂声明的练习,请前往这个网站:https://cdecl.org/

摘要

分析复杂声明需要解决两个问题:符号的解析顺序,和每个符号要翻译为什么语义。传统解决方案中,前者使用螺旋法则解决,后者不同的软件有不同的解析方法。

本文尝试使用以优先级代替螺旋法则处理符号解析顺序问题,以“解析状态机”处理每个符号要翻译为什么语义的问题,实现解析复杂类型声明。

本文的主要特点是使用“解析状态机”记录上一个符号的解析结果对下一个符号解析的影响,从而确定声明中每个部分的应当解析为何种确切含义;使用已有的运算符优先级表格和部分新增符号的优先级,代替螺旋法则分析声明的中,每个部分的解析顺序,从而统一声明解析和普通运算的解析。

目录

  • 引例
  • 从优先级入手分析复杂声明的方法
    • 优先级的意义
    • 一个声明中都有那些“部分”
    • 如何解析复杂声明
  • 统一声明的解析和运算顺序的解析
  • 后记

引例

《C++primer》中有这样一个例子:

int *ptrs[10]; // ptrs是含有10个整型指针的数组
...
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
...

虽然在P103页上解释了原因,但可能不是很清楚。

从优先级入手分析复杂声明的方法

1. 优先级的意义

优先级决定了一个“部分”被解析的顺序。

2. 一个声明中都有那些“部分”

本文章仅强调如何应用,因此本部分内容是我对声明中包含部分的理解。

部分 示例 注意点
运算符 (),[],sizeof,* 具体参考附表部分。声明中的()在表格中找不到对应用法,请参考附表下面的说明
符号(变量名/函数名) int *ptrs[10]中的ptrs 从变量名开始解析整个声明,整个声明中,变量名/函数名只有一个
类型描述符 int,float等 在解析过程中,优先级最低

这三大部分之间的解析优先级顺序为:

符号 > 运算符 > 类型描述符

处于同一优先级的运算符,在一个声明中,从左向右优先级递减。

3. 如何分析复杂声明

首先说明在分析过程中,使用到的名词:

  • 待解析部分:声明中还未被解析的部分

  • 已解析部分:声明中已经被解析的部分的集合

  • 已解析语义:已解析部分所表达的语义

  • 下一部分:声明中,下一个需要解析的部分

  • 解析状态机:当前正在解析的角色,其中角色有:初始,变量,变量指针,函数,函数指针,符号,符号指针(具体身份未定)等。下一部分将作何解释和解析状态机有关。

解析复杂声明的方式如下:

step1: 选择当前声明中优先级最高的独立部分

step2: 根据当前解析状态机解析这部分内容,得到解析结果

step3: 根据解析结果转移状态机【重要】

step4: 将解析结果对应的语义添加到已解析语义中

step5: 重复上述步骤,直到整个式子结束

例:

下面的例子中,如果出现(...)或者[...],表示暂时将这个括号中的内容和括号本身视为一个整体。

int (*Parray)[10];

初始状态下:

  • 未解析部分:int (*Parray)[10];

  • 已解析语义:未定义

  • 下一部分:未定义

  • 解析状态机:初始

step1:

在int, (...), [10]中,()和[]的优先级同级,按从左向右优先级递减判断,(...)高于[10]。

(...)是一个组合部分,非独立部分,因此继续在(*Parray)中找具有更高优先级的。

*Parray中,符号Parray优先级最高

  • 未解析部分:int (*Parray)[10];

  • 已解析语义:未定义

  • 下一部分:Parray

  • 解析状态机:初始

step2-step3-step4:

解析状态机为初始,Parray是“符号”:

  • 未解析部分:int (*)[10];

  • 已解析语义: 这是符号Parray <-(step4)

  • 下一部分:Parray

  • 解析状态机:符号 <-(step3)

上述为一轮循环,未解析部分中仍存在内容,因此解析继续。

  • step1: (*)中的*是此时优先级最高的,更新“下一部分”为:*。

  • step2: 当前状态机为“符号”,对解析不构成影响,因此直接将*解析为指针。

  • step3: 根据解析结果为指针,解析状态机转移为“符号指针”。

  • step4: 修改已解析语义为:Parray一个符号指针。

继续解析未解析部分的内容(第二轮结束时,解析状态机为符号指针):

第三轮 第四轮
未解析部分 int [10] int
下一部分 [10] int
解析结果 指向10个单位的数组 数组成员的数据类型为int
解析状态机 符号指针->数组 数组->数组成员
已解析语义 符号Parray是一个指向容量为10个单位大小的数组的指针 符号Parray是一个指向容量为10个int类型大小的数组的指针,数组的成员为int类型

同样,int *ptrs[10]可以如此解析:

初始 第一轮 第二轮 第三轮 第四轮
未解析部分 int *ptrs[10] int *ptrs[10] int *[10] int * int
下一部分 未定义 ptrs [10] * int
解析结果 未定义 符号ptrs 十个单位大小的数组 数组成员是指针 数组成员是int类型的指针
解析状态机 初始 初始->符号 符号->数组 数组->数组成员 数组成员
已解析语义 未定义 ptrs是符号 ptrs是容量为10单位大小的数组 prts是容量为10指针大小的数组,数组成员为指针类型 ptrs是容量为10个int类型指针大小的数组,数组成员为int类型

4. 使用优先级解析复杂声明的缺陷

优先级实际上是针对运算过程而设立的。因此在优先级表中有不包含所有声明中可用的对象。针对处理const时,如果将const和*设置为同级别:

int * const param;

param是一个指针,指向一个const的int类型数据,这是错误的。

按照螺旋法则,应当解析为param是一个const类型的指针,指向一个int类型数据。

const int * param;

param是一个指针,指向const int类型数据,这是正确的。

因此,只有将声明的同级优先顺序改为:同优先级情况下,从符号开始,向两边递减,符号两边的优先级右边大于左边时,才能正确解释这个问题。即使用和普通算数运算优先级解析复杂声明有一个根本缺陷,即同级优先级判优的策略(从符号向两边递减)和普通运算的同级判优策略(从左向右)是不同的。

这种不同目前仅发现出现在const中,可以在解析前调整const位置来避免这个问题。但是否实际中有更多类似的问题,目前还未研究。

统一声明的解析和运算顺序的解析

运算可以解析为一颗多叉树(使用递归找子表达式的方法,递归和回溯路径形成的一颗树结构,参考页面)。在需要使用这种分治的思路同时解决声明和表达式的问题中,需要将类型描述符也视为运算符。以int (*param)[10] = a > b ? &array_a : &array_b;为例,因为整个表达式先解析计算优先级高的部分,再计算优先级低的部分,最终得到这个表达式的解析结果,这个过程对应到递归过程中,是回溯过程。综上,表现在多叉树中,优先级越高,对应节点的深度越深,优先级越低,越接近根节点,形成如下图所示的多叉树。

image

这也是通过优先级解析复杂表达式我目前发现的唯一好处。

后记

上述内容你会看到很多不严谨的地方,例如为什么在int (*Parray)[10]的第三轮解析中,直接丢掉了()且它在第二轮中没有被解析(第二轮解析了*),为什么int *ptrs[10]的第四轮解析为数组成员是int类型指针,而不是数组成员是int类型,前者看起来是综合了已解析的语义才得到了正确解析结果。

这是因为如果用程序化的语言来表述这些内容,会让整个过程更加复杂,理解如何应用螺旋法则不等于写一个clang在大脑里运行。例如在解析了*Parray后,()确实已经没有了实际意义语义,它在这个过程中看起来就是给它内部的内容提升一个优先级(这也是我单独将声明中的()作为新的运算符添加到运算符优先级表格中的原因之一),而int functionName(int)在解析到(int)时,它却可以结合当前解析状态机(状态为:符号)得到functionName是一个函数的名称(查找运算符优先级表格可以发现()有作为函数调用的解释),从而得到已解析语义为:functionName是有一个int类型参数的函数。

上述内容最重要的部分实际上是阐述了如何理解声明中的优先级,并根据优先级选择下一个需要解析的部分。同时提供了解析状态机的视角,从而提供了一个途径,来利用上次解析结果来分析当前部分究竟应当解析为什么含义。这两点是在不参考资料,且希望形成一个相对广泛适用性解析方案时,难以发现的关键。在根据优先级来分析复杂声明的应用过程中会发现,这种解析方法分析声明的顺序和螺旋法则的解析顺序实际上是相同的。

对于更加复杂的例子,使用优先级解析声明同样表现良好。例如:

void (signal(int sig, void (func)(int)))(int)

这是linux中signal的声明,这个声明的含义是:signal是一个函数,它的需要一个int类型和一个返回值为void、参数为int的函数指针作为参数。signal函数的返回值是一个指针,这个指针指向一个返回值为void类型且参数为int的一个函数。

这个例子中*signal(...)和 (*func)(int)中的*又在不同的情况下被解释为了不同的含义。前者是指返回值为指针,后者是指函数指针。如果考虑到了解析状态机(上一次解析结果带来的影响),可以发现前者是在“函数”为解析状态机的情况下,将*解析为返回值;而后者是因为(*func)确定了这是一个“指针符号”。在这个例子中仍然可以提问,为什么*signal(...)在解析了signal是个函数后,不能认为他是个函数指针呢,而且状态机从函数变为函数指针看似还很有道理。这个问题以我的专业就无法解答了,我在处理这个问题时规定了一些强制性的顺序(解析状态机只能在处于“符号”阶段向“符号指针”转换),这就体现了解析状态机的作用,解析状态机可以提供这种“按照确定顺序转变”的视角。这种问题在螺旋法则中也同样存在,法则只能分析解析声明中每个部分的顺序,而不能确定语义。

(实际上最重要的是作者是软件工程的学生,并不研究编译原理因此只能止步于此)

附表

运算符优先级

IMG_20251231_083615_edit_393510412254989

IMG_20251231_083706_edit_393454476589021

运算符优先级中并未规定()用于声明中的优先级,这里规定到第二级,即和".","->"一组的位置。

posted on 2025-12-30 20:42  24-Fahed  阅读(24)  评论(0)    收藏  举报