指针是C/C++里非常重要的概念,它是内存地址的抽象。指针必须和实际的类型结合起来才有意义,因此,我们会看到指向各种类型的指针,包括数据类型的指针、数组的指针以及函数的指针。这有时让初学者感到困惑。
希望这篇文章可以或多或少地减少一些大家在指针学习过程中的困惑。
“右左右左”秘籍
口诀如下:
找名字,定类型,先往右,后往左,遇括号,换方向,找不到,即掉头。
普通指针
int *a;
一目了然,这里不需要用秘籍就能看出a是一个指向int类型的指针。我们也可以试着用一下秘籍。
- 找到名称 a
- 往右看,无
- 往左看,a是一个指针
- 往右看,无(可直接跳过)
- 往左看,指针a指向int类型地址
常量指针和指针常量
更容易看懂的描述:指向常量的指针和指针常量
const int x = 1;
const int y = 3;
const int *a = &x;
a = &y;
// *a = 1; error: read-only variable is not assignable
int u = 2;
int v = 4;
int* const b = &u;
*b = 2;
// b = &v; error: cannot assign to variable 'b' with const-qualified type 'int *const'
以上,a是指向常量的指针,b是指针常量。
大家可以看到每段代码中的最后一行注释,被注释的代码在编译时会报错,错误信息来自clang++ apple llvm 8.1.0,用其他编译器也会有类似报错,描述信息可能会不同。
第一个报错信息说明给指针a指向的内容赋值是不行的,因为指针a指向的是一个常量,这是很显然的,因为定义x和y的时候,定义的就是常量。
修改第一段代码,去掉常量x和y的const修饰,编译时依然会报错。
int x = 1;
int y = 3;
const int *a = &x;
a = &y;
// *a = 1; error: read-only variable is not assignable
因为指针a指向的类型是常量,不会受实际指向的变量或常量的类型影响。
第二个报错信息说明不能给常量b赋值。
到这里大家应该能注意到我在描述a和b的命名的区别了:指针a和常量b
来吧,我们使用秘籍验证一下
const int *a = &x;
- 找到名称 a
- 往右看,无(忽略赋值)
- 往左看,a是一个指针
- 往左看,指针a指向const int类型地址(可分成两步,指针a指向一个int类型地址,这个地址中的数据是个常量)
int* const b = &u;
- 找到名称 b
- 往右看,无(忽略赋值)
- 往左看,b是一个常量
- 往左看,b这个常量存的是一个指针
- 往左看,b这个常量存的指针指向一个int类型地址
再来看下面这个定义
const int* const b1 = &u;
可以接上面秘籍第5步,再往左看,b1这个常量存的指针指向一个int类型地址,这个地址中的数据是一个常量
再变一下
int const* const b2 = &u;
头晕了没,这个新的定义和上面那个是一样的。但如果硬要从字面上看区别,可以体会下面两段描述的区别。
b1是一个常量,存了一个指针,指针指向了一个存储int类型的地址,这个地址里存的数据是一个常量
b2是一个常量,存了一个指针,指针指向了一个存储常量的地址,这个地址里存的是一个int类型数据。
数组指针和指针数组
更容易看懂的描述:指向数组的指针和存储指针的数组
int *c[10];
int (*d)[10];
来猜猜,c和d分别是什么?10秒后往下看秘籍
int *c[10];
- 找到名称 c
- 往右看,c是一个数组,这个数组有10个元素
- 往左看,c这个数组存的是指针
- 往左看,c这个数组存的指针指向int类型地址
int (*d)[10];
- 找到名称 d
- 往右看,遇到小括号返回
- 往左看,d是一个指针
- 往右看,d这个指针指向一个有10个元素的数组
- 往左看,d指向的这个10元素的数组存储int类型数据
运用两次秘籍以后,c和d是什么就很明显了。
函数指针和返回指针的函数
int *f(int i);
int (*fp)(int i);
经过之前的介绍,这里就直接上秘籍吧,看看f和fp分别是什么
int *f(int i);
- 找到名称 f
- 往右看,f是一个函数,函数有一个int类型的参数
- 往左看,f的返回类型是指针
- 往左看,f返回的指针指向int类型的地址
int (*fp)(int i);
- 找到名称 fp
- 往右看,遇到小括号返回
- 往左看,fp是一个指针
- 往右看,fp指向一个函数,函数有一个int类型的参数
- 往左看,fp指向的函数返回值是int类型
来两个复杂一点的例子
int (*f(int i))(bool b)
一眼看不出f是什么,祭出秘籍
- 找到名称 f
- 往右看,f是一个函数,函数有一个int类型的参数
- 往左看,f的返回类型是指针
- 往右看,f返回的指针指向一个函数,函数有一个bool类型的参数
- 往左看,f返回的函数指针指向的函数返回值是int类型
int (*(*fp)(int i))(bool b);
- 找到名称 fp
- 往右看,遇到小括号返回
- 往左看,fp是一个指针
- 往右看,fp指向一个函数,函数有一个int类型的参数
- 往左看,fp指向的函数返回值是一个指针
- 往右看,fp指向的函数返回的指针指向一个函数,这个函数有一个bool类型的参数
- 往左看,fp指向的函数返回的函数指针指向的函数返回值是int类型
看到这里是不是已经感觉开始绕口令了?
对于C/C++,我们可以使用typedef把这类复杂的定义表达得清晰一些
typedef int (*FP1)(bool b);
typedef FP1 (*FP2)(int i);
FP2 fp;
虽然敲的代码变多了,但定义变得更清晰了
最后
int (*(*(*(*fppp)(int i))(bool b))(char c))(long l);
看到这个是不是有点想骂人的感觉,这次我们不用秘籍,用typedef一层层地把这个定义分析清楚。但分析的步骤不是从最内层的名称开始,而是从最外层的返回值开始。
第一层
typedef int (*FP1)(long l);
把FP1代换回原定义
FP1 (*(*(*fppp)(int i))(bool b))(char c);
第二层
typedef FP1 (*FP2)(char c);
把FP2代换回原定义
FP2 (*(*fppp)(int i))(bool b);
第三层
typedef FP2 (*FP3)(bool b);
把FP3代换回原定义
FP3 (*fppp)(int i);
到这里,fppp是什么已经比较清楚了。它是一个函数指针,指向的函数有一个int类型参数,返回值是函数指针,这个函数指针指向的函数有一个bool类型的参数,返回值是函数指针,这个函数指针指向的函数有一个char类型的参数,返回一个函数指针,这个函数指针指向的函数有一个long类型的参数,返回值是int类型。
最后的最后
在实际代码中,前面描述的这种N层函数指针的定义很少被用到。但在支持Currying和Partial Application的语言里这种返回函数指针的函数是非常平常的。比如用Haskell的伪代码,上面那个复杂的定义就能写得很清晰。
int->bool->char->long->int
网友评论