你或许还不那么懂指针
是int *a还是int* a?
对于这个问题,答案是二者均可
实际上,int *a更偏向于一种语法规则,在我们定义多个变量时,就能够体现其好处,比如
1 | int *a, b; |
这个语句的含义就是定义了一个名为a的整型指针变量和一个名为b的整型变量。即“*”仅修饰了a
而int* a则更能表示这个语句的本质,即a是一个int*类型的变量
因此,下面这个语句表示的不是一个数组的指针,而是一个数组,其中有 10 个int*类型的变量
1 | int *a[10]; |
与之需要区分的是行指针
1 | int (*a)[10]; |
常量指针和指针常量
我们直到使用const关键字可以定义常量,比如const int a = 10;
那么,如何理解下面两个语句
1 | const int *p1; |
我们把p1称作常量指针,把p2称作指针常量
这两个名称记起来并不困难,const int *p中,const在前,所以先读常量,*在后,后读指针,因此这时一个常量指针
另一个同理
至于二者的区别,可以理解为const int *p中,const修饰了int,即p指向的值的类型,那么这个值就是无法修改的,比如
1 | int a = 10, b = 20; |
同样的,int *const p;中,const修饰了p这个指针变量,因此,指针指向的地址不能改变,比如
1 | int a = 10, b = 20; |
此外,还有常量指针常量,即值和地址都不能修改
1 | const int *const p; |
| 类型 | 声明方式 | 修饰对象 | 指针指向(地址) | 指针内容(值) |
|---|---|---|---|---|
| 常量指针 | const int *p; |
指针指向的数据 | 可变 | 不可变 |
| 指针常量 | int *const p; |
指针本身 | 不可变 | 可变 |
| 常量指针常量 | const int *const p; |
指针指向的数据和指针本身 | 不可变 | 不可变 |
函数指针
函数指针用于指向函数“代码”内存的地址,通过对指针解引用,就可以使用函数,如下
1 |
|
从上面的代码可以看出
-
函数名本身就是一个函数指针
-
函数指针可以直接使用,也可以先解引用再调用
我们还可以通过类型别名简化代码,以便将函数作为参数 [1]
1 |
|
在上面的代码中,comp函数作为参数被func函数调用,comp被称为回调函数
三个可能导致错误的指针
空指针
空指针(Null Pointer)即明确指向“无”的指针
在C语言中定义为NULL,而在C++11后使用nullptr
一般在一个指针(如函数指针)初始化时,我们用空指针进行占位,否则就会变成也指针
野指针
野指针(Wild Pointer)即未初始化的指针
指针变量在刚创建时(比如int *p;),它里面是随机分配的垃圾值
此时如果对齐进行解引用(如*p = 10;),程序可能会报错
这常常发生在用结构体构架链表的过程中,比如
1 | typedef struct Node |
要解决这个问题,有两种方法:静态分配内存和动态分配内存
1 | // 静态分配内存:在栈中申请内存 |
悬空指针
当一个指针变量p指向的内存中的值被释放(手动释放或自动释放)后,p仍然指向该地址,而地址中的值都是些垃圾值,此时p就成为了悬空指针(Dangling Pointer)
这中指针产生的问题可能会发生在一些初学者身上,比如
1 | int *get_nums() |
我们知道,函数在栈中运行,当函数结束时,函数内存(包括其中的变量)都会被自动释放,因此,get_nums函数中的nums并不能被成功返回
解决这个问题,一般有下面几种解决方式
1.使用动态内存分配
1 | int *get_nums() |
在其他函数使用完后要注意手动释放内存
2.由调用者提供空间
1 | void fill_array(int *arr) |
3.声明静态变量
1 | int* get_nums() |
万能指针void*
void*类型的变量可以接收任意类型的指针的值,比如
1 | int main() |
上面代码中,指针p既可以赋值为一个int*类型的指针,也可以被赋值为一个char*类型的指针
问题在于,由于不知道p指向的空间有多大,因此不能进行*p和加减法移位操作
利用万能指针,作为函数参数,可以避免不知道参数类型的情况,比如memcpy函数
1 | // 标准库原型:它可以处理任何类型的数据块 |
因此,善用万能指针,能避免不必要的函数重载
数组退化和函数退化
数组退化
数组名的本质
首先我们需要明确一点:数组名不是指针变量
要验证这点,我们可以做下面的实验
1 |
|
得到的输出如下
1 | p = 0000005ED19FFACC |
在这个实验中,我们定义了一个int*类型的指针变量p,作为指针变量,我们能够打印出其指向变量的地址;而其本身作为变量,我们也可以通过&p打印其自身的地址
事实证明,这两个地址确实不一样
这里要对指针和指针变量进行区分
-
指针:即地址,是一个常量
-
指针变量:是一个变量,可以修改,一个指针变量的内容是一个指针(地址)
在大多数情况,我们将指针变量称为指针
然而,arr作为数组名,我们发现arr和&arr打印出来的结果是一样的
对此的解释是,arr不作为指针或指针变量,仅仅是一个数组的标识
其本身是一个常量,因此无法进行“整体”的修改,如下
1 | int a[] = {1, 2, 3}; |
实际上,{1, 2, 3}也是一个表达式,并非指针,因此下面的写法也是错误的
1 | int *p = {1, 2, 3}; |
与之相对的,"123"是一个指针,因此下面的写法是正确的
1 | char *p = "123"; |
导致数组退化的情况 [2]
在C/C++中,数组名在大部分情况下会退化成指针
通过这个指针,我们就能访问和修改数组中的元素,比如
-
算术运算导致数组退化
1 | int arr[] = {1, 2, 3}; |
需要注意arr[i],和*(arr+i)(算术运算)的等价性
-
赋值导致的数组退化
将数组赋值给相应类型的指针
1 | int arr[] = {1, 2, 3}; |
-
传参导致数组退化
在数组作为函数参数时,我们往往要添加一个数组长度的参数
这是因为直接传入数组名无法传递数组的全部信息(数组退化,传递一个int*类型的指针)
1 | void func(int arr[100]) |
在这里,arr是一个int*类型的指针变量,因此输出结果为 8(64 位操作系统下),而不是100 * sizeof(int)
不会退化的情况 [2:1]
虽然大部分情况下,数组会退化,但是还是有不会退化的情况的
-
sizeof运算
通过sizeof函数/关键字,我们仍然可以得到数组的大小
1 | int arr[100] = {0}; |
-
取地址
上面我们发现数组名可以取地址,实际上,通过&arr,我们可以得到一个指向数组的指针
具体可以参考下面的对比
1 |
|
输出结果如下
1 | a = 00000066ABBFFB20 |
可以看到,a + 1相当于在a的基础上加一个一个int的大小(退化),而&a + 1则是加了四个int(即一个数组)的大小
多维数组的退化
经过前面的实验,我们知道了一个一维数组退化后会变成一个对应类型的指针(如int*)
多维数组类似,即“降维”,比如
1 | int arr[2][2]; |
可以看到,这实际上就是行指针,这里的定义p的过程如下
-
首先,
p是一个指针,(*p) -
这个指针指向一个行(一维数组),
(*p)[2] -
每一行中的元素类型是整型,
int (*p)[2]
同样的,三维数组如下
1 | int arr[3][2][2]; |
-
首先,
p是一个指针,(*p) -
这个指针指向一个矩阵(二维数组),
(*p)[2][2] -
每一矩阵中的元素类型是整型,
int (*p)[2][2]
函数的退化
在上文介绍的函数指针中,就涉及到了一点函数退化的机制,比如
1 | void (*p1)() = myFunc; // 隐式退化:函数名直接赋值 |
基于此特性,就会有下面的奇怪的写法
1 |
|
因为 test 会退化成指针,而对指针解引用 *test 后,它又因为身处表达式中而再次退化成指针
因此可以对一个函数名无限的解引用而其结果不变



