C语言的声明可以嵌套,所以可以构成一些相当复杂的嵌套,这种过于复杂的声明并不推荐,但是我们应该学会如何去阅读这些声明。并在后面的部分会讲述如何利用typedef去简化这些声明

left-right rule

"left-right" rule是解读C语言声明的强力规则,掌握这个规则足够让我们解决几乎所有的C声明,而且规则并不复杂,分为下面几步

  1. 从变量名开始阅读
  2. 从包含变量名的最内部的括号由内而外解释,对于每个括号的内部,先往右边解释,再往左边解释,层层解释> 直至结束。

第一条规则告诉你如何开始,第二条规则告诉你如何解释,然后,我们要记住一些规则,你可以理解为这是规定的内容,是死的。

  • * 读作 “指向…的指针” ,接下里的标识符修饰指针指向的内容
  • [] 读作 “…的数组” ,接下来的标识符修饰数组的元素
  • () 读作 “返回…的函数”,接下来的标识符修饰函数返回值

读作...是用语言的方式去将声明一层层定义出来,就是反映了接下来修饰的内容,理解后面的部分我认为会更加直接。

好的,看了上面的内容你大概了还是相当模糊的,不要急,上面的内容暂时不必关系,下面我会开始列举一些例子,可以一边看例子一边看解释规则,如果你想自己尝试一下也可以先自己解读。


先来看一点我们以前可能遇到的头疼声明

1
2
3
1. const int * a;
2. int const * a;
3. int * const a;

不知道你是否还记得结果,在这三个声明中,只有3的指针是只读的,也就是指针常量,其它两个都是指针指向的对象是只读的,也就是常量指针。我们来一个个解读。

const int * a

  1. 先找到变量名a

  2. 无括号,总共就一层,先往右看,没有任何标识符

  3. 往左看,首先我们看到的是一个*,OK,那么此时a已经是一个指针了,接下来的标识符只会修饰指针指向的内容,这一点很关键

  4. 继续往左看,看到int,说明指针指向一个int类型的变量

  5. 再看到const,表明该int类型的变量是一个常量

那么这个声明的结果就是变量a是一个指针,指针指向的内容是一个int类型的常量

int const * a

  1. 找到变量名a
  2. 往右看,无标识符
  3. 往左看,*,a是一个指针
  4. 往左看,const,指向的变量是一个常量
  5. 往左看,int,该常量是int类型的

那么这个声明的结果就是变量a是一个指针,指针指向的内容是一个int类型的常量

int * const a

  1. 找到变量名a
  2. 往右看,无标识符
  3. 往左看,const,a是常量
  4. 往左看,*,a是一个指针,也就是a现在是一个常量指针,接下来的标识符会开始修饰指针指向的变量
  5. 往左看,int,指针指向的变量是int类型的

那么这个声明的结果就是变量a是一个常量指针,指针指向的内容是一个int类型的变量

从上面的例子中,有两点很重要

  1. 按照顺序读取
  2. 一定要理解标识符什么时候修饰变量a,什么时候修饰a指向的内容,当a被定义为指针以后,标识符就只会修饰指针指向的内容,这是一个固定的规则

再来看一个容易混淆的例子

1
2
1. int *p[10];
2. int (*p)[10];

1是指针数组,2是数组指针

int *p[10]

  1. 找到变量名p
  2. 往右看,[10],p是一个数组,接下来标识符只能修饰数组元素
  3. 往左看,*,数组元素是指针
  4. 往左看,int,指针指向的变量是int类型

int (*p)[10]

  1. 找到变量名p
  2. 因为有括号,所以要先处理(*p)
  3. 往右看,无标识符
  4. 往左看,*,p是一个指针,接下来的标识符只会修饰指针指向的内容
  5. 解释完括号内部的,现在解释外部的,int ()[10]
  6. 往右看,[10],指针指向的变量是数组类型的
  7. 往左看,int,数组的元素是int类型

我们继续来看一点复杂一些的例子

1
char * const *(*b)(int a);
  1. 找到变量名b
  2. 有括号,我们要先解释(*b)
  3. *b往右看,无标识符
  4. 往左看,b是一个指针
  5. 然后再解释char * const * (int a)
  6. 往右看(int a),b指向的变量是一个函数,参数是一个整型变量,接下来左边的标识符都只会修饰函数返回值
  7. 往左看,*,该函数的返回值是一个指针
  8. 往左看const,返回值的指针指向一个常量
  9. 往左看,*,这个常量是一个指针
  10. 往左看,char,指针常量指向一个char类型变量

