C 语言:莫名其妙的声明

Apr 15, 2025

我最近在看《C 专家编程》。不得不承认这是本好书,探讨了很多我困惑很久,而未能够得到解答的疑问。其中一章详细讲解了 C 语言奇怪的声明语法:声明不能简单地从左往右阅读,必须理解操作符的优先级规则,时而要从右往左,时而要跳跃解读,蹦来蹦去。

为什么设计成这样呢?据说是为了让声明形式和使用形式保持一致,举例来说,你可能有如下的声明:

int (*(*foo)(void))[3];

使用起来是这样:

int (*ptr_to_arr)[3] = (*foo)();

怎么样,是不是突然感觉有点道理了?

书中还介绍了一个实用程序 cdecl,它的作用是将 C 语言声明转写成人类语言的形式,例如刚刚的声明用我的 cdecl 程序解析,输出如下:

foo is pointer to function (void) returning pointer to array 0..2 of int

怎么样,有意思吧?这里还有个在线版本:cdecl.org

不过说来话长,这个小程序还是挺不好写的,按照书上的思路,你要先把标识符(你可以理解成变量名)之前的所有符号压入一个堆栈中,然后开始处理标识符后面的符号,如 []()

不过鉴于括号有多义性(一方面可以改变结合的优先级,另一方面代表此处是一个函数)、函数参数可以套娃指针参数等等问题,对我来说实际写起来难度不小。

书上的参考实现里使用到了标准库中的 ungetc 函数,这个函数出现在 get_token 函数里。

void get_token(void) {
  char *p = this.string;
  /* 略过空白字符 */
  while ((*p = getchar()) == ' ')
    ;
  /* 读入的标识符以A-Z,0-9开头 */
  if (isalnum(*p)) {
    while (isalnum(*++p = getchar()))
      ;
    ungetc(*p, stdin);
    /* 前面的循环会误食到不应该读的、非纯数字字母字符,
     * 这里用 ungetc 吐回去 */
    *p = '\0';
    this.type = classify_string();
    return;
  }

  /* 标识符为指针 */
  if (*p == '*') {
    strcpy(this.string, "pointer to");
    this.type = '*';
    return;
  }

  /* 标识符为括号或逗号 */
  this.string[1] = '\0';
  this.type = *p;
  return;
}

简单来说,ungetc 函数可以把字符放回标准流中,这样就不需要再弄一个变量记录前一个读取的字符,还是挺有用的。

我照抄了网上的实现,不过加上了解析函数参数列表的功能,虽然还有 Bug,我也懒得再改了。以后的事情,以后再说吧(