C++ 入门学习经验 06——指针(二):解引用、空指针和野指针到底怎么理解
大家好啊!这里是 阳阳的博客 ,一个正在努力学习技术的大学生。
上一篇我们聊了指针的上半部分,主要说了指针到底是什么、地址可以怎么理解,以及为什么很多同学一看到指针就会有点害怕。
那今天这篇,我们就接着把指针这个话题往下聊一聊。
先看个例子:
int a = 10;
int* p = &a;
cout << p << endl;
cout << *p << endl;
这里的 p 是什么? *p 又是什么? &a 是什么? 为什么有时候 * 是定义指针,有时候又像是在取值?
刚开始看确实容易绕,因为同一个符号在不同地方看起来像有不同意思。如果老师讲得快一点,自己又没多敲几遍,就很容易出现一种感觉:课上好像听懂了,自己写的时候大脑直接空白。
所以这篇文章咱们就先把几个最容易让新手混乱的地方讲清楚:指针变量、解引用、空指针、野指针,以及指针和普通变量之间到底是什么关系。
好了,废话不多说,咱们直接开始。
一、先把这三个东西分清楚:变量、地址、指针
我们先来看一段最基础的代码:
int a = 10;
int* p = &a;
这两行代码看着很短,但里面其实有三个很重要的东西:
a 普通变量,里面放的是 10
&a a 这个变量在内存里的地址
p 指针变量,里面放的是 a 的地址
这里我觉得可以用一个生活里的例子来理解。
假设有一个房间,房间里面住着一个人。
a就像这个房间本身,里面放着一个值10
&a就像这个房间的门牌号
p就像一张纸条,纸条上写着这个房间的门牌号
所以指针本身不是那个房间,它只是保存了房间地址的一张“纸条”。
很多同学一开始容易把 p 和 a 混在一起,觉得 p 好像就是 a。其实不是。
a 里面放的是数字 10。 p 里面放的是地址。 只不过这个地址指向了 a。
我们可以看代码:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int* p = &a;
cout << a << endl;
cout << &a << endl;
cout << p << endl;
return 0;
}
这里输出的结果大概会是这样:
10
0x61ff08
0x61ff08
当然,每个人电脑上地址不一定一样,这个不用纠结。重点是:
&a 和 p 输出出来通常是一样的
因为 p 里面存的就是 a 的地址。
这里大家可以先记一句比较简单的话:
普通变量装的是值,指针变量装的是地址。
只要这句话能先记住,后面很多东西就没那么乱了。
二、*p 到底是什么意思?
接下来就是比较容易卡住的地方:*p。
还是这段代码:
int a = 10;
int* p = &a;
如果我们写:
cout << p << endl;
输出的是地址。
如果我们写:
cout << *p << endl;
输出的是 10。
那 *p 到底是什么意思呢?
我自己的理解是:
p 是门牌号
*p 是根据这个门牌号找到房间,然后看看房间里住着什么
也就是说,p 只是地址,而 *p 是通过这个地址找到对应的变量,再访问里面的值。
这个过程就叫 解引用 。
这个名字听起来有点官方,刚开始不用太纠结它的术语。你可以先理解成:
通过指针找到它指向的那个东西。
比如:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int* p = &a;
cout << p << endl; // 输出 a 的地址
cout << *p << endl; // 输出 a 的值
return 0;
}
这里 p 和 *p 的区别非常重要:
p :地址
*p :地址对应位置里的值
很多同学刚学的时候会写懵,尤其是看到这个:
*p = 20;
心里可能会想:这是什么意思?是把 p 改成 20 吗?
其实不是。
*p = 20; 的意思是:通过 p 这个地址,找到它指向的那个变量,然后把那个变量改成 20。
比如:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int* p = &a;
*p = 20;
cout << a << endl;
return 0;
}
这段代码输出什么?
答案是:
20
为什么?
因为 p 指向的是 a,所以 *p = 20; 实际上就是把 a 改成了 20。
可以这样理解:
*p = 20;
约等于:把 p 指向的那个变量改成 20。
这里我觉得需要注意一下:不是 p 变成了 20,而是 p 指向的那个变量变成了 20。
这两个差别特别大。
三、如何 区分int* p 和 *p 呢?
这里还有一个需要理解的地方:
int* p = &a;
和:
*p = 20;
这两个地方都有 *,但是意思好像不一样。
其实确实不完全一样。
在定义变量的时候:
int* p;
这里的 * 是在说明:p 是一个指针变量。
你可以把它理解成:
我要定义一个 int 类型的指针 p。
而在使用变量的时候:
*p
这里的 * 是在解引用,表示:
访问 p 指向的那个变量。
所以我们可以简单分成两种情况:
定义时:int* p; 表示 p 是指针
使用时:*p 表示访问 p 指向的内容
刚开始学的时候,大家可以不要一上来就追求特别严谨的语言,可以先用这种方式区分:
左边带类型的时候,多半是在定义指针。
没有带类型、单独使用 *p 的时候,多半是在通过指针取值或改值。
比如:
int* p = &a; // 定义一个指针 p,让它指向 a
cout << *p; // 输出 p 指向的内容
*p = 30; // 修改 p 指向的内容
这样一对比,其实会清楚很多。
大家多敲几遍类似的代码,慢慢就会顺起来。
四、空指针:暂时不指向任何有效位置
再往下,我们会遇到一个词: 空指针 。
一般我们会这样写:
int* p = nullptr;
或者有些老代码里会看到:
int* p = NULL;
现在更推荐用 nullptr。
那空指针是什么意思呢?
简单来说:
这个指针现在没有指向任何有效的变量。
还是用门牌号的例子。
如果普通指针像一张纸条,上面写着某个房间的门牌号,那么空指针就像一张纸条,上面明确写着:
我现在没有指向任何房间。
这样做的好处是:至少我们知道它现在是“空的”。
比如:
int* p = nullptr;
这比下面这种写法要安全很多:
int* p;
为什么呢?
因为:
int* p;
只是定义了一个指针变量,但没有给它一个明确的地址。这个时候它里面可能是一个乱七八糟的地址。
这就像你拿到一张纸条,上面不知道原来写了什么奇怪门牌号。你要是根据这个门牌号去找房间,就很危险。
所以我觉得可以这样:
int* p = nullptr;
暂时不知道指向谁,就先让它等于 nullptr。
后面要用的时候,再让它指向一个明确的变量:
int a = 10;
p = &a;
这里还要注意一点:空指针不能直接解引用。
比如:
int* p = nullptr;
cout << *p << endl;
这就是很危险的写法。
因为 p 根本没有指向有效变量,你还要通过 *p 去访问它指向的内容,那程序很可能直接崩掉。
所以在使用指针前,可以先判断一下:
if (p != nullptr) {
cout << *p << endl;
}
刚开始写代码时,不一定每次都能想到这么严谨,但至少要知道:空指针可以存在,但不要直接 *p。
五、野指针:不应该存在的指针
什么是野指针?
简单理解就是:
指针里面保存了一个不确定的、无效的、或者已经不该访问的地址。
比如最常见的一种情况:
int* p;
cout << *p << endl;
这个 p 没有初始化,它里面到底是什么地址,我们不知道。这个时候直接 *p,就像拿着一张不知道哪里来的门牌号,硬要去开别人家的门。
结果可能是:
程序崩溃
输出奇怪的值
还有一种情况也容易造成野指针,比如指向的东西已经不存在了。这个对初学者来说可能暂时遇得少一点,后面学动态内存的时候会更明显。
现阶段大家先记住一个比较实用的原则:
指针定义时尽量初始化。
不知道指向谁,就先写 nullptr。
不要随便解引用一个不知道指向哪里的指针。
比如不要这样:
int* p;
*p = 10;
而应该这样:
int a = 0;
int* p = &a;
*p = 10;
这里 p 明确指向了 a,所以 *p = 10; 才有意义。
六、指针和普通变量的关系:不是复制值,而是找到原件
我们前面在值传递那一篇里提过,函数参数如果只是普通变量,很多时候传进去的是一份“复印件”。
比如:
void change(int x) {
x = 20;
}
int main() {
int a = 10;
change(a);
cout << a << endl;
}
输出还是:
10
因为 change 函数里面改的是 x,不是外面的 a。
那指针为什么能修改外面的变量呢?
我们来看:
#include <iostream>
using namespace std;
void change(int* p) {
*p = 20;
}
int main() {
int a = 10;
change(&a);
cout << a << endl;
return 0;
}
这次输出是:
20
为什么?
因为我们传进去的是 a 的地址。
change(&a) 的意思是:把 a 的地址传给函数。
函数里面的 p 虽然也是一个局部变量,但它保存的是 a 的地址。所以当我们写:
*p = 20;
就是通过这个地址找到了外面的 a,然后把 a 改成了 20。
这里可以继续用复印件和原件来理解。
普通值传递像是:
你把文件复印了一份交给别人。
别人怎么改复印件,都不会影响你的原件。
指针传递像是:
你把原件所在的位置告诉了别人。
别人按照这个位置找到原件,就可以直接改原件。
所以指针的重点不只是“传了一个东西”,而是传了一个可以找到原变量的位置。
这也是为什么指针一方面很有用,另一方面也需要小心。
七、总结
指针这个东西,刚开始不要急着一次学完。今天这篇内容看起来不少,但其实核心就几句话。
普通变量装的是值,指针变量装的是地址。p 表示地址,*p 表示通过这个地址找到里面的值。int* p 是定义指针,*p 是使用指针访问内容。不知道指向谁的指针,先写 nullptr。不要解引用空指针,也不要解引用没初始化的野指针。指针能改外面的变量,是因为它拿到了变量的地址。
总之,这篇我们把指针下半部分几个最容易绕的点先捋了一遍:指针变量、解引用、空指针、野指针,还有指针为什么能修改外面的变量。
不用一开始就把所有东西都理解得特别透,先多敲、多试、多对比,慢慢就会顺起来。
希望这篇文章能帮你少走一点弯路,也能在遇到指针问题的时候心里更有底一点。
如果这篇文章对你有帮助,麻烦点赞、关注和收藏吧,谢谢! 😄
有什么问题或者想法,欢迎在评论区留言,我们一起交流!
我们下篇见~
更多推荐
所有评论(0)