分析C语言的声明

原文1链接:http://www.javashuo.com/article/p-mhqyzuvs-od.html

原文2链接:https://www.cnblogs.com/monster-prince/p/6215769.html

文章1

让我们先来看一些C语言的术语以及一些能组合成一个声明的单独语法成分。其中一个非常重要的成分就是声明器(declarator)——它是所有声明的核心。简单地说,声明器就是标识符以及与它组合在一起的任何指针、函数括号、数组下标等,如下表所示。为了方便起见,我们把初始化内容(initializer)也放到里面,并分类表示。

声明器

注:上表中* const volatile、* volatile、* const、* volatile const,这里的const和volatile是用于限定指针的。

一个声明由下表所示的各个部分组成(并非所有的组合形式都是合法的,但这个表描述了我们进一步讨论所要用到的词汇)。声明确定了变量的基本类型以及初始化值(如果有的话)。

声明说明符

注:这里的类型限定符const和volatile是用于限定类型说明符指定的类型,与上面* const volatie等是不一样的。

注:上表中倒数第二行“零个或多个声明器”的意思是,举例来说就是一次声明多个:static const int i,j,k,l,m.n;,这里的j、k、l、m、n就属于倒数第二行中提到的。而这里的i就是倒数第三行中提到的。

注:const static int * const p(); 

const、static、int 属于声明说明符;* const、p()属于声明器

p是一个函数,它返回一个指针,这个指针是只读的,这个指针指向一个int类型的对象,并且该对象也是只读的,最后这个函数只能在声明所在的文件内可见。

声明说明符:声明说明符是以嵌套形式组织的,以上为例有三个声明说明符const、static、int。

const是一个类型说明符,它后面的声明说明符是static int;

static是一个存储说明符,它后面的声明说明符是int;

int是一个类型说明符,它后就就是声明器,已经没有嵌套了

对于声明说明符的排列顺序,C标准并没有规定,谁嵌套谁都可以,所以static const int、int static const 、int const static 都是一个意思。

让我们看一下如果你使用这些部件来构造一个声明,情况能够复杂到什么程度。同时要记住,在合法的声明中存在限制条件。你不可以像下面那样做:

 

  • 函数的返回值不能是一个函数,所以像foo()()这样是非法的
  • 函数的返回值不能是一个数组,所以像foo()[]这样是非法的
  • 数组里面不能有函数,所以像foo[]()这样是非法的

 

但像下面这样则是合法的:

 

  • 函数的返回值允许是一个函数指针,如: int (*fun())()
  • 函数的返回值允许是一个指向数组的指针,如:int (* foo())[]
  • 数组里面允许有函数指针,如:int (* foo[])()
  • 数组里面允许有其它数组,如:int foo[][]

 

扩展

C标准中的“右左法则”是用来辨识一个声明的方法,其具体流程如下:

首先从未定义的标识符开始,然后往右看,再往左看。每当遇到圆括号时,就应该调转阅读方向。一旦解析完圆括号里面所有东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。

例1:int (*func)(int *p);

首先找到未定义标识符func,它的外面有一对圆括号,而且它的左边有一个*,所以func是一个指针;

跳出这个圆括号,看右边,也是一个圆括号,说明(*func)是一个函数,而func是一个指向函数的指针,这类函数有一个int *类型的形参;

跳出这个圆括号,看左边,是一个int,说明这类函数返回的是一个int类型的值

例2:int (*func)(int *p, int (*f)(int *));

未定义标识符func,它外面有一对圆括号,而且它的左边有一个*,所以func是一个指针;

跳出这个圆括号,看右边,也是一个圆括号,说明(*func)是一个函数,而func是一个指向函数的指针,这类函数具有int * 和 int (*)(int *)这样的形参;

跳出这个圆括号,看左边,是一个Int,说明这类函数返回的是一个int类型的值。

例3:int (*func[5])(int *p);

未定义标识符func,看右边,有一对方括号,说明它是一个具有5个元素的数组;

看左边,是一个*,说明数组的元素是指针;

跳出这个圆括号,看右边,也是一对圆括号,说明数组中的元素是函数指针,这类函数具有int *这样的形参;

跳出这个圆括号,看左边,是一个int,说明这类函数返回的是一个int类型的值

例4:int (*(*func)[5])(int *p);

未定义标识符func,看左边,是一个*,说明它是一个指针;

跳出这个圆括号,看右边,是一对方括号,说明(*func)是一个数组,一个指向数组的指针;

看左边,是一个*,数组的元素是指针;

跳出这个括号,看右边,是一对圆括号,说明数组中的元素时函数指针,这类函数具有int * 这样的形参;

