C++指针

141 阅读15分钟

上一篇:C++控制流语句

下一篇:C++引用

1. 概述

本篇主要讨论的还是原始的指针,不是智能指针智能指针后续会继续深入。

计算机处理内存,对计算机来说,内存就是一切,当我们编写一个程序并启动它时,所有的程序都被加载到内存中,所有的指令在告诉计算机你写的代码要做什么,所有的这些都被加载到内存中。CPU就是这样访问你的程序并开始执行它的指令。当你创建一个变量,当你从磁盘中加载数据时,所有东西都存储在内存中。如果没有内存就什么也做不了,而指针对于管理和操纵内存非常重要。

指针是一个整数,一种存储内存地址的数字。这里暂时不讨论内存在计算机中是如何工作的,只是做个简单比喻。只要把你的内存放在电脑里面,就像一条长直线,就好比,一条街,有开始有结束,线上就是一排房子,没有房子是横穿街道的。这里的假设是只有一条街,一排房子,这就是计算机中的内存,它只是一条线性的线,在这条街上的每一所房子都有一个号码和地址,把这个比喻用在电脑上。想象一下,这条直线上的每一栋房子都有一个地址,这个地址是一个字节,是一个字节的数据。显然,我们需要一种方法来寻址所有的字节byte,也就是我们这条街上所有的房子的地址。例如,假设某人在网上订了东西,要送货上门,这货物要被送到正确的房子里,或者可能有人把东西从他们房子里送出去。无论哪种方式,你需要能够从这些房子的内存字节中读写。因此,指针就是这些地址,这些地址告诉我们房子在哪里。这是非常重要的,因为我们在代码中做的几乎所有事情都是在从内存中读写。在现在的C++,你完全有可能写一个不使用指针的C++程序,然而,指针依然是非常有用的工具。正如我刚才提到的,内存可能是你拥有的最重要的东西,计算机可以提供的最重要的资源,可用于做所有的事情。所以,能够对内存有更多的控制是至关重要的。

一个指针只是一个地址,它是一个保存内存地址的整数。这就是所有,忘掉所谓的类型,类型与这些无关,类型只是我们为了让生活更容易而创作的某种虚构,这都不重要。如果你有一个int指针,或者你有一个实体类和一个实体指针,没关系,类型是完全无意义的,所有类型的指针都是保存内存地址的整数,它的本质就是保存内存地址的整数,就是这样。

2. 案例

下面,我们来看案例

1. 项目准备

准备一个简单的项目,项目中有一个main.cpp文件,文件内容如下

image.png

2. 开始案例

1. 创建一个空指针

接下来,我们要创建一个指针,创建一个最纯粹的指针,一个空的指针。void*void是意思是无类型。

记住,我说过,一个指针就是一个地址,它只是一个在内存中保存地址的整数。它不需要类型,如果我们给指针一个类型,这只是在说,这个地址的数据,被假设为我们给的类型,除此之外,这个类型没有任何意义。类型只是一些我们在实际的源代码可以编写的东西,使我们的生活在语法层面上更容易,为了让我们的生活更加轻松。我们可以使用指针类型,然而类型不会改变一个指针的实质,实质就是指针只是一个保存内存地址的整数。

所以void指针void*,这意味着我们现在不关心我们的代码中,这个指针是什么类型的,因为我们只想保存一个地址。我们给它一个名称ptr,void* ptr,ptr,pointer的简写。我们给这个指针赋值为0,void* ptr = 0;

image.png

我们给这个指针的内存地址是0,这是什么意思?0实际上不是一个有效的内存地址,内存地址不会一直到0。0是无效的,这意味着这个指针是无效的。无效指针是完全可以接受的状态,但我要说的是0不是一个有效的内存地址,我们不能从内存地址0中读取或写入,如果我们尝试这样做的话,我们的程序就会崩溃,所以0意味着没有。

void* ptr = 0;也可以写成void* ptr = NULL;

image.png

鼠标悬停在NULL上可以看到

image.png

NULL实际上是一个#define NULL 0,和我们用0是一样的,或者我们可以用C++的关键字nullptr,这个会在C++11里面介绍。

image.png

到目前为止,我们写了第一个指针,它是无类型的,他的内存地址是0,完全无用的,但它是一个指针,这可能是我们能写的最简单的指针。

2. &运算符获取变量内存地址

接下来,让我们做一些更有用的事情

让我们创建一个整数,我要创建一个变量,我把它叫做var,并给它赋值为8,如下

image.png

当然,我们创建的每个变量都会占用内存,内存都有一个内存地址,因为我们需要一个地方来存储这个变量。如果我想知道这个变量的内存地址,你在内存里的什么地方,我们可以通过使用&运算符来做到这点。