那么这个声明的结果就是变量a是一个函数指针,函数的参数是一个整型变量,返回值是一个指针,指针指向一个指针常量,指针常量指向一个字符型变量

这里要说明一下,函数的参数也是要经过解释的,只不过这个例子和下面这个例子的函数参数都复杂我就直接跳过了。

1
char *(* a[10])(int **p);
  1. 找到变量名a
  2. 先处理(* a[10])
  3. 往右看,[10],a是一个数组,接下来的标识符只会修饰数组元素
  4. 往左看,*,数组元素是指针类型
  5. 接下来解释char *()(int **p)
  6. 往右看,(int **p),指针指向一个函数,参数是二级指针
  7. 往左看,*,返回值是一个指针
  8. 往左看,char,char类型指针

那么这个声明的结果就是变量a是一个数组,数组元素是函数指针,函数参数是二级指针,返回值一个指向字符型变量的指针

OK,如果你看完并理解了上面的内容,你会发现解释C的声明实际上也不是太复杂,我重新总结一下要点,我相信现在的你看这些要点会有新的感悟

  1. 按照顺序解释
  2. 记住现在解释的内容是什么,比如变量已经声明为指针后,要清楚接下来的标识符只会修饰指针指向的内容,而不会再来修饰这个指针本身。

虽然现在我们会解释C语言的声明,但是复杂的声明在绝大部分情况下都不是好的选择,接下里我们介绍用typedef来简化声明

用typedef来简化声明

typedef在我们初学C时其实并不太常用,大多数时候我们用来简化结构体的声明

1
2
3
4
struct Node{
int a;
}
typedef struct Node Node;

接下来我们就可以直接用Node而不是struct Node来定义变量,这种做法是否明智不是我们要讨论的内容,但是仅仅是这样的使用远远没有展现出typedef的力量。

typedef和#define的区别

如果我们仅仅只是像上面一样用过typedef,我们可能会对typedef的真实作用产生误区。

#define是完全的字符串替换,仅此而已,但是typedef从某种意义上来说,它真实的重命名了一个类型。

比如下面这个例子

1
2
3
4
#define A int*
typedef int* B;
A a,b;
B c,d;

变量a是指针,而变量b只是一个普通的整型变量,因为实际的定义是这样的

1
int *a,b;

但是c和d都是指针,因为B是类型int *的别名,是一个整体,可以说B本身就是一个新的类型。

上面的例子已经展现了typedef与字符串替换大不相同,但typedef不仅于此,如果你没有真正的明白typedef的作用,那么下面这个例子你一定无法理解

1
2
typedef int Array[20];
Array a;

此时的a是一个长度为20的整型数组。Array是一个独立的类型,这个类型是长度为20的整型数组。

现在你应该可以明白typedef的用法了,下面就来看看typedef怎么简化声明。

typedef简化声明

我们来简化上面的例子

char *(* a[10])(int **p);

1
2
3
typedef char *(*Func)(int **p);
// 那么a可以定义如下
Func a[10];

我们用typedef把函数部分和数组部分拆分了开来,这些简化效果不是特别明显,我们来看更复杂一点的例子。

1
char *(* (*a[10])[10][10])(int **p,char *(*f)(int *p[10]));

这个例子已经毫无可读性了,也不需要耗费心神去解释,我们来简化这个声明。

1
2
3
4
5
6
7
8
9
10
// 先简化作为参数的函数
typedef char *(*ParamFunc)(int *p[10]);
// 再把整个函数部分抽离出来
typedef char *(*Func)(int **p,ParamFunc f);
// 最后再把数组部分加上
typedef Func (*ComplexPointer[10])[10][10];
// 接下来就可以直接定义了
ComplexPointer a;
// 下面是不使用typedef的,对比一下
char *(* (*a[10])[10][10])(int **p,char *(*f)(int *p[10]));

可见typedef在复杂声明中的力量,当然,简化的方法可以按照你自己的想法任意的组织。

好了,本文主要是讲述了C语言的声明相关内容,希望能让你有所收获。