跳出这个括号,看左边,是一个int,说明这类函数返回的是一个Int类型的值

例5:int (*(*func)(int *p))[5];

未定义标识符func,看左边,是一个*,说明它是一个指针;

跳出这个圆括号,看右边,是一对圆括号,说明func是一个指向函数的指针,这类函数具有int* 这样的参数;

看左边,是一个*,说明这类函数返回的是一个指针;

跳出这个圆括号,看右边,是一对方括号,说明这个返回的指针指向一个具有5个元素的数组;

看左边,是一个int,说明数组的元素是Int类型

[非法] 例6:int func(void)[5]

未定义标识符func,看右边,是一对括号,func是一个函数,没有参数;

由于右边是一对方括号,所以函数返回的是一个具有5个元素的数组;

这个数组的元素是int类型

文章2

C语言的声明晦涩难懂这一点应该是名不虚传的,比如说下面这个声明:

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

这可不是吓人的,熟悉C语言的人会发现,这原来就是ANSI C标准中的信号的信号处理函数的函数原型,如果你没有听说过,那么你确实应该好好补补你的C语言了。那么这个函数原型是什么意思呢?后面会说明,在这里提出就是证明在C语言中,的确存在这种晦涩难懂的声明。

  为什么在C语言中会存在这种晦涩难懂的声明呢?这里有几个原因。首先,在设计C语言的时候,由于人们对于“类型模型”尚属陌生,而且C语言进化而来的BCPL语言也是无类型语言,所以C语言先天有缺。然后出现了一种C语言设计哲学——要求对象的声明形式与它的使用形式尽可能相似,这种做法的好处是各种不同操作符的优先级在“声明”和“使用”时是一样的。比如说:

  声明一个int型变量时:int n;

  使用这个int型变量时:n

可以看出声明形式和使用形式非常相似。不过它也有缺点,它的缺点在于操作符的优先级是C语言中另外一个设计不当的地方。也就是说,C语言之前存在的操作符优先级的问题在这里又继续影响它的声明和定义,这就导致程序员需要记住特殊规则才能推测出一些稍微复杂的声明,当然之前也说过,C语言并不是为程序员设计的,它只是为编译器设计的。在C++中,这一点倒是有所改善,比如说int &p;就是声明p是一个只想整形地址的数也就是指针。C语言的声明存在的最大问题是你无法以一种人们所习惯的自然方式从左向右阅读一个声明,在ANSI C引入volatile和const关键字之后,情况就更糟糕了。由于这些关键字只能出现在声明中,这就使得声明形式与使用形式完全对得上号的越来越少了。我相信有很多学习C语言的人都搞不太清楚const与指针之间的声明关系,请看下面的例子:

  const int * grape;

  int const * grape;

  int * const grape;

  const int * const grape;

  int const * const grape;

怎么样?如果你能正确的分析它们的含义,那么说明你的C语言学得不错,如果你已经晕了,那也不怪你,毕竟这种情况只会在C语言里出现。不过,还是让我们来解决这几个例题,首先我们要明白const关键字,它的名字经常误导人们,导致让人觉得它就是个常量,在这里有个更合适的词适合它,我们把它叫做”只读“,它是个变量,不过你只有读取它的权限,不能对它进行任何修改。我是这么分析这种const声明的:只要const出现在"*"这个符号之前,可能是int const *,也可能是const int *,总之,它出现在”*"之前,那么就说明它指向的对象是只读的。如果它在”*"这个符号之后,也就是说它靠近变量名,那么就说明这个指针是只读的。换句话也可以这么说,如果它出现在"*"之前,说明它修饰的是标识符int或者其他类型名,那么说明这个int的值是只读的,说明它指向的对象是常量;如果它出现在“*"之后,说明它修饰的是变量名grape,那么说明这个指针本身是只读的,说明这个指针为常量。这样再来看上面两个例题就很简单了,第一个和第二个的const均出现在"*"符号之前,而"*"之后没有const变量,那么说明这两个都是常量指针,也就是说指向的int值是只读的;第三个const则出现在"*"之后,而”*"之前没有,说明第三个是一个指针常量,这个指针是只读的;第四个和第五个const出现在“*"之前和之后,就说明它既是指针常量也是常量指针,指针本身和指针所指向的int值都是只读的。

  看到这里,相信大家已经对C语言这种晦涩的声明语法有所体会了,这样看来,正常人都不是很喜欢这种晦涩的语法,可能只有编译器才会喜欢了吧!

  下面我们来看看声明是如何形成的:

  首先要了解的东西叫做声明器——是所有声明的核心。声明器是标识符以及与它组合在一起的任何指针、函数括号、数组下标等。下面我列出一个声明器的组成部分,首先它可以有零个或多个指针,这些指针是否有const或是volatile关键字都没有关系,其次,一个声明器里有且只有一个直接声明器,这个直接声明器可以是只有一个标识符,或者是标识符[下标],或者是标识符(参数),或者是(声明器)。书中给出的表格可能有些困难,所以把它总结下来就是这么一个公式:

  声明器 = 直接声明器( 标识符 or 标识符[下标] or 标识符(参数) or (声明器) ) + (零个或多个指针)