如果我在一个已存在的变量前面加上一个&号,如&var,我们实际上是在问这个变量,你的内存地址是什么,当然,也就是我们取到这个变量的内存地址,接下来把它赋值给一个新的变量ptr,如下

image.png

我们现在有了变量的内存地址,且我们把他存储在另一个变量中,这个变量这是一个指针。

接下来,我们在std::cin.get();上添加一个断点,

image.png

F5运行程序

image.png

现在,看看我们得到了什么,我们可以看看变量的值,有两种方式可以直接在自动窗口中看到变量的值。

image.png

也可以通过鼠标悬停的方式查看。如果把鼠标悬停变量var上。

image.png

可以看到var的值是8。

如果我把鼠标悬停在ptr指针上

image.png

可以看到它的值稍有不同,以十六进制格式呈现给我们。如你所见0x0019febc,它仍然是一个数字,不过是以十六进制展示,它是一个整数。这个指针变量现在保存的是var变量的内存地址。也就是我们看到的数字0x0019febc

3. 内存视图,内存地址定位内存

然后我们复制它

image.png

紧接着,我们可以打开内存视图

image.png

image.png

这个内存视图现在显示的是应用程序中的所有内存。

我们将刚刚粘贴的内存地址放入地址栏中,当然,ptr = 要去掉,然后按下回车,如下

image.png

可以看到,我们被带到了这个内存地址。我们知道一个整数是4个字节的数据。

image.png

它的值是8,我们现在正在查看我们的计算机内存,我们可以看到,在这个内存地址有8这个值,因为我们创建了这个变量,我们把它的值设为8。

在基本层面上,以上就是指针的全部了。其他的一切都是建立在此基础之上。这就是指针的工作方式,指针是一个保存地址的变量,一个整数,指针就像其他变量一样。它不是保存变量a这样的值本身,它保存着一个内存地址,它的内存地址也是值,是一个整数。这个整数有多大,这个指针有多大,取决于很多东西,可能是32位整数,也可能是64位,也可能是16位,这并不重要。关键是它是一个整数。

4. 指针类型

回到代码

我们将void*指针改为int*指针,实际上我们没有改变任何东西。

image.png

F5再次运行我们的代码

image.png

image.png

复制ptr的值0x010ff878,输入到内存视图的地址栏回车

image.png

可以看到它仍然被设为8。

我们回到代码,我们可以将这个指针类型设置成一个完全不同的类型,比如double类型,如下。

image.png

当然,VS给了我们一个错误提示。

image.png

因为编译器会发出警告,不允许这样,我们可以进行以下转化。如下。

image.png

接下来,按下F5运行程序

image.png

image.png

复制ptr的值0x00b4f8c0到内存视图的地址栏按下回车

image.png

可以看到,回车后会把视图定位到内存中存储数据的地方。这里有四个字节,他们的值是8。

类型无关紧要,但类型对该内存的操作很有用。所以如果我想对它进行读写,类型可以帮助我们,因为编译器会检查。例如,一个整数int应该是4个字节,所以我要在那儿设置一个值,他会设置4个字节的内存。但最终类型是无关紧要的,在后续的篇章中,会更深入的研究它,但是现在,不用担心类型。

5. *运算符逆向引用

回到代码,我们恢复代码为空指针。

image.png

既然空指针这么好用,那为什么不用空指针呢?假设我们想使用我们的数据,我有一个指针指向那个数据。现在我们想要写入或读取这些数据。如,现在这个变量的值是8,我想改变这个变量的值,我该怎样改变呢?我知道数据在哪里,但是我怎么才能访问它呢?

这就需要靠逆向引用了(指针的 * 运算符通常被称为dereference运算符)。

我们有变量var,指针ptr指向var。但是我们怎么才能从这个指针ptr内存地址回到这个var变量内存的值呢

我们可以通过在指针前面插入一个*来实现这一点。如*ptr,我实际上是在逆向引用那个指针,这意味着我现在正在访问我可以读取或写入数据的数据。现在我们来修改值,如*ptr = 10;

image.png

可以看到VS给我们一个错误提示,为什么,因为我们说过这个指针是一个空指针void*,也就是说,计算机怎么可能将这个值写入到一个void指针,它不知道那是什么。这个10是short类型吗,2个字节的整数?是int型吗,4个字节的整数?是long long型吗,8个字节的整数?我们不知道需要多少字节的数据。我们刚刚说它是10,但是10可以代表很多东西。这个时候就需要类型了。我们需要告诉编译器,这个10是一个int型的整数,所以是4个字节。

那么,我们修改指针类型为int,如下

image.png

现在,我们告诉了编译器,这是一个int型的整数,这是我们告诉编译器的,编译器自己并不知道。

