C 语言中的复杂指针声明
当初学习 C 语言时就对指针的声明感到疑惑。比如应该是 int *ip;
还是 int* ip;
?其中的 *
是表示指针类型吗?但 *
又是解引用运算符,难道用在声明和表达式中是两个意思吗?
直到前段时间读了 Kernighan 和 Ritchie 的所著的《The C Programming Language》后豁然开朗,感觉对指针的理解基本明晰了。
指针声明的含义
首先 *
作为运算符只有乘和解引用两种含义。声明 int *ip;
中的 *
显然不可能表示乘,那么只能表示解引用。让我们看看 C 语言设计者是怎么想的:
The declaration of the pointer
ip
,int *ip;
is intended as a mnemonic; it says that the expression*ip
is an int.
在指针 ip
的声明中,int *ip;
是一个助记符,表示 *ip
是一个 int
类型的表达式。
所以指针声明 int *ip;
的字面含义是规定了变量 ip
在解引用后的类型。规定了解引用后的类型,也就确定了变量 ip
的类型,即指向 int
类型的指针。不过虽然 *
在这里表示解引用的含义,但并不会真正进行解引用操作。
这样就解开了一开始的疑惑,应该是 int *ip;
而不是 int* ip;
。虽然二者都能编译通过,但前者是更方便理解的,它告诉我们 *ip
可以用在所有 int
类型可以使用的地方。
对于函数的声明也是类似的。比如 int *f()
表示 *f()
是 int
类型,也就是说函数 f()
的返回值是一个指向 int
类型的指针。知道了 *f()
是 int
类型,很容易判断 1 + *f()
是合法的表达式。
一般地,声明 T *p;
表示 *p
是 T
类型,p
是指向 T
类型的指针。
现在来分析 int **pp
就很容易了。可以先将其看作 int *(*pp);
,根据上面的规则可知 *pp
是指向 int
类型的指针。即 pp
在解引用后是一个指针,所以 pp
是指向指针的指针。
const
修饰符
现在引入 const
修饰符,const
修饰符的位置有两种,一种是修饰指针,一种是修饰指针指向的对象。判断时还是按照上面的规则。
const int *p;
和上面的 T *p;
做对比,可知 T
是 const int
,所以 p
是指向 const int
类型的指针,即 p
是指向 int
类型的常量指针。p
指向的对象是不可变的,但 p
本身是可变的。
1 |
|
int *const p;
和上面的 T *p;
做对比,可知 T
是 int
,所以 p
是指向 int
类型的指针,但 p
被 const
修饰,即 p
是指针常量。p
本身是不可变的,但 p
指向的对象是可变的。
1 |
|
将上面两种情况结合得到 const int *const p;
,这时 p
本身和指向的对象都是不可变的。
复杂定义
更复杂的定义结合了指针、数组和函数,这时需要按照运算符优先级和结合性来判断类型。
首先明确运算符优先级 p()
= p[]
> *p
p()
代表函数,p[]
代表数组,*p
代表对 p
解引用。
对于数组的声明,可以用类似指针的理解方法。T a[5]
表示 a
是一个包含 5 个元素的数组,a[i]
是 T
类型。
识别方法:从变量名称出发,按照优先级和运算符结合判断变量的类型。即首先要判断出 p
是指针、数组还是函数,然后再明确 p
的具体信息(如指向的对象类型、数组元素类型、函数参数返回值类型等)。
下面是一些复杂声明的例子,可以先尝试分析一下。
1 |
|
int *p[5];
[]
的优先级高于*
,p
先与[]
结合。原声明可以改写为int *(p[5]);
,所以p
是包含 5 个元素的数组。- 判断数组元素的类型,因为
*p[i]
是int
类型,所以p[i]
是指向int
类型的指针。 - 故
p
是包含 5 个指向int
类型的指针的数组。
int (*p)[5];
- 由于有括号,
p
先与*
结合,所以p
是指针。 - 判断指针指向的类型,
(*p)
再与[5]
结合,所以*p
是包含 5 个元素的数组,p
指向一个数组。 - 判断数组的元素类型,
(*p)[i]
是int
类型。 - 故
p
是一个指向数组的指针,数组中的元素是int
类型。
int (*p)(int, int);
- 由于有括号,
p
先与*
结合,所以p
是指针。 - 判断指针指向的类型,
(*p)
再与(int, int)
结合,所以*p
是一个函数,接收两个int
参数,返回值为int
。 - 故
p
是一个函数指针,指向一个接收两个int
参数,返回值为int
的函数。
int (*p)(int);
和上一个类似,只是函数只接受一个 int
参数。p
是一个函数指针,指向一个接收一个 int
参数,返回值为 int
的函数。
int (*p[5])(int, int);
[]
的优先级高于*
,p
先与[]
结合,所以p
是包含 5 个元素的数组。- 判断数组元素的类型,
(*p[i])
再与(int, int)
结合,所以(*p[i])
是一个函数,接收两个int
参数,返回值为int
。因此p[i]
是指向此类函数的函数指针。 - 故
p
是包含 5 个函数指针的数组,这些函数指针指向的函数接收两个int
参数,返回值为int
。
int *(*p[5])(int);
和上一个类似,p
是包含 5 个函数指针的数组,这些函数指针指向接收一个 int
参数,返回值为 int
指针的函数。
int (*(*p())[])()
p
先与()
结合,所以p
是函数,无参数p()
再与*
结合,所以p()
的返回值是指针- 判断指针的类型,
(*p())
再与[]
结合,所以(*p())
是一个数组,p()
是指向数组的指针 - 判断数组的元素类型,
(*p())[]
与*
结合,所以数组元素(*p())[]
是指针 - 判断指针的类型,
*(*p())[]
再与()
结合,所以*(*p())[]
是函数,无参数,返回值为int
- 综合得到
p
是一个无参数,返回值是指针的函数。该指针指向一个数组,数组中的元素类型为指向无参数,返回值为int
的函数的指针
int (*(*p[3])())[5];
p
先与[]
结合,所以p
是包含 3 个元素的数组- 判断数组元素的类型,
p[i]
再与*
结合,所以p[i]
是指针 - 判断指针的类型,
(*p[i])
再与()
结合,所以(*p[i])
是无参数的函数,p[i]
是指向此类函数的函数指针 - 判断函数的返回值类型,
(*p[i])()
再与*
结合,所以(*p[i])()
的返回值是指针 - 判断指针的类型,
*(*p[i])()
再与[]
结合,所以*(*p[i])()
是一个数组,(*p[i])()
是指向数组的指针 - 判断数组的元素类型,
(*(*p[3])())[5]
是int
类型 - 综上可知
p
是一个包含 3 个元素的数组,数组中的元素是函数指针,这些函数指针指向接收无参数,返回值为指向包含 5 个int
元素的数组的指针。
总结
虽然实际中基本不会用到后面几个特别复杂的声明,但掌握分析方法对于数组指针、指针数组、函数指针等相对常见的复杂声明还是很有帮助的。
关于指针的其他内容推荐阅读《The C Programming Language Second Edition》第五章。其中 5.12 节详细讲解了复杂声明的分析方法,并给出了一个声明的解析程序 dcl
。
cdecl 是一个在线的 C 语言声明解析工具,可以将声明和英文描述互相转换。
本文是我对指针声明的一些理解,如有错误或不足之处,欢迎指正。