本期我们将学习 C++ 中的指针。
指针是一个令很多人都很痛苦的内容,然而指针其实没有大家想象中的那么复杂。另外我先要说明本期我们要讨论的是原始的指针,还有一种常用的指针叫智能指针,这个我们在之后的内容中会接触学习。
计算机处理内存,对计算机来说内存就是一切,如果非要我说出编程中最重要的一件事,我可能会说是内存。
当你编写了一段程序并启动它时,所有的程序都被载入到内存中,指令告诉计算机在你写的代码中要做什么。所有这些都被加载到内存中,CPU 就是这样访问你的程序并执行它的指令的。
当你创建一个变量,并从磁盘中读取数据时,所有的这些都存储在内存中,如果没有内存就什么也做不了。而指针对于 管理 和 操纵内存 非常重要。
在接下来的内容中,可能会多次提到一句话,指针是一个整数,一种存储内存地址的数字,希望大家能牢记并彻底理解这句话。
本期我们不会深入讨论内存在计算机中是如何工作的,但是我们还是要有个大概的印象,就是只要把你的内存放在电脑里,它就像一条很长的直线而不是一大块。内存就像我们现实世界中的一条街,这一条街有开始也有结束,就像一根线一样,线上就是一堆房子, 没有房子是横穿街道的,假设只有这一条街道,一排的房子,我们现在把这个比喻用在电脑上,它只是一条线性的线,在这条直线上的每一所房子都有一个号码和一个空间,空间是一个字节,我们显然需要一种方法来寻址所有的 byte 来定位我们这条街上所有的房子。
例如,假设某人在网上订了东西想要送货上门,他需要被送到正确的房子里,或者可能有人把东西从他们的房子里取出去,无论哪种操作,你需要能够从这些房子的内存字节中读写,指针就是这些地址,这些地址告诉我们房子在哪里,这是非常重要的,因为我们在代码中所做的几乎所有的事情都是在从内存中读写,当然你完全有可能写一个不使用指针的 C++ 程序,你完全可以这样做。然而指针是非常有用的工具,正如我刚才提到的,内存可能是你拥有的最重要的东西,是计算机可以提供的很重要的资源,它可以被用于做几乎所有的事情,能够对内存有更多的控制至关重要。
再次重申,一个指针只是一个地址,它是一个保存内存地址得到整数,这就是所有内容了,忘掉所谓的类型,类型与这些无关,类型只是我们为了让生活更容易而创造的的某种虚构,这都不重要,让我们来看些例子。
第一个指针
我们来创建一个空指针,void 的意思是无类型。
千万记住,一个指针只是一个地址,它只是一个在内存中保存地址的整数,它不需要类型,如果我们给指针一个类型,我们只是说,这个地址的数据,被假设为我们给的类型,除此之外它没有任何意义,它只是一些我们在实际的源代码可以编写的东西,使我们的生活在语法层面上更容易,为了让我们的生活更轻松。
我们当然可以使用指针类型,不过类型不会改变一个指针的实质,——指针只是一个内存地址,它是一个整数。
所以 void 指针 意味看我们现在不关心我们的代码中这个类型是什么类型的,因为我们只想保存一个地址。
我把它称为 ptr( pointer 的简写),其值设置为0。0 是什么意思?我们给这个指针的内存地址是 0,这是什么意思?
0 实际上不是一个有效的内存地址,内存地址不会一直到 0,这是无效的,这意味着这个指针是无效的,无效指针是完全可以接受的状态,但我要说的是 0 不是一个有效的内存地址,我们不能从内存地址 0 中读取或写入数据,如果我们尝试这样做的话,程序会崩溃,所以 0 意味着没有。
我们也可以这样写。
这种写法实际上是一个 #define,你把鼠标悬浮在上面可以看到,是 #define NULL 0,和我们用 0 是一样的。或者我们也可以用 C++ 关键字 nullptr,这个会在 C++11 里面介绍。
好的,我们设置了我们的第一个指针,它是无类型的,它的内存地址是 0,一点用处没有,但它可能是你能写的最简单一个指针,这可以让我们做一些更有用的事情。
对上面的代码我们做一些解释。
我们创造一个整数变量,当然我们创建的每个变量都有一个内存地址,因为我们需要一个地方来存储这个变量。
如果我想知道这个变量的内存地址,我可以通过使用 & 运算符来做到这一点,如果我在一个已经存在的变量前面加上一个 & 符号,我们实际上是在问这个变量,嘿,你的内存地址是什么?
我们取这个变量的内存地址,把它赋值给新的变量 ptr。
我们现在有了变量 var 的内存地址,我们把它存储在另一个变量中。
我们设置一个断点调试一下。
我们可以看变量的值,如果把鼠标放在 var 上面,可以看到它的值是 8,如果我把鼠标悬停在 ptr 上面,我们可以看到一个十六进制的数字,如你所见,仍然只是一个数字,它是一个整数,这个指针变量现在保存的是 var 变量的内存地址。我可以复制它,打开内存视窗。
这个视图现在显示程序中的所有内存,粘贴地址到输入栏,然后去掉开始的那些信息,按下 enter 我们被带到这 个内存地址,我们可以看到在这个内存地址有 8 这个值。
[
在基本层面上,这就是它的全部,其他的一切都建立在此之上,这里没有什么魔法,这就是指针的工作方式,指针是一个保存地址的变量,就像其他变量一样,它不是保存变量 a 这样的值本身的值,它保存着一个内存地址,它的内存地址也是值,也是一个整数,这个整数有多大,以及这个指针有多大,取决于很多东西,可能是32 位整数,也可能是64 位,这并不重要,关键是,它是个整数。
回到我们的代码,将 void 修改为 int 并运行,你会发现实际上没有改变任何变化。
我们再做一下修改。
调试程序,你可以在内存视窗看到它仍然被设为 8,这里不同类型地址的赋值需要做一下转换。
虽然在以上例子可以看出类型无关紧要,但类型对该内存的操作很有用,如果我想对它进行读写,类型可以帮助我,因为编译器会知道。例如,一个整数应该是四个字节,所以我在那儿设置一个值,它会设置四个字节的内存。
让我们回到我们的空指针,所以空指针这么好用,为什么不用空指针呢?假设我想使用我的数据,我有一个指针指向那个数据,现在我想要写入或读取数据,我该如何操作呢?换句话说,我们知道数据在哪里,但是我怎么能访问它呢,这就要靠逆向引用了(指针的 * 运算符通常被称为 dereference 运算符)。
我们有变量 var,指针 ptr 指向 var,但是我怎么才能回到这个 var 呢? 你可以通过在指针前面插入一个星号来实现这一点,换句话说,我实际上是在逆向引用那个指针,这意味着我现在可以访问我可以读取或写入数据的数据。我们试着这样做一下。
运行后我们发现程序报错了,因为我们说过这个指针是一个空指针,也就是说,计算机怎么可能将这个值写入到一个 void 指针,它不知道那是什么,这个 10 是 short 类型吗?——两个字节的整数,是 int 类型吗?——四个字节的整数,是 long long 类型吗?—— 8 个字节的整数,它不知道这需要多少字节的数据,我们刚刚说它是 10,但是 10 可以代表任何东西,这个时候就需要类型了,我们需要告诉编译器,这是一个整数,所以是 4 个字节,我们修改一下。
当然,是我们告诉编译器,这是一个整数,编译器自己并不知道这是不是正确的,如果我们犯了错,比如我们说这实际是一个 double,那程序的运行可能就有点麻烦了。
好吧,通过写代码的时候,逆向引用 *指针,我可以访问这个数据,这个例子中,我写入这个数据。
你现在应该知道指针是如何工作的了,这就是它的全部,指针只是指向内存中的一个位置。有些人说它指向一个内存块,这样说不是很准确,因为我们不知道这块内存是多大,在这个例子中,它是 4 个字节,因为我们创建了一个整数,一个整数是 4 字节的内存,所以我们确实知道这个指针指向的内存是 4 个字节。然而在实际的指针中,并不知道内存多大,当我们创建数组时,它会跟踪内存大小,这个之后再讲。
简单地说,我们不知道指针有多大,我们不知道指针指向的数据多大,因为指针并不包含数据,一个指针就是一个整数,它是一个内存地址,就是这样。
到目前为止,我们一直在栈上直接创建数据,如果我们像上面的例子一样操作,那就是在栈中创建变量(之后我们会讲到栈和堆的内容)。
new & delete
如果我想在堆上创建一个变量,或许我可以问我们的电脑,嘿,我想让你给我分配一些内存,我想有一定的尺寸(比如 8 个字节),我会这样做。
上面的代码给我们分配了 8 个字节的内存,并返回一个指向那块内存开始位置的指针,然后我可以使用 memset 的函数,它可以用我们指定的数据填充一段内存块。
memset 接收一个指针,这个指针将会是内存块的开始的指针,然后是将要填入的值,比如 0,最后是应该填入多少字节,我们要 8 个字节。
运行这个程序。在内存视窗可以看到 buffer 位置的连续 8 个字节都为 0。
这个例子中,我们使用了新的关键字 new 申请了堆内存,当我们完成它后,我们也应该删除数据。
我们可以通过键入 delete 完成删除,我们知道它是一个数组,我们使用数组来分配堆内存,所以我们应该使用 delete[ ] 来删除 buffer。
这个例子再次强调了,这个指针,我们分配 8 个 char,1 个 char 是一个字节,这样我们就分配了 8 个字节, 我们用来存储数据的指针指向了数据的开头。
指针的指针
还有一点我想说的是指针本身是变量,这些变量也存储在内存中,这意味着我们可以得到双指针或三指针,意思可以有指向指针的的指针。这一切可以如何运作呢,好吧,你只要往下一层想,我现在有一个指针指向我的指针,于是我有了一个变量 a 来存储内存地址,它指向另一个变量 b,变量存储变量 c 的内存地址。就这么简单。
在 buffer 的例子中,我们可以创建一个双指针试一下。
设置一个断点进行调试。
就是这样 ,指针的指针,不是很难理解吧。
后话
回到指针上来,再次强调,它只是存储内存地址的整数,这就是他们应该在你脑子里的全部,后面我们我会讲更多关于指针处理的话题,处理算术和更高级的指针操作。
好了,本期内容就到这里,下期再见。