一、指针是什么?
1、指针、地址、内存
相信很多同学在学习了指针之后还不清楚指针是什么?
- 对于指针来说,它在内存中其实是中一个最小单元的编号,也就是地址。通俗一些说其实就相当于我们在酒店开了一间房,这个房间的编号就叫做地址,你也可以把它叫做一个指针,那么这间房间就只是这个酒店里面的一个编号而已
- 其实这么看来就可以说==指针、内存、地址==这三者其实是等价的
- 上面说到的是在内存中指针的说法,但是在我们学习C语言时,口头上所称的指针,通常指的是
指针变量
,是用来存放内存地址的变量
【总结一下】:指针就是地址,口语中说的指针通常指的是指针变量
2、指针与变量
- 上面说到了指针变量,那我们就接着这个来做一个展开:当我们去定义出一个变量的时候,其实可以可以使用
[&]取地址操作符
去取出这个变量在内存中的地址,然后存放到一个变量中,那此时这个变量就叫做【指针变量】 - 一起到VS中通过代码来看看
c
int a = 10;
int* pa = &a;
printf("%p\n", &a);
printf("%p\n", pa);
- 可以看到,通过将变量a的地址取出来给到变量pa,然后以
%p内存地址
的形式去打印【pa】和【&a】的值就可以看到两个值是相等的,就可以说明pa里面确实存放了a的地址 - 在操作符章节的时候我有讲到过如何去看待
int* pa = &a
,对于这个*
来说值得就是pa这个变量是一个指针变量。它的前面的int
表示的就是这个指针变量存放的是一个整型的地址,那它就是一个【整型指针变量】 - 这样看可能会比较抽象,我们可以通过调用【内存】的方式来观察一下
- 此时就可以发现变量a在内存中是占4个字节的,而
[&a]
则是取出了首字节的地址,这么看相信你一定是非常得清晰了
- 不仅如此,除了整型变量的地址可以被存起来,字符型的地址也可以被存放起来,如下
c
char ch = 'c';
char* pc = &ch;
printf("%p\n", &ch);
printf("%p\n", pc);
- 那既然这个pc是一个变量的话,操作系统也会在内存中为其分配地址,我们可以去打印这个【指针变量】的地址看看
- 同样,我们可以通过调用【内存】的方式去观察一下,便可以看出char类型的变量在内存中确实只占一个字节
【总结一下】:指针变量,用来存放地址的变量
。(存放在指针中的值都被当成地址处理)
3、解答:为何指针均为4个字节❓
上面我们讲到了一个指针可以存放一个变量的地址,明白了整型和字符型的变量在内存中所占的大小,那指针在内存中占多少空间呢?
- 这里我定义了三个不同类型的变量以及不同类型的指针变量去接收它们的地址,接着使用
sizeof()
去计算了它们各自的地址
c
int main(void)
{
char ch = 'c';
int a = 10;
float f = 3.14f;
char* pc = &ch;
int* pa = &a;
float* pf = &f;
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(pa));
printf("%d\n", sizeof(pf));
return 0;
}
- 从运行结果可以看出每个指针变量的大小均为4个字节,这是为什么呢?这还要从机器中的【地址线】讲起
- 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
- 就是0101这样的存储方式,然后根据二进制的逢二进一去罗列出这32根地址线可以存储下多少地址,这里告诉你,一共是有2^32^个地址可以存储
- 每个地址标识一个字节,那我们就可以计算出(2^32^Byte = 4GB) 4G的空间进行编址
这里我们就明白:
- 在32位 的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储(1B = 8b),所以
一个指针变量的大小就应该是4个字节
- 那如果在64位 机器上,如果有64个地址线,那
一个指针变量的大小是8个字节
,才能存放一个地址
- 可以看到,若是我们将编译器放在64位系统上运行,最后的显示结果就为【8】,就可以验证我们上面的说法
【总结一下】:
- 指针变量是用来存放地址的,地址是唯一标识一个内存单元的
- 指针的大小在32位平台是4个字节,在64位平台是8个字节
二、指针的进一步理解
对指针有了初步的一个认识之后,接下去我们来进一步的认识指针有什么用?它存在的意义究竟是什么?
1、指针和指针类型
- 我们都知道,变量有不同的类型,整型,浮点型等。那指针有没有类型呢?
------> 准确的说:有的
- 在上一小节我介绍了指针可以存放其对应数据类型变量的地址,那对于指针本身来说这个类型究竟意味着什么呢?可以看到下面有各种各样不同类型的指针👇
c
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
这里可以看到,指针的定义方式是: [type + *]
- 那有同学一定会想,既然==指针里面存放的都是变量的地址,而且每个指针的大小都是4个字节==。那为何不把指针定义为一个统一的标准呢?就像【宏定义】一样定义出这个指针,完全不需要去考虑它需要存放什么变量类型的地址
- 例如就把指针统一地定义为【ptr】就会非常方便
这一点现在还说不清楚,当你看完指针存在的意义时就会明白这一切了👇
2、指针的解引用
然后先来说说有关指针解引用的问题
- 一样看到下面这段代码,指针pc里面存放的是ch的地址,指针pa里面存放的是a的地址,在上面我只讲到了指针和变量之间地址的关系,但是没有说到
地址和值
的关系 - 那对于一个值来说它是存放在这块地址上的,既然我的这个指针存放了变量的地址,那可不可以访问到这块地址中存放着的内容呢❓
c
char ch = 'c';
int a = 10;
char* pc = &ch;
int* pa = &a;
- 此时就可以使用到
[*]
解引用这个操作符了,便可以取到这块地址中所存放的内容,可以看到与其存放的变量中的值都是一样的
c
printf("ch = %c\n", ch);
printf("*pc = %c\n", *pc);
printf("a = %d\n", a);
printf("*pa = %d\n", *pa);
- 我们知道赋值运算符,若是一个变量的值你不想要了,那就可以修改这个变量的值,那既然指针可以访问到这个变量的值,可以不可以修改呢❓
- 也是一样,可以通过
[*]
解引用的方式就可以做到
c
*pc = 'd';
*pa = 20;
- 通过运行结果就可以看到里面的值确实做了修改
上面只是带你进一步了解了指针的更多作用,下面我们要真正地深入指针的挖掘,理解指针存在的意义了
⭐指针存在的意义1:访问字节的范围
好,我们来看如何去展示不同类型的指针究竟有什么它们各自存在的意义
- 首先我定义了一个变量a为它放入了一个值,要注意这个值它不是一个地址,而是一个十六进制的值,将其转换为二进制就可以发现刚好为32位,那一个整型变量可存放的数据大小也为4个字节32位
- 然后将这个变量的地址存放到一个指针中去,那这个指针存放的便是一个整型数据的大小,此时它就是一个【整型指针】。然后我通过解引用获取到了这块地址中所存放的内容,现在要去做一个修改
c
int a = 0x11223344;
int* pa = &a;
*pa = 0;
- 此时我们就可以通过内存块的变化来看看究竟
解引用
修改值后内存中是如何变化的👈
相信通过上面这幅图一定是非常清晰了
- 但是我为了要验证不同的指针类型究竟有什么不同的意义,所以我便将这个==整型变量的地址存放到一个字符型的指针中去,然后再去修改这个整型变量的值==,看看会发生什么变化
c
int a = 0x11223344;
char* pc = &a;
*pc = 0;
- 可以看到,只改变了一个字节,也就是8个比特位的长度,我这里一行显示的4个字节,在内存中是一行显示一个字节的
【总结一下】:==指针类型 决定了指针在进行解引用操作的时候能访问几个字节【权限有多大】==
-
char*
的指针,解引用访问1个字节 -
int*
的指针,解引用访问4个字节 -
double*
的指针,解引用访问8个字节
⭐指针存在的意义2:类型决定步长
除此之外,不同类型指针存放的意义就是它们移动的步长不一样
- 通过下面的代码可以看到,两个不同类型的指针都接受了整型变量的地址,我们知道指针是可以进行偏移的,那使这两个指针都向后偏移1会发生怎样的变化呢?
c
int a = 10;
int* pa = &a;
char* pc = &a;
printf("pa = %p\n", pa);
printf("pa + 1= %p\n\n", pa + 1);
printf("pc = %p\n", pc);
printf("pc + 1= %p\n", pc + 1);
- 很明显可以看出,对于整型指针来说,
+1
会向后偏移4个字节;而对于字符型指针来说+1
会向后偏移1个字节。这其实就可以看出不同指针其实还是有着它们的不同意义
【总结一下】:==指针类型 决定了指针的步长(向前 / 向后走一步都多大距离) ==
-
char*
的指针 + 1【跳过一个字符型,也就是向后走1个字节】 -
short*
的指针 + 1【跳过一个短整型,也就是向后走2个字节】 -
int*
的指针 + 1【跳过一个整型,也就是向后走4个字节】 -
double*
的指针 + 1【跳过一个浮点型,也就是向后走8个字节】
小练习:初始化数组
通过了解了上面有关不同指针类型的概念之后,相信你对指针一定能够有了一个自己的理解,接下去我们来做一个习题练练手
- 现在我初始化了一个数组,大小为10,首先对其所有元素初始化为0。拿一个指针变量去接收一下这个数组的首元素的地址,这样来看就可以通过这个指针访问到这个数组了
c
int arr[10] = { 0 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; ++i)
{
*p = i + 1;
p++;
}
- 通过一个循环去遍历这个数组的大小,然后使用
*
解引用便可以访问到当前循环遍历的那个元素,就可以利用循环变量做一个初始化了,然后p++
每次让指针向后偏移一个元素,便可以初始化完所有的数组元素了
相信我这么说你还有点懵,没关系,可以通过画图来分析一下
- 首先看到指针p指向数组的首元素地址,此时使用
*p
便可以访问到这块地址上的元素,即可以做修改,此时【i = 0】,i + 1
便是1,所以这块地址中的内容会被初始化为1,接着p++
,接着p++
跳过4个字节的大小,因为它是一个整型指针,便来到arr[1]
这块空间的地址
- 同理,来到数组第二个元素的地址后,依旧可以通过
*p
访问到这块地址中的内容做一个修改,然后指针p会向后移动4个字节的大小,刚好跳过一个数组元素,因为这是一个整型数组,数组中的每一个元素都是整型的
- 最后,当【i】遍历完了整个数组的大小之后也通过这个指针初始化完了数组的所有元素,此时指针也移动到了数组最后一个元素地址
- 来看一下运行结果
- 当然,为了简洁方便,我们也可以将初始化元素和指针的后移动同时进行,也就是下面这种写法
c
for (int i = 0; i < sz; ++i)
{
*(p + i) = i + 1;
}
p + i
访问到了让当前数组位置所在地址,然后通过*
解引用便访问到了这块地址所在内容,然后一样的【i + 1】便可以进行一个修改,将其放在循环里就可以使得每次i
的值在变化的同时带动指针的偏移,最后也是可以完成数组的初始化
三、野指针
概念: 野指针就是指针指向的位置是==不可知==的(随机的、不正确的、没有明确限制的)
1、野指针成因
对于野指针相信大家在使用指针的时候都会遇到,可能也有同学听说过它是一个很危险的东西,而且在写代码的时候一不小心就使一个指针变成了野指针,接下去我将出现野指针的情况做一个罗列👇
① 指针未初始化
- 首先第一种就是这个指针未初始化的情况,也就是你定义了整型指针,但是呢并没有在系统中为其分配一块空间,此时这个指针p就指向了内存中一块随机的地址,此时这个指针就叫做【野指针】
- 然后在这个时候又使用
*p
访问到了这块地址中的内容,并对其做一个修改,那么此时就会出现问题
c
int main(void)
{
int* p;
*p = 20;
return 0;
}
② 指针越界访问
- 第二种情况就是指针越界访问,用我们刚才那个关于初始化数组的小练习,此时我在遍历这个数组的时候在边界多访问了一次,那此时就会造成一个越界访问
- 若是指针p访问arr数组内的地址是没有问题的,因为这些地址是操作系统已经分配给我们的,但若是多访问一个位置的话其实这块地址就是一个随机的地址,那这个指针也就成了【野指针】
c
int main(void)
{
int arr[10] = { 0 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i <= sz; ++i)
{
*(p + i) = i + 1;
}
}
- 可以看到编译器报出了错误❌
③ 指针指向的空间释放
- 其实这可以联系我们的生活实际,比如说我在酒店里开了一间房,房间的房号叫302,此时就想要叫我的好兄弟明天也一起来住(doge),于是就告诉了他在XX酒店XX房间号。但是呢我只付了一个晚上的钱,到了第二天早上便只好退房了。
- 但是到了第二天张三却真的拿了他的行李箱过来住了,可以呢酒店前天说这间房已经退了不可以住了,不过张三执意要住,可是这间房呢已经退换回去了,还给酒店了,张三没有了使用权,此时他的这个行为就可以被称为是非法访问
- 还有另外一种空间释放指的是在堆区动态申请内容后释放,要使用到
free()
,这一块就不在这里讲了,后面介绍到动态内存规划的章节再做细讲,如果想了解的可以看看我的这篇【链表】文章 ------> 带你从浅入深真正搞懂链表
2、如何规避野指针
知道了会产生野指针的情况,那我们就要针对这些情况去做一些风险规避
1. 指针初始化
- 这一块很简单,只要是定义了一个指针,那就千万别忘了对它进行一个初始化,无论是让其保存一个地址或者是置为空都可以
- 其实可以把野指针看作是一条野狗,若是让这个指针保存一个地址也就是有个主人管住它了,那也就不会产生危险;将其置为空其实就使用链子把它拴起来了,也不会有问题
c
int a = 10;
int* pa = &a;
char* pc = NULL;
2. 小心指针越界
- 这一块的话自己小心和注意一点就行
3. 指针指向空间释放,及时置NULL
- 这个我在前面说起过,对于从堆区中动态申请的一块空间现在要将其释放了,也就是还给操作系统,但是呢你初始化后的指针还是指向堆区中的这块地址,只是它被释放了而已,此时就要让这个指针指向空(也就是NULL),这样就可以防止随意操作一块随机地址的风险了
4. 避免返回局部变量的地址
- 这一点我们上面也看到过了,若是返回一个在函数中创建的局部变量,此时外界虽然是接受到了这个变量的地址,但是这个变量的作用域只是在这个函数内部,除了作用域就销毁了,若是外界有一个指针接受了这个随机的地址,然后再去操作它就非常危险了🈲
- 这一点可以看C++引用章节的传引用返回部分 ,有细说到为何不能返回局部变量
5. 指针使用之前检查有效性
- 如果你还是担心自己的程序会出现野指针的问题,那么就要在操作一个指针的时候检查一下这个指针是否合法,也就是像下面这样在操作这个指针pa的时候判断一个它是否为空
c
int a = 10;
int* pa = &a;
if (pa != NULL)
{
printf("%d\n", *pa);
}
- 可以看到,若是不对其进行一个判断,然后这个指针又是一个空指针的话,就会造成一个很大的问题
- 当然,如果你觉得这样写条件判断比较麻烦的话也是有其他简便的办法的,就是使用
assert()
进行一个断言。这种方式的话就比较粗暴一些了,直接给你弹出一个警告框
c
int main(void)
{
int a = 10;
int* pa = NULL;
assert(pa);
printf("%d\n", *pa);
return 0;
}
- 也就是像下面这样,还会告诉具体哪行出现了错误,当然也就是你写断言的那样
四、指针运算
在了解了许多有关指针的基本知识和指针的使用技巧后,我们就要使用指针去做一些运算的工作,一起来看看
1、指针与整数的运算
- 首先通过下面这段程序来看看指针和整数之间的运算
- 首先是定义了一个float类型的数组,然后定义了一个
float
类型的指针。不过在一开始定义出来的时候没有进行一个初始化。我们循环内部对其进行了一个初始化,首先让其指向这个数组的首元素地址,再通过for循环去遍历这个数组 - 主要来看的就是就是循环的内部这个指针是如何变化的,
*vp++
这个表达式有两个操作符,一个是[*]解引用
操作符,一个则是[++]递增
操作符,如果你对操作符优先级了解的话可以知道【++】是比【*】的优先级来得高的,所以它会先进行一个运算,可是呢可以看出这是一个后置++,所以这个表达式所操作的还是==vp当前所指向的这块地址==。那么解引用取到的就是当前这块地址所存放的内容,可以看到右边是将其修改为0
c
#define N_VALUES 5
int main(void)
{
float values[N_VALUES];
float* vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
}
- 程序的思路和我们上面那个小练习初始化数组是一样的,这是这里的运算表达式稍微复杂一些而已
- 到最后初始化完成后也就是指向这一块地址
- 接着通过打印这个数组来看看是否初始化完了这个数组中的所有元素
2、指针的关系运算
- 好,我们继续来看指针的关系运算。与上一段类似,所以一些初始化的代码就不给出了
c
#define N_VALUES 5
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
- 可以看到,在这个for循环中,指针vp首先是指向数组的最后一个元素的后一个位置,那有同学问到这不是指针访问越界了吗?【野指针】!!!!
- 不要激动:metal:其实这不算是越界访问,而只能说是【越界指向】,这个指针就是指向了一下这块地址,但是呢并没有对这块地址进行任何的操作,那也就不会有危险。
- 其实对于野指针来说最大的危险就是一个指针指向了一块没有被操作系统分配过的、随机的地址,而且还去访问、修改这块地址中的内容
举个生活中的小案例:若是某一天你在银行:bank:前面溜达、经过一下,但是呢银行的保安说你是来抢银行的,那这个时候你一定不乐意了。那此时就可以将自己想作是那个指针,然后银行就是那个随机的地址,你就是看了看这块随机的地址,但是并没有去动它,是不会存在危害滴!
好,题外话,我们回归代码
- 来解释一下
*--vp
是什么意思,前置- -代表的就是让这个指针先前移一个位置,也就是让它从越界的那个位置回到存放最后一个元素所在的位置,此时也会不会造成越界访问了,然后再使用*
解引用操作符访问到这个地址的内容,同样进行一个初始化 - 此时数组就被初始化好了 👉可是呢,还是有同学会觉得这样去写代码不是很直观,毕竟前置- -的这个代码阅读性并不是很高,因此就将数组的初始化修改成了下面这样
c
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
- 这么看起来的话其实就非常直观了,大家应该是都可以很轻松地看懂,指针从数组的最后一个位置开始遍历,直到遍历到第一个元素的地址为止,指针的偏移也放到了for循环中,而不是放在循环体的表达式里
- 但是呢这样的判断会使得指针vp最后偏移到了
数组的最前端
,也会产生一个越界的情况
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
【标准规定】:
- 允许指向数组元素的指针与指向数组最后一个元素后面 的那个内存位置 的指针比较,但是不允许与指向第一个元素之前 的那个内存位置的指针进行比较。
- OK,我知道你已经晕了,来解释一下,其实很好理解,就是对我们上面讲过的有关指针运算的两道题目做一个总结罢了。一句话来说就是【
指针可以越界指向数组最后一个元素后面的那个位置,但是不可以指向第一个元素前面的那个位置
】,不要问我为什么,因为人家标准就是这么规定的,你就不要越界访问那个位置就可以了
3、指针与指针的运算
接下去我们来说说有关指针和指针之间的运算,题目的情景我们之前在讲函数递归的时候有说起过
- 首先我们来做个引入,请你算算下面这段代码最后输出的结果为多少
c
int main(void)
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
- 答案是9,你算对了吗❓开始我们有讲到过【地址】其实就是【指针】,那么对于两个地址之间的差值其实就是两个指针之间的距离 ,简单点说那也就是
&arr[9]
自&arr[0]
偏移了9个位置,所以它们之间的元素个数就是【9】
- 不过两个指针的相减,需要它们指向同一块连续内存空间,像下面这种情况就是不对的,因为int类型的变量和char类型的变量在内存中不是连续存放的,它们在内存中的距离是不确定的,是随机的
c
int a = 10;
char ch = 'c';
printf("%d\n", &a - &ch);
好,接下去来看看指针与指针之间的运算
- 这里是要去求解一个字符串的长度,我们可以使用自带的库函数
strlen()
、自定义函数变量累加、递归,在本文中,我还要再介绍一种方法,也就是使用【指针】 - 思路很简单,函数形参接受了一个数组的首元素地址,在内部拿一个字符型指针接受一下,然后通过这个字符型指针去遍历这个字符串,我们知道对于一个字符串来说以
\0
作为结束的标志 ,因此只需要每次解引用判断是否遍历到\0
即可 - 最后当遍历到字符串结尾的时候将末尾的指针 与形参接受的首元素地址,也就是指向首元素地址的指针,进行一个相减,就可以获取到这个字符串的长度了
c
int my_strlen(char* str)
{
char* pc = str;
while (*pc != '\0')
{
pc++;
}
return pc - str;
}
五、指针和数组
关于指针聊了这么久,接下去我们看看指针和数组之间有什么联系,如果想了解数组相关知识的,可以看看这篇文章------> 窥探数组设计的种种陷阱
我们看一个栗子🌰
- 在数组章节就有讲起过,数组名就是首元素地址,验证一下将它们的地址都打印出来可以发现是相同的
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
- 因此我们就可以将数组名作为数组的首元素地址给到一个指针,通过这个指针就可以去遍历这个数组,因为
arr[i]
和*(p + i)
都可以访问到数组中下标为i这个元素,因此&arr[i]
和p + i
所访问的地址也是一样的
c
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
- 既然
&arr[i]
和p + i
所访问的地址也是一样的,那我们就可以通过*(p + i)
去访问这个数组中的所有元素
✒【总结一下】
- 数组和指针不是一个东西,数组能够存放一组数,是一个连续的空间,它的大小取决于
数组元素的类型和个数
;而指针则是用来存放地址,它的大小取决于你当前编译器所运行的环境
,是32位 / 64位 - 数组和指针的联系在于:数组名是首元素地址,把数组名给到一个指针变量之后,可以通过这个指针变量来访问这个数组中的所有元素
六、二级指针
好,看完了上面的这些内部,你对指针的一些基础算是入门了,接下去我们来做一些提升,学习一下二级指针🗡
1、变量、指针、二级指针
- 通过上面的学习可以知道,对于一个指针变量来说,可以接受一个变量的地址,那么这个指针其实就叫做【一级指针】,那我们知道对于一级指针来说也是有地址的,我也带你看过了,那现在我想做的一件事就是
把一个一级指针的地址存放到一个变量里
- 下面这个
pp
就是一个二级指针,它存放有一级指针p的地址
c
int a = 10;
int* p = &a;
int** pp = &p;
- 现在我通过下面这张图为你做一个讲解。首先变量a的值为10,地址为
0x00befd90
,然后一级指着变量p存放了变量a的地址,所以可以说它指向a。对于变量p来说,它也有自己的地址,为0x00befd84
,二级指针变量pp里则是存放了这个地址,所以可以说它指向p
- 然后来解释一下有关指针变量前面的一些星号:对于变量p,它前面的
*
表示它是一个指针变量,【int】则表示它保存的一个int
类型变量的地址,就说它指向一个整型变量 - 对于变量pp,它前面的
*
表示它是一个指针变量,【int *】则表示它保存的一个int*
类型变量的地址,就说它指向一个指针变量
👉万不可以把二级指针理解为就是前面两个星号这么简单,要将指针和地址之间的关系联系起来
肯定还有同学没有理解,我通过一个生活小案例来说明一下
- 其实【指针与地址】的关系就和【钥匙🔑与保险箱】的关系是一个道理,就比如说这个二级指针pp,它就是一把钥匙,它里面存放了一级指针p的地址,所以可以顺利地找p这个箱子,这样的话就可以通过这把钥匙打开这个箱子
- 但是这个箱子里面呢存有一把小钥匙,也就是一级指针p,它里面呢存放有变量a的地址,通过这把钥匙你就可以找到变量a,然后便可以通过新找到的这把钥匙打开变量a这个箱子,里面就有一个数值为10,它就是你要找的那个宝藏【看过极限挑战 的读者应该都明白】
2、有关二级指针的运算
对二级指针有了一个初步的了解之后,我们来看看有关二级指针的一些运算
📚通过对二级指针进行一次解引用
,可以获取到一级指针变量,重新改变指向
📚通过对二级指针进行两次解引用
,可以获取到一级指针所存放的变量,重新改变值
- 看到下面这段代码,我新定义了一个变量b,然后可以看到通过
*pp
对二级指针进行了一个解引用的操作,这就获取到了一级指针变量p,此时我将变量b的地址存放到它里面去,这也就改变了指针p的指向
c
int a = 10;
int b = 20;
int* p = &a;
int** pp = &p;
*pp = &b;
- 我们可以通过DeBug调试来看看【上面是变化前,下面是变化后】
- 可以观测到,一级指针变量p存放的地址以及值都发生了改变,而且二级指针pp里所存放的一级指针变量p也发生了变化。这就是一次解引用可以实现的操作
接下去我们来看看通过两次解引用可以做到什么
- 看到如下代码,我通过两次解引用获取到了变量a的值,然后对其做了一个修改
c
int a = 10;
int* p = &a;
int** pp = &p;
printf("a = %d\n", a);
**pp = 200;
printf("a = %d\n", a);
- 通过运行结果就可以看出a的值确实发生了变化,执行前和执行后的值有所不同
- 通过DeBug调试也是可以看出因为pp里面存放有p的地址,而p里面又存放有a的地址,我们可以将这个二次解引用做一个分解。第一次解引用首先找到一级指针p,然后再进行一个解引用便找到了变量a,此时就有了一个【修改的权限】
通过以上的叙述,相信你对二级指针有了一个初步的认识和理解,在之后的【指针进阶 · 提升篇】 中我还会再详解二级指针
这里补充一点,上面说到只要是指针均为4个字节,那对于二级指针来说呢?也是4个字节吗?
- 可以看到,的确如此,无论是一级指针还是二级指针,其大小并不取决于指针本身的类型,而是取决于当前这段代码所处的平台,在32为平台下均为4个字节,在64位平台下均为8个字节