如果我们输出var的值

image.png

F5运行程序

image.png

可以看到打印的值是10。所以我们成功的把它从8改成了10。

如果我们在*ptr = 10;这行加个断点。

image.png

F5运行我们的程序

image.png

我们在自动窗口中鼠标右键长按ptr的值拖动到内存视图地址栏

image.png

image.png

可以看到定位到了ptr指针指向的内存位置。可以看到这个内存存储的是8。

我们按下F10

image.png

可以看到内存存储的值变成了0a0a在十六进制中就是10。

通过写代码的时候,逆向引用(*)一个指针,我们可以访问这个指针指向的数据。

在这个例子中,我写入了这个数据。所以现在你应该知道指针是如何工作的。这就是它的全部,指针只是指向内存中的一个位置,有些人说它指向一个内存块,这不是很准确,因为我们不知道这块内存有多大。在这个例子中,它是4个字节,因为我们创建了一个int整数,一个int型的整数是4字节的内存,所以我们确定知道这个指针指向的内存是4个字节。然而,在实际的指针中,并不知道内存多大,我们不知道指针指向的数据多大,因为指针并不包含数据,一个指针就是一个整数,一个内存地址。

到现在为止,我们一直在栈上直接创建数据。如果我们像int var = 8;这样创建数据,那就是在栈中创建它。

6. new关键字创建堆变量

我们可以在堆上创建一个变量,或者我可以问我们电脑分配一些内存,我想有一定的尺寸,我现在可以使用char*,我们知道char是一个字节,对吧。当我们写下这个char* buffer = new char[8];,我们真正要求的是8字节的内存。new char[8]给我们分配了8个字节的内存,并返回一个指向那块内存开始的指针buffer。然后我可以使用一个叫做memset的函数,这个函数可以用我们指定的数据填充一个内存块

memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值
该函数接收三个参数
第一个参数接收一个指针,指向要填充的内存块
第二个参数接收一个int类型的整数,是这块内存将要被设置的值
第三个部分接收一个int类型的整数,是这块内存将要被设置的大小,表示占用多少字节。

memset接收三个参数,第一个参数接收一个指针,这个指针将会将会是内存块开始的指针。第二个参数,是这块内存将要被设置的值,比如设置0。第三个部分,就是指定这块内存的大小,这种情况下应该填入多少字节。我们知道我们有8个字节,所以这样写memset(buffer, 0, 8);,如下

image.png

F5运行我们的程序。

image.png

鼠标悬停在buffer指针上

image.png

或看自动窗口

image.png

可以看到这里内存地址。

我们在内存视图中输入buffer指针

image.png

回车

image.png

可以看到,我们有8个字节的内存。当然,它们都被设为0。

这是因为我们使用了memset函数。在这个例子中,我们使用了新的关键字new来申请了堆内存,当我们完成它后,我们也应该删除数据。

7. delete关键字删除堆内存

我们可以通过delete关键字,我们知道,我们使用数组来分配的堆内存,所以我们应该使用delete[]来删除buffer。

image.png

1. 双指针/多指针

无论如何,这个指针buffer,我们分配了8个char,一个char是一个字节,这样我们就分配了8个字节。我们用来存储数据的指针指向了数据的开头。还有一点我想说的是,指针本身是变量,这些变量也存储在内存中,我们一个新的指针指向存储指针变量的内存,这样我们可以得到双指针或三指针,意思是指向指针的指针等。这一切是如何运作的呢,你只要往下一层想,我现在有一个指针指向我的另一个指针,现在我有一个变量来存储内存地址,也就是说这个变量是一个指针,它指向另一个变量,另一个变量也是存储的一个内存地址,它也是一个指针。

在我们的buffer例子中,我们可以创建一个双指针char**,这意味着这个指针被指向另一个指针,给它取名为ptr,然后将buffer的内存地址赋值给它,char** ptr = &buffer;

image.png

在删除buffer数据那行加上断点

image.png

按下F5运行程序

image.png

image.png

可以看到ptr的值0x00d3dfb8,将ptr的值复制内存视图的地址栏,回车

image.png

可以看到,这里的b8dfd300,我们知道这个指针有4个字节的内存,在32位程序中,现在的运行环境是x86,一个内存地址是32bit。b8dfd300因为电脑的字节存储次序(元组排列顺序)实际上顺序颠倒了,如果以这个b8dfd300为基础,这里我们要重排下,让它变成00d3dfb8,输入到内存视图的地址栏

image.png

回车

image.png

可以看到,我们被带到存储这一堆0的buffer的内存地址。

就是这样,指针的指针展示完了。我还是要回到这一点,指针只是存储内存地址的整数。

上一篇:C++控制流语句

下一篇:C++引用