0 前言
指针之于C语言,就像子弹于枪械。没了子弹的枪械虽然可以用来肉搏,却失去了迅速解决、优雅解决战斗的能力。但上了膛的枪械也非常危险,时刻要注意是否上了保险,使用C语言的指针也是如此,要万分小心,一着不慎就可能灰飞烟灭。
对于一重指针,熟悉C语言的已经烂熟于心,但很多人对于双重指针甚至n重指针仍然抱有恐惧的心理。本文从实例出发,讲解多重指针背后的意义和使用方法。
1 n重指针介绍
1.0 什么是指针?什么是指针变量?什么是解引用?
很多人经常习惯性将指针变量说成指针,实际上指针和指针变量不是同一个东西。指针和指针变量的区别如下:
指针:内存地址
指针变量:存放内存地址的变量
解引用:*运算符获取指针(内存地址)指向(表示的)对象的值(也就是获取内存地址上存储的值,获取大小和指针类型有关)
指针和指针变量的大小都是CPU寻址的位数大小,假如使用32位MCU则大小为32位。
有关指针变量的定义和解引用及指针的解引用操作实例如下:
c
int main(void)
{
int val = 0x1234;
int *p = &val; // 指针变量,初值为变量val的内存地址
printf("*p : 0x%x\r\n", *p); // 解引用指针变量
printf("*(int *)&val : 0x%x\r\n", *(int *)&val); // 将val地址强制转换成int型指针,然后解引用
return 0;
}
打印结果如下:
1.1 n重指针解引用
下面是1-4重指针的解引用,简单来说n重指针存储的是n-1重指针的地址,如果n=1(一重指针)则存储的就是对象地址:
c
#include "stdio.h"
int main(void)
{
int val = 0x1234;
int *p1 = &val;
int **p2 = &p1;
int ***p3 = &p2;
int ****p4 = &p3;
printf("****(int ****)p4 : 0x%x\r\n", ****(int ****)p4);
printf("***(int ***)p3 : 0x%x\r\n", ***(int ***)p3);
printf("**(int **)p2 : 0x%x\r\n", **(int **)p2);
printf("*(int *)p1 : 0x%x\r\n", *(int *)p1);
return 0;
}
打印结果:
1.2 n重指针变量的定义及解引用
下面是1-4重指针变量的解引用,简单来说n重指针变量存储的是n-1重指针变量的地址,如果n=1(一重指针变量)则存储的就是对象地址:
c
int main(void)
{
int val = 0x1234;
int *p1 = &val;
int **p2 = &p1;
int ***p3 = &p2;
int ****p4 = &p3;
printf("****p4 : 0x%x\r\n", ****p4);
printf("***p3 : 0x%x\r\n", ***p3);
printf("**p2 : 0x%x\r\n", **p2);
printf("*p1 : 0x%x\r\n", *p1);
return 0;
}
打印结果:
我们定义n重指针变量,可以将它拆开成2个部分看:
解引用的赋值也可以看成2个部分:
1.3 多重指针变量的用途
1.3.1 修改指针变量的值
实际上,假如我们定义多重指针变量只是为了获取对象的值,那多重指针变量相当于绕远路到达终点而不是直接使用一重指针变量直达终点。绕远路到达终点的好处在于可以修改中间指针变量的值,甚至修改我们的目的地。假如我们需要在子函数内修改父函数的指针变量的值,就可以用到双重指针,例子如下:
c
/**
* @brief 将p的值修改为0x12345678
*
* @param p 双重指针
*/
void set_p(int **p)
{
*p = (int *)0x12345678;
}
int main(void)
{
int *p1;
set_p(&p1);
printf("p1 val : 0x%X\r\n", p1);
}
打印结果如下:
说明:
我们通过&操作符取一重指针p1的内存地址传递给形参(二重指针,避免编译器警告),形参内对p1的内存地址进行一次解引用然后赋值,将p1的值修改为0x12345678。
1.3.2 避免编译器警告
对我们来说指针就是地址,n重指针存储的也是地址,那么我们为什么不可以全部使用一重指针去解引用指针呢?这主要是为了让编译器理解我们的意图,避免告警。下面的例子就会出现一个警告:
c
/**
* @brief 将p的值修改为0x12345678
*
* @param p 双重指针
*/
void set_p(int *p)
{
*p = (int)0x12345678;
}
int main(void)
{
int *p1;
set_p(&p1);
printf("p1 val : 0x%X\r\n", p1);
}
警告内容:
打印结果:
说明:
实际上我们通过一重指针也可以修改指针变量的值,但是编译器不知道我们的意图,向我们抛出了警告。因此,多重指针在一些场合下还可以避免警告产生。
1.4 在物理层面看多重指针的意义
指针就是内存地址,是有实际物理意义的。下面打印1-4重指针在内存上的地址,分析物理内存上多重指针解引用的过程。相关程序如下:
c
int main(void)
{
int val = 0x12345678;
int *p1 = &val;
int **p2 = &p1;
int ***p3 = &p2;
int ****p4 = &p3;
/* 地址 */
printf("val addr : 0x%X\r\n", &val);
printf("p1 addr : 0x%X\r\n", &p1);
printf("p2 addr : 0x%X\r\n", &p2);
printf("p3 addr : 0x%X\r\n", &p3);
printf("p4 addr : 0x%X\r\n", &p4);
/* 解引用时值变化过程 */
printf("*p1 : 0x%x \r\n", *p1);
printf("**p2 : 0x%x -> 0x%x\r\n", *p2, **p2);
printf("***p3 : 0x%x -> 0x%x -> 0x%x\r\n", *p3, **p3, ***p3);
printf("****p4 : 0x%x -> 0x%x -> 0x%x -> 0x%x\r\n", *p4, **p4, ***p4, ****p4);
}
打印结果如下:
注:本文使用PC运行该程序,CPU寻址位数为64位,因此指针大小为64位。
示意图如下(以p4的解引用为例):
2 总结
(1)指针就是内存地址,指针变量就是存储了内存地址的变量,指针的大小和CPU支持的寻址位数一致,指针解引用对象的大小和指针类型大小一致。
(2)多重指针可以作为函数形参,来实现对指针变量的修改。
(3)多重指针的解引用可以理解为绕远路获取对象的值,n重指针只有进行n次解引用才能获取到对象的值,1-n-1次解引获取到的都是指针(内存地址)。