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*ipis 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 语言声明解析工具,可以将声明和英文描述互相转换。
本文是我对指针声明的一些理解,如有错误或不足之处,欢迎指正。