这个式子已经相当简洁了,不过早些时候提到过,()操作符在C语言中代表的意思太多了,在这里就体现了出来,它既表示函数,又表示声明器,还表示括号优先级。为了让大家更好的理解,我来举出一些例子给予说明:

  有一个直接声明器,并且这个声明器为标识符:n

  有一个直接声明器为标识符,还有一个指针:  * n

  有一个直接声明器为标识符[下标],还有一个指针: * n[10]

  有一个直接声明器为标识符(参数):  n(int x)

这些声明器看上去跟我们平时的声明很相似,但是好像又不完整,别着急,因为声明器只是声明的一个部分,下面我们来看一条声明的组成部分:C语言中的声明至少由一个类型说明符和一个声明器以及零个或多个其他声明器和一个分号组成。下面我们一一来介绍这每个部分:

  首先类型说明符有这些:void、char、short、int、long、signed、unsigned、float、double以及结构说明符、枚举说明符、联合说明符。然后我们知道C语言的变量存储类型有auto、static、register,链接类型有extern、static,还有类型限定符const、volatile,这些都是C语言常见的关键字和各种类型,那么一个声明中至少要有一个类型说明符,这个很好理解,因为这个类型说明符告诉计算机我们要存储的数据类型。

  声明器的部分见上面,我已经把它说得比较清楚了。

  关于其他声明器,我举出一个例子大家就明白了:

  char i, j;

  看到它同时声明了两个变量,其中j就是其他声明器,这表示同一条声明语句中可以同时声明多个变量。

  最后一个分号,是C语言中一条语句结束的标志。

  至此,C语言的声明就已经很清楚了,不过要注意在声明的时候还是有一些其他规则,比如说函数的返回值不能是一个函数或数组,数组里面也不能含有函数。

  了解了C语言声明的详细内容之后,我们再来看看如何分析C语言的声明

  接下来我们要做的事情就是,用通俗的语言把声明分解开来,分别解释各个组成部分。

  在这里提一句,关于分析C语言的声明部分,在《C与指针》的第13章——高级指针话题中也有详细的描述,会一步一步从简单的声明到复杂的声明,再介绍一些高级指针的用法。而在本书中,我们将着重建立一个模型来分析所有的声明。

  先来理解C语言声明的优先级规则:

  A  声明从它的名字开始读取,然后按照优先级顺序依次读取

  B  优先级从高到低依次是:

      1. 声明中被括号括起来的部分

      2. 后缀操作符:括号()表示这是一个函数;[]则表示这是一个数组

      3. 前缀操作符:星号* 表示"指向...的指针“

  C  如果const或volatile关键字存在,那么按我在前面所说的办法判断它们修饰标识符还是修饰类型

  下面,还是给出一个例子来帮助理解:

  char * const *(*next) ();

  A  首先,变量名是next

  B  next被一个括号括住,而括号的优先级最高,所以”next是一个指向...的指针

      然后考虑括号外面的后缀操作符为(),所以”next是一个函数指针,指向一个返回值为...的函数“

      然后考虑前缀操作符,从而得出”next是一个函数指针,指向一个返回值为...的指针的函数“

  C  最后,char * const是一个指向字符的常量指针

  所以,我们可以得出结论:next是一个函数指针,该函数返回一个指针,这个指针指向一个类型为char的常量指针。

当然,如果不想自己分析这些复杂的声明,你还有一个好的选择,就是用一个工具来帮助你分析;或者你想知道自己的分析对不对,也可以用到这个工具——cdecl,它是一个C语言声明的分析器,可以解释一个现存的C语言声明。下面我简述它的安装和使用过程,同样是在Linux上:

  首先,安装命令

sudo apt install cdecl

  然后直接输入应用程序名进入程序

cdecl

  然后直接输入,来检测一下我们刚刚分析的例子

cdecl> explain char * const *(*next) ();
declare next as pointer to function returning pointer to const pointer to char

  噢,看起来很不错嘛,我们分析得对,这个程序也解释得很棒,怎么样,是不是对这个程序感到好奇,下面我们来尝试自己实现这个程序

  首先我们想办法用一个图来表示分析声明的整个过程,上面给出的步骤很有用,但是还是不够直观,在书中作者给出了一个解码环的图来描述这个步骤,下面我把这个图大致的描述出来,有的地方可能加上我自己的理解和修改:

   

  要注意的事项我已经把它们都标志出来了,现在让我们用这个流程图来分析一个实例:

  char * const *(*next) ();

  分析过程中另外有一点需要特别注意,那就是需要逐渐把已经处理过的片段“去掉”,这样便能知道还需要分析多少内容。

  

  上面的表格就是这个表达式根据前面给出的流程图分析声明的全部过程,从表格中第一列可以看出这个表达式被处理过的部分在一步一步的去掉,这个程序的处理过程现在已经讲得非常清楚了接下来,给出实现这个过程的具体代码,为了简单起见,暂且忽略错误处理部分,以及在处理结构、枚举、联合时只简单的用“struct”,“enum“和”union“来代表,假定函数的括号内没有参数列表,否则事情就变得复杂多了!这个程序可能要用到一些数据结构,比如说堆栈,像这种需要一个一个按序列读取的程序总是免不了要用到堆栈的,在表达式求值等其他应用也经常见到。

  一个简单的代码实现如下所示:

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MAXTOKENS 100
#define MAXTOKENLEN  64

enum type_tag{IDENTIFIER, QUALIFIER, TYPE};

struct token{
    char type;
    char string[MAXTOKENLEN];
};

int top = -1;
struct token stack[MAXTOKENS];
struct token thisToken;

#define pop stack[top--]
#define push(s)  stack[++top] = s

enum type_tag classify_string(void)
{
    char  *s = thisToken.string;
    if(!strcmp(s, "const"))
    {
        strcpy(s, "read-only");
        return QUALIFIER;
    }
    if(!strcmp(s, "volatile")) return QUALIFIER;
    if(!strcmp(s, "void")) return TYPE;
    if(!strcmp(s, "char")) return TYPE;
    if(!strcmp(s, "signed")) return TYPE;
    if(!strcmp(s, "unsigned")) return TYPE;
    if(!strcmp(s, "short")) return TYPE;
    if(!strcmp(s, "int")) return TYPE;
    if(!strcmp(s, "long")) return TYPE;
    if(!strcmp(s, "float")) return TYPE;
    if(!strcmp(s, "double")) return TYPE;
    if(!strcmp(s, "struct")) return TYPE;
    if(!strcmp(s, "union")) return TYPE;
    if(!strcmp(s, "enum")) return TYPE;

    return IDENTIFIER;
}

void gettoken(void)
{
    char *p = thisToken.string;

    while((*p = getchar()) == ' '); //略过空白字符

    if(isalnum(*p)) //读入标识符的首字符介于A-Z,0-9
    {
        while(isalnum(*++p = getchar()));
        ungetc(*p, stdin);
        *p = '\0';
        thisToken.type = classify_string();
        return;
    }

    if(*p == '*')
    {
        strcpy(thisToken.string, "pointer to");
        thisToken.type = '*';
        return;
    }
    thisToken.string[1] = '\0';
    thisToken.type = *p;
    return;
}

void read_to_first_identifer() //理解分析过程中的所有代码段
{
    gettoken();
    while(thisToken.type != IDENTIFIER)
    {
        push(thisToken);
        gettoken();
    }
    printf("%s is ", thisToken.string);
    gettoken();
}

void deal_with_arrays()
{
    while(thisToken.type == '[')
    {
        printf("array ");
        gettoken(); //数字或']'
        if(isdigit(thisToken.string[0]))
        {
            printf("0..%d ", atoi(thisToken.string)-1);
            gettoken(); //读取']'
        }
        gettoken(); //读取']'之后的再一个标记
        printf("of ");
    }
}

void deal_with_function_args()
{
    while(thisToken.type != ')')
    {
        gettoken();
    }
    gettoken();
    printf("function returning ");
}

void deal_with_pointers()
{
    while(stack[top].type == '*')
    {
        printf("%s ", pop.string);
    }
}

void deal_with_declarator()
{
    switch(thisToken.type) //处理标识符之后可能存在的数组/函数
    {
        case '[' : deal_with_arrays(); break;
        case '(' : deal_with_function_args(); break;
    }

    deal_with_pointers();

    while(top >= 0)
    {
        if(stack[top].type == '(')
        {
            pop;
            gettoken(); //读取')'之后的符号
            deal_with_declarator();
        }
        else
        {
            printf("%s ", pop.string);
        }
    }
}

TEST_F(Testcase, test)
{
    MuteOff();
    read_to_first_identifer(); //将标记压入堆栈,直到遇见标识符
    deal_with_declarator();
    printf("\n");
}

实验结果: