大家好啊!这里是 阳阳的博客 ,一个正在努力学习技术的大学生。

上一篇我们聊了指针的上半部分,主要说了指针到底是什么、地址可以怎么理解,以及为什么很多同学一看到指针就会有点害怕。

那今天这篇,我们就接着把指针这个话题往下聊一聊。 

先看个例子:

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 就像一张纸条,纸条上写着这个房间的门牌号

所以指针本身不是那个房间,它只是保存了房间地址的一张“纸条”。

很多同学一开始容易把 pa 混在一起,觉得 p 好像就是 a。其实不是。

a 里面放的是数字 10p 里面放的是地址。 只不过这个地址指向了 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。不要解引用空指针,也不要解引用没初始化的野指针。指针能改外面的变量,是因为它拿到了变量的地址。

总之,这篇我们把指针下半部分几个最容易绕的点先捋了一遍:指针变量、解引用、空指针、野指针,还有指针为什么能修改外面的变量。

不用一开始就把所有东西都理解得特别透,先多敲、多试、多对比,慢慢就会顺起来。

希望这篇文章能帮你少走一点弯路,也能在遇到指针问题的时候心里更有底一点。

如果这篇文章对你有帮助,麻烦点赞、关注和收藏吧,谢谢! 😄

有什么问题或者想法,欢迎在评论区留言,我们一起交流!

我们下篇见~

更多推荐