前言
指针是一个令很多人都很痛苦的内容,然而指针其实没有大家想象中的那么复杂。
对计算机来说内存就是一切,如果非要我说出编程中最重要的一件事,我可能会说是内存。
当你编写了一段程序并启动它时,所有的程序都被载入到内存中,指令告诉计算机在你写的代码中要做什么。所有这些都被加载到内存中,CPU 就是这样访问你的程序并执行它的指令的。
当你创建一个变量,并从磁盘中读取数据时,所有的这些都存储在内存中,如果没有内存就什么也做不了。而指针对于 管理 和 操纵内存 非常重要。
01 指针的概念
其实,指针是一个整数,一种存储内存地址的数字,指针的值就是一串数字,而这数字其实就是一个地址,仅此而已
内存它就像一条很长的直线而不是一大块。内存就像我们现实世界中的一条街,这一条街有开始也有结束,就像一根线一样,线上就是一堆房子, 没有房子是横穿街道的,假设只有这一条街道,一排的房子,我们现在把这个比喻用在电脑上,它只是一条线性的线,在这条直线上的每一所房子都有一个号码和一个空间,号码就是地址,空间是一个字节,我们显然需要一种方法来寻址所有的 byte 来定位我们这条街上所有的房子。
例如,假设某人在网上订了东西想要送货上门,他需要被送到正确的房子里,或者可能有人把东西从他们的房子里取出去,无论哪种操作,你需要能够从这些房子的内存字节中读写,指针就是这些地址,这些地址告诉我们房子在哪里,这是非常重要的,因为我们在代码中所做的几乎所有的事情都是在从内存中读写,当然你完全有可能写一个不使用指针的 C++ 程序,你完全可以这样做。然而指针是非常有用的工具,正如我刚才提到的,内存可能是你拥有的最重要的东西,是计算机可以提供的很重要的资源,它可以被用于做几乎所有的事情,能够对内存有更多的控制至关重要。
再次重申,一个指针只是一个地址,它是一个保存内存地址得到整数虚构
02 第一个指针
我们来创建一个空指针,void 的意思是无类型。
千万记住,一个指针只是一个地址,它只是一个在内存中保存地址的整数,它不需要类型,如果我们给指针一个类型,我们只是说,这个地址的数据,被假设为我们给的类型,除此之外它没有任何意义,它只是一些我们在实际的源代码可以编写的东西,使我们的生活在语法层面上更容易,为了让我们的生活更轻松。
我们当然可以使用指针类型,不过类型不会改变一个指针的实质,——指针只是一个内存地址,它是一个整数。
所以 void 指针 意味看我们现在不关心我们的代码中这个类型是什么类型的,因为我们只想保存一个地址。
我把它称为 pdata,其值设置为0。0 是什么意思?我们给这个指针的内存地址是 0,这是什么意思?
0 实际上不是一个有效的内存地址,内存地址不会一直到 0,这是无效的,这意味着这个指针是无效的,无效指针是完全可以接受的状态,但我要说的是 0 不是一个有效的内存地址,我们不能从内存地址 0 中读取或写入数据,如果我们尝试这样做的话,程序会崩溃,所以 0 意味着没有。
我们也可以这样写。
这种写法实际上是一个 #define,你把鼠标悬浮在上面可以看到,是 #define NULL 0,和我们用 0 是一样的。或者我们也可以用 C++ 关键字 nullptr,这个会在 C++11 里面介绍。
好的,我们设置了我们的第一个指针,它是无类型的,它的内存地址是 0,一点用处没有,但它可能是你能写的最简单一个指针,这可以让我们做一些更有用的事情。
对上面的代码我们做一些解释。
我们创造一个整数变量,当然我们创建的每个变量都有一个内存地址,因为我们需要一个地方来存储这个变量。
如果我想知道这个变量的内存地址,我可以通过使用 & 运算符来做到这一点,如果我在一个已经存在的变量前面加上一个 & 符号,我们实际上是在问这个变量,嘿,你的内存地址是什么?
我们取这个变量的内存地址,把它赋值给指针变量 pdata。
我们现在有了变量 num的内存地址,我们把它存储在另一个变量中。
我们设置一个断点调试一下。
我们可以看变量的值,当代码执行到下面两句时,在下图左框2pdata的值为0x7fffffffdd0c,其实这是一个整形数,不过这个数字代表着存放变量num的值,所以我们查看0x7fffffffdd0c这个地址时,你会发现这个地址的值是num变量的值
int num = 9;
pdata = #
将 void 修改为 int 并运行,你会发现实际上没有改变任何变化。(自己去试一下)
Ubuntu 1.84.2Visual Studio Code 下载配置与vscode查看内存Hex Editor插件,简单易懂-CSDN博客
如果你的VScode还不能查看内存,请看上篇博文
03 如何访问指针保存的地址
我们再做一下修改。
假设我想使用我的数据,我有一个指针指向那个数据,现在我想要写入或读取数据,我该如何操作呢?换句话说,我们知道数据在哪里,但是我怎么能访问它呢,这就要靠逆向引用了(指针的 * 运算符通常被称为 dereference 运算符)。
我们有变量 num,指针 pdata指向 num,但是我怎么才能回到这个 num呢? 你可以通过在指针前面插入一个星号来实现这一点,换句话说,我实际上是在逆向引用那个指针,这意味着我现在可以访问我可以读取或写入数据的数据。我们试着这样做一下。
04 表达式必须是指向完整对象类型的指针C/C++(8
因为我们说过这个指针是一个空指针,也就是说,计算机怎么可能将这个值写入到一个 void 指针,它不知道那是什么,这个 10 是 short 类型吗?——两个字节的整数,是 int 类型吗?——四个字节的整数,是 long long 类型吗?—— 8 个字节的整数,它不知道这需要多少字节的数据,我们刚刚说它是 10,但是 10 可以代表任何东西,这个时候就需要类型了,我们需要告诉编译器,这是一个整数,所以是 4 个字节,我们修改一下。
当然,是我们告诉编译器,这是一个整数,编译器自己并不知道这是不是正确的,如果我们犯了错,比如我们说这实际是一个 double,那程序的运行可能就有点麻烦了。
好吧,通过写代码的时候,逆向引用 *指针,我可以访问这个数据,这个例子中,我写入这个数据。
简单地说,我们不知道指针有多大,我们不知道指针指向的数据多大,因为指针并不包含数据,一个指针就是一个整数,它是一个内存地址,就是这样。
05 不能将 "int *" 类型的值分配到 "short *" 类型的实体
当我将指针pdata的类型改为short *时,编译器会报错,因为不能将 "int *" 类型的值分配到 "short *" 类型的实体,如果我不想改变指针的类型的话,有没有办法可以解决这个问题,答案是可以的,将num的类型直接强转就行
这样就可以解决
06 new & delete
到目前为止,我们一直在栈上直接创建数据,如果我们像上面的例子一样操作,那就是在栈中创建变量(之后我们会讲到栈和堆的内容)。
如果我想在堆上创建一个变量,或许我可以问我们的电脑,嘿,我想让你给我分配一些内存,我想有一定的尺寸(比如 8 个字节),我会这样做。
#include <iostream>
#include <string.h>
int main()
{
int *pdata = NULL;
int num = 9;
pdata =#
*pdata = 10;
char *buf= new char[8];
memset(buf, 1, 8);
return 0;
}
上面的代码给我们分配了 8 个字节的内存,并返回一个指向那块内存开始位置的指针,然后我可以使用 memset 的函数,它可以用我们指定的数据填充一段内存块。
memset 接收一个指针,这个指针将会是内存块的开始的指针,然后是将要填入的值,比如 0,最后是应该填入多少字节,我们要 8 个字节。
运行这个程序。在内存视窗可以看到 buffer 位置的连续 8 个字节都为1 。
这个例子中,我们使用了新的关键字 new 申请了堆内存,当我们完成它后,我们也应该删除数据。
我们可以通过键入 delete 完成删除,我们知道它是一个数组,我们使用数组来分配堆内存,所以我们应该使用 delete[ ] 来删除 buf。
这个例子再次强调了,这个指针,我们分配 8 个 char,1 个 char 是一个字节,这样我们就分配了 8 个字节, 我们用来存储数据的指针指向了数据的开头。
07 指针的指针
还有一点我想说的是指针本身是变量,这些变量也存储在内存中,这意味着我们可以得到双指针或三指针,意思可以有指向指针的的指针。这一切可以如何运作呢,好吧,你只要往下一层想,我现在有一个指针指向我的指针,于是我有了一个变量 a 来存储内存地址,它指向另一个变量 b,变量存储变量 c 的内存地址。就这么简单。
在 buffer 的例子中,我们可以创建一个双指针试一下。
#include <iostream>
#include <string.h>
int main()
{
int *pdata = NULL;
int num = 9;
pdata =#
int **pdata1 = &pdata;
return 0;
}
我回到指针上来,再次强调,它只是存储内存地址的整数