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.

Kernighan&Ritchie -- The C Programming Language Second Editon P94

在指针 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; 表示 *pT 类型,p 是指向 T 类型的指针。

现在来分析 int **pp 就很容易了。可以先将其看作 int *(*pp);,根据上面的规则可知 *pp 是指向 int 类型的指针。即 pp 在解引用后是一个指针,所以 pp 是指向指针的指针。

const 修饰符

现在引入 const 修饰符,const 修饰符的位置有两种,一种是修饰指针,一种是修饰指针指向的对象。判断时还是按照上面的规则。

const int *p; 和上面的 T *p; 做对比,可知 Tconst int,所以 p 是指向 const int 类型的指针,即 p 是指向 int 类型的常量指针p 指向的对象是不可变的,但 p 本身是可变的。

1
2
3
4
5
const int a = 10;
const int b = 20;
const int *p = &a;
*p = 20; // 错误,p指向的对象是不可变的
p = &b; // 正确,p本身是可变的

int *const p; 和上面的 T *p; 做对比,可知 Tint,所以 p 是指向 int 类型的指针,但 pconst 修饰,即 p指针常量p 本身是不可变的,但 p 指向的对象是可变的。

1
2
3
4
5
int a = 10;
int b = 20;
int *const p = &a;
*p = 20; // 正确,p指向的对象是可变的
p = &b; // 错误,p本身是不可变的

将上面两种情况结合得到 const int *const p;,这时 p 本身和指向的对象都是不可变的。

复杂定义

更复杂的定义结合了指针、数组和函数,这时需要按照运算符优先级和结合性来判断类型。

首先明确运算符优先级 p() = p[] > *p

p() 代表函数,p[] 代表数组,*p 代表对 p 解引用。

对于数组的声明,可以用类似指针的理解方法。T a[5] 表示 a 是一个包含 5 个元素的数组,a[i]T 类型。

识别方法:从变量名称出发,按照优先级和运算符结合判断变量的类型。即首先要判断出 p 是指针、数组还是函数,然后再明确 p 的具体信息(如指向的对象类型、数组元素类型、函数参数返回值类型等)。

下面是一些复杂声明的例子,可以先尝试分析一下。

1
2
3
4
5
6
7
8
int *p[5];
int (*p)[5];
int (*p)(int, int);
int (*p)(int);
int (*p[5])(int, int);
int *(*p[5])(int);
int (*(*p())[])();
int (*(*p[3])())[5];

int *p[5];

  1. [] 的优先级高于 *p 先与 [] 结合。原声明可以改写为 int *(p[5]);,所以 p 是包含 5 个元素的数组。
  2. 判断数组元素的类型,因为 *p[i]int 类型,所以 p[i] 是指向 int 类型的指针。
  3. p 是包含 5 个指向 int 类型的指针的数组。

int (*p)[5];

  1. 由于有括号,p 先与 * 结合,所以 p 是指针。
  2. 判断指针指向的类型,(*p) 再与 [5] 结合,所以 *p 是包含 5 个元素的数组,p 指向一个数组。
  3. 判断数组的元素类型,(*p)[i]int 类型。
  4. p 是一个指向数组的指针,数组中的元素是 int 类型。

int (*p)(int, int);

  1. 由于有括号,p 先与 * 结合,所以 p 是指针。
  2. 判断指针指向的类型,(*p) 再与 (int, int) 结合,所以 *p 是一个函数,接收两个 int 参数,返回值为 int
  3. p 是一个函数指针,指向一个接收两个 int 参数,返回值为 int 的函数。

int (*p)(int);

和上一个类似,只是函数只接受一个 int 参数。p 是一个函数指针,指向一个接收一个 int 参数,返回值为 int 的函数。

int (*p[5])(int, int);

  1. [] 的优先级高于 *p 先与 [] 结合,所以 p 是包含 5 个元素的数组。
  2. 判断数组元素的类型,(*p[i]) 再与 (int, int) 结合,所以 (*p[i]) 是一个函数,接收两个 int 参数,返回值为 int。因此 p[i] 是指向此类函数的函数指针。
  3. p 是包含 5 个函数指针的数组,这些函数指针指向的函数接收两个 int 参数,返回值为 int

int *(*p[5])(int);

和上一个类似,p 是包含 5 个函数指针的数组,这些函数指针指向接收一个 int 参数,返回值为 int 指针的函数。

int (*(*p())[])()

  1. p 先与 () 结合,所以 p 是函数,无参数
  2. p() 再与 * 结合,所以 p() 的返回值是指针
  3. 判断指针的类型,(*p()) 再与 [] 结合,所以 (*p()) 是一个数组,p() 是指向数组的指针
  4. 判断数组的元素类型,(*p())[]* 结合,所以数组元素 (*p())[] 是指针
  5. 判断指针的类型,*(*p())[] 再与 () 结合,所以 *(*p())[] 是函数,无参数,返回值为 int
  6. 综合得到 p 是一个无参数,返回值是指针的函数。该指针指向一个数组,数组中的元素类型为指向无参数,返回值为 int 的函数的指针

int (*(*p[3])())[5];

  1. p 先与 [] 结合,所以 p 是包含 3 个元素的数组
  2. 判断数组元素的类型,p[i] 再与 * 结合,所以 p[i] 是指针
  3. 判断指针的类型,(*p[i]) 再与 () 结合,所以 (*p[i]) 是无参数的函数,p[i] 是指向此类函数的函数指针
  4. 判断函数的返回值类型,(*p[i])() 再与 * 结合,所以 (*p[i])() 的返回值是指针
  5. 判断指针的类型,*(*p[i])() 再与 [] 结合,所以 *(*p[i])() 是一个数组,(*p[i])() 是指向数组的指针
  6. 判断数组的元素类型,(*(*p[3])())[5]int 类型
  7. 综上可知 p 是一个包含 3 个元素的数组,数组中的元素是函数指针,这些函数指针指向接收无参数,返回值为指向包含 5 个 int 元素的数组的指针。

总结

虽然实际中基本不会用到后面几个特别复杂的声明,但掌握分析方法对于数组指针、指针数组、函数指针等相对常见的复杂声明还是很有帮助的。

关于指针的其他内容推荐阅读《The C Programming Language Second Edition》第五章。其中 5.12 节详细讲解了复杂声明的分析方法,并给出了一个声明的解析程序 dcl

cdecl 是一个在线的 C 语言声明解析工具,可以将声明和英文描述互相转换。

本文是我对指针声明的一些理解,如有错误或不足之处,欢迎指正。


C 语言中的复杂指针声明
http://blog.qzink.me/posts/C语言中的复杂指针声明/
作者
Qzink
发布于
2025年1月4日
许可协议