0?wx_fmt=jpeg


内容简介

1、第一部分第十二课:指针一出,谁与争锋

2、第一部分第十三课预告:第一部分小测验



指针一出,谁与争锋


上一课《【C++探索之旅】第一部分第十一课:小练习,猜单词》中,我们用一个小游戏来总结了之前几课学习的知识点。


现在,终于来到第一部分的最后一个知识点了,也是C++的基础部分的最后一个讲题。之后进入第二部分,就会开始面向对象之旅。因此,这一课也注定不平凡。系好安全带吧,因为马力要加足了!


指针这个C系语言的难点(著名的C语言里也有指针),令无数英雄"尽折腰",也是这个系列课程里最难的课之一。不过不用怕,只要耐心地跟着小编走,哪能不湿鞋... 噢,不是,是肯定能治好您的"腰间盘突出"!


只要腰间盘一康复,你会发现,很多其他的知识点都会变得异常清晰和容易。而且,腰不酸了,腿不疼了,一口气都能上五楼了... 小编你够了...


指针在C++的程序中被广泛使用。在之前的课程中我们已经在使用了,只是你没意识到罢了。只要学好了指针,对于内存的掌控水准就又上了一个台阶。


好了,不吊胃口了。我们出发吧~



内存地址的问题


你是否还记得之前讲到内存的那一课?也就是《【C++探索之旅】第一部分第四课:内存,变量和引用》。希望你能够再温习一下那一课,特别是那几张内存的图示。


那一课中说到,当我们声明一个变量时,操作系统会把内存的一小块空间借给我们使用,并且给这一块空间起一个名字(变量的名字)。就好像借给我们一个抽屉使用,并且在抽屉外面贴一个标签。例如:


int main()
{
    int userAge(16);
    return 0;
}


我们可以用内存图示来说明上面的代码:


0?wx_fmt=jpeg


上图既简单又清楚,不是吗?不过,上图简化了不少事情。


你会慢慢发现,在电脑中,所有都是井然有序,符合逻辑的。我们之前把内存中的地址空间比喻为一个个的抽屉,而这些抽屉上会被贴标签(如果被分配给了变量的话),也就是变量的名字。实际的情况可比这个复杂。


电脑的内存确实是由多个"抽屉"组成的,在这一点上我们之前并没有说错。而且,现代的电脑的内存里有多达数十亿个"抽屉"!


因此,我们需要一个体系来帮助我们在茫茫抽屉海中找到心仪的那个抽屉。内存中的每一个抽屉都有一个独一无二的编号,也就是内存地址。如下图所示:


0?wx_fmt=jpeg


上图中,我们看到了在内存中的那些"抽屉",每个抽屉对应一个内存地址。


我们之前的程序只使用了其中的一个"抽屉",它的内存地址是53768,这个抽屉里存放了我们的变量userAge。


注意:每一个变量都有唯一的内存地址,每个内存地址上一次也只能存放一个变量。


在我们之前的课程中,为了访问一个变量,我们使用变量的名称,例如:


userAge = 18; // 用变量名来访问变量,对变量重新赋值


但是,我们也可以用变量的地址作为访问变量的媒介。例如,我们可以对电脑"说":乖,给我显示内存地址53768上的内容。


也许你会说:既然有了第一种用变量名来访问变量的方法,既简单又有效率,那为什么还要用第二种方法呢?


确实,不过我们马上会看到,有时候利用内存地址来访问变量的方法是必要的。


在这之前,我们先来学习如何得知变量的内存地址。


显示内存地址


在C++中,和C语言一样,用于获取一个变量的内存地址,我们需要用到&这个符号。&也被称为"取地址符"。


假如我想要获取变量userAge的内存地址,很简单,我只要这样写:


&userAge


写一个程序试一下:


#include <iostream>
using namespace std;

int main()
{
   int userAge(16);
   
   cout << "变量userAge的内存地址是 " << &userAge << endl;
   
   return 0;
}


运行后,在我的电脑上显示的是:


变量userAge的内存地址是 0x22ff0c


0?wx_fmt=jpeg


你运行自己的程序时,显示的内存地址一般来说和我的不一样,毕竟这个内存地址是操作系统分配的。


虽然上面显示的内存地址中包含了字母,但其实它是一个数字。


因为内存地址打印出来的时候默认是以十六进制的格式来显示的(开头的0x表示后面的数字是十六进制的,因此其实是十六进制数22ff0c)。我们很熟悉十进制,也就是我们一般用的数制,电脑最底层其实只知道0和1组成的二进制。


不过十进制,二进制,十六进制之间都是可以相互转换的。至于如何转换,一般学校里都教过了。如果没有,那百度一下也不是难事。或者你用电脑里自带的计算器程序就可以实现进制间的转换了。


例如,上面的这个十六进制的数(0x22ff0c),转化成十进制是2293516,转换成二进制是1000101111111100001100 。


我们的&符号,在这里是用于显示变量地址。之前讲到引用时,我们也用到了&符号。因此,不要搞混了两种用法。


接下来,我们就来看看拿这些地址能干什么。



指针,指哪打哪


内存地址其实就是以数字来表示的。


我们已经知道,在C++中,有多种数据类型可以储存数字:int,unsigned int,double,等。因此,我们可以将内存地址储存在变量中吗?


回答是肯定的。


不过,要储存内存地址,我们不使用之前见过的那些普通变量类型,而要用一种特殊的类型:指针。


指针,简而言之就是:储存别的变量的内存地址的一种变量。


请记住这句话。


声明一个指针


声明一个指针,就和以前我们声明一个普通变量是类似的,需要两样东西:


  • 类型

  • 名字


至于名字,就和以前一样,只要符合规则,随便你取什么名字都行。


不过,指针的类型却有些特别。这个类型须要指明我们要存储的地址上的变量的类型,还要加上一个*号。例如:


int *pointer;


看上去有点特别是吗?比之前我们声明一个int型的变量时多加了一个星号(*)。正是这个*号标明了这是一个指针变量。


上面的代码声明了一个指针,名字是pointer,其内容是int型变量的内存地址。也可以说成:pointer指向一个int型的变量(之所以叫"指针"的原因)。


我们也可以写成:


int* pointer;


这次我们让*和int相邻,之前我们是将*紧邻pointer来写的。这两种写法的效果是一样的,但是我们推荐前一种。


为什么呢?因为如果是写成int* pointer这样的形式,很容易让我们认为int*是一个整体,如果我们在一行上同时声明多个指针,很容易就会这么写:


int* pointer1, pointer2, pointer3;


我们的初衷是:声明三个指针变量pointer1, pointer2, pointer3,都指向int型变量。但事实上,只有pointer1成功地被声明为了指针变量,后面的pointer2和pointer3只是int型变量!


这是初学需要注意的。因此,我们一般这么写:


int *pointer1, *pointer2, *pointer3;   // 声明三个指针变量


同样地,我们可以声明指向其他变量类型的指针。例如:


double *pointerA;
//声明一个指针,其中可以储存double类型的变量的地址

unsigned int *pointerB;
//声明一个指针,其中可以储存unsigned int类型的变量的地址

string *pointerC;
//声明一个指针,其中可以储存string类型的变量的地址

vector<int> *pointerD;
//声明一个指针,其中可以储存vector<int>类型的变量的地址

int const *pointerE;
//声明一个指针,其中可以储存int const类型的变量的地址


注意:指针是一种变量类型,这种变量类型在每一个操作系统上的大小是固定的,就好像int型,double型这样。不要认为指针可以储存int类型的变量的地址,这个指针就是int型指针,这样的说法是不准确的。


你可以测试一下,用sizeof操作符来获取指针类型所占的字节数。


例如:


cout << "指针的大小是 " << sizeof(pointer) << endl;


在我的电脑上,打印的结果是


指针的大小是 4


也就是说指针的大小是4个字节,是不随其所指向的内存地址上的变量的类型而改变的。


暂时,上面那些指针还只是声明了,没有被赋值。


这是很危险的!


因为此时指针里面包含的可以是任意的内存地址。假如你使用这样未赋值的指针的话,你并不知道自己在操作内存中的哪块地址。有可能这块内存地址上保存着极为重要的数据。


因此,永远记得:声明指针之后,在使用前一定要先对其赋值。


因此,一个不错的习惯就是声明的同时给指针赋初值,就算是初始化啦。通常习惯赋0,例如:


int *pointer(0);

double *pointerA(0); 

unsigned int *pointerB(0);

string *pointerC(0);

vector<int> *pointerD(0);

int const *pointerE(0);


还记得这一课的开始处我们给的一幅内存图吗?内存的第一个可用的抽屉的标号是1而不是0,地址为0的内存空间一般不可用。


因此,当我们为一个指针变量赋初始值0时,意味着它不指向任何内存地址。就好像用一根缰绳把一匹会乱跑的悍马栓在0这个木桩上("套马的汉子,你威武雄壮...")。


因此,假如你声明一个指针变量时还未决定让其指向什么地址,那么给其赋初值0是很必要的。


储存一个内存地址


现在我们已经会声明指针变量了,接下来要学习的就是如何把另一个变量的内存地址储存到这个指针变量中。


我们已经知道,要获得变量的内存地址,需要用到&符号。那么就很简单了,例如:


int main()
{    
    int userAge(16);    //一个int型的变量
    
    int *ptr(0);    //一个指针变量,其中可以储存一个int型变量的内存地址    
    
    ptr = &userAge;    //把int型变量userAge的地址存放到ptr这个指针变量里    
    
    return 0;
}


以上程序中,最关键的一句就是


ptr = &userAge;


执行完这句指令之后,指针变量ptr里面的内容就变成了userAge这个变量的地址,我们说:ptr指向userAge。


用一张内存图示来说明:


0?wx_fmt=jpeg


上图中,我们见到了我们的老朋友userAge,此变量存放在内存地址53768处,变量的值是16。


新增的内容当然就是指针啦。在内存地址14566上(当然这些内存地址都是举个例子)存放着一个指针变量,名字是ptr,注意看:ptr的值就是53768,也就是我们的userAge的地址。


好了,现在你差不多理解了吧。


当然了,也许你还是有疑问:为什么要把一个变量的内存地址存放到另一个变量里呢?有什么好处呢?


相信我,你会"守得云开见月明"的。


假如你理解了上图的话,那么是时候深入学习咯。


显示内存地址


指针也是一种变量类型,因此,我们可以显示其值。


写个程序:


#include <iostream>
using namespace std;

int main()
{
    int userAge(16);
    int *ptr(0);
    
    ptr = &userAge;
    
    cout << "变量userAge的内存地址是 " << &userAge << endl;
    cout << "指针变量ptr的值是 " << ptr << endl;
    
    return 0;
}


运行以上程序,显示:


变量userAge的内存地址是 0x22ff0c

指针变量ptr的值是 0x22ff0c


0?wx_fmt=jpeg


看到了吗?我们的userAge变量的内存地址和指针变量ptr的值是一样的。


访问指针指向的内容


还记得指针的作用吗?它使我们可以不通过变量名就访问一个变量。


那么怎么做呢?需要使用*号,它可以获取指针指向的变量的值。


例如:


int main()
{
   int userAge(16);
   int *ptr(0);  
   
   ptr= &userAge;
    
   cout << "指针变量ptr所指向的变量的值是  " << *ptr << endl; 
     
   return 0;
}


程序执行到 cout << *ptr 时依次做了以下的操作:


  1. 找到名字是ptr的内存地址空间

  2. 读取ptr中的内容

  3. "跟着"指针指向的地址(也就是ptr的值),找到所在地址的内存空间

  4. 读取其中的内容(变量userAge的值)

  5. 打印这个值(此处是16)


此处我们又一次使用了星号(*),术语称为:"解引用"一个指针。


还记得之前我们用星号来声明指针变量吗?因此,同一个符号在不同情况下作用也许不一样。


符号小结


我承认,这些符号是有点让人头晕。目前为止,星号(*)有两个作用,而且&在这一课之前是用于引用的。


这可不能怪我,你们说对吧,要怪也得怪C++之父。


好吧,我们来小结一下:


假如我们有如下代码:


int number = 16;
int *pointer = &number;


那么:


对于int型变量 number 来说


  • number:number的值

  • &number:number的内存地址


对于指针 pointer 来说


  • pointer:指针的值,也就是其所指向的另一个变量的内存地址。也就是number的地址,即&number

  • *pointer:指针所指向的内存的值,也就是number的值


如果不太理解,可以画一些图来帮助掌握。



动态分配


你想要知道指针到底可以用来做什么吗?那好,我们先来学习第一种用法:动态分配。


内存的自动管理


在我们以前关于变量的课程里,我们已经学过:当变量被定义时,大致说来程序其实做了两步:


  1. 程序请求操作系统分配给它一块内存空间。用专业术语说就是:内存分配。

  2. 用一个数值来填充这块内存空间,用术语说就是:变量的初始化。


上面的两个步骤是自动完成的,程序会替我们打理。而且,当我们的程序执行到函数的末尾时,就会把操作系统分配的内存空间自动归还。专业术语叫做:内存释放。在这种情况下,内存释放也是自动的。


我们现在就来学习不是自动的方式,也就是手动的。是的,聪明如你应该猜到了,我们要使用指针。


分配内存空间


为了手动申请内存空间,我们需要使用运算符new。


new在英语中时"新的"的意思。


new会申请一个内存空间,如果成功,则返回指向这块内存空间的指针。所以嘛,就轮到我们指针上场啦。例如:


int *pointer(0);
pointer = new int;


上面的两行程序中的第二行向操作系统申请一块内存地址,为了储存int型变量。这块内存地址的地址值将储存在pointer这个指针里。原理如下图所示:



0?wx_fmt=jpeg


从上图中可以看到,我们一共使用了两个内存空间:


  • 第一块内存空间的地址是14563,其中存放的是一个还没被赋初值的int型变量,而且此变量也没有名字。

  • 第二块内存空间的地址是53771,其中存放的是我们的指针pointer,指针的值是14563。


记得:在内存地址14563上的变量是没有变量名的,只能通过指针pointer来访问。


因此,如果我们修改指针pointer的值,我们就失去了唯一访问那块没有标签的内存的机会。你将不能再使用那块内存,也不能删掉它。这块内存就好像迷失了一般,不过又占用着内存,术语称为:内存泄漏。因此必须当心!


一旦手动分配成功,这个变量就和平时的变量一样使用。只不过我们是通过指向这个变量的指针来操作它的,因为我们并没有给这个变量起名字。需要用到解引用(*)。


int *pointer(0);
pointer = new int;
*pointer = 2;  //通过指针访问内存,以改写其中的内容


那块没有标签的内存空间(相当于没有变量名的变量)现在被填充了数值,是2。因此,内存里的情况如下:


0?wx_fmt=jpeg


使用完此内存地址后,我们需要向操作系统归还这块内存地址(指针指向的那块内存地址)。


释放内存


我们用delete运算符来释放内存。


delete在英语中是"删除,清除"的意思。例如:


int *pointer(0);
pointer = new int;

delete pointer;  //释放内存。注意:是释放了指针指向的那块内存


执行上面的操作后,指针pointer所指向的那块内存就被归还给操作系统了。不过,内存却还一直存在,而且还是指向那块内存,不过,我们没有权利再使用这个指针了。如下图所示:


0?wx_fmt=jpeg


上图还是比较形象的。如果我们循着指针的所指的箭头找去,我们会达到一块已经不属于我们的内存。因此我们不能再使用这块内存了。


"伊人已嫁,吾将何去何从?何以解忧,唯有稀粥"


因此,为了避免我们之后又误操作这块内存,须要在delete操作之后,再删去这个指向已不属于我们的内存的箭头。聪慧如你应该想到了,就是将指针的值置为0(无效的内存地址)。如果没有置0这一步,那么程序往往会奔溃,即使一时没奔溃,也是存在隐患的。


int *pointer(0);
pointer = new int;

delete pointer;    //释放内存
pointer = 0;       //把指针指向一个无效的地址


记得:手动分配了内存之后,使用完一定要释放这块内存。不然,你的内存会越来越少。一旦内存不够用了,你的程序就会奔溃。


一个完整的例子


我们用一个完整的例子来结束这一小节吧:询问用户的年龄,并借助指针来显示年龄


#include <iostream>
using namespace std;

int main()
{   
    int* pointer(0);   
    pointer = new int;   
    
    cout << "您的年龄是 ? ";   
    cin >> *pointer;   
    //借助指针变量pointer,我们改写pointer指向的内存地址上的内容
    
    cout << "您 " << *pointer << " 岁了." << endl;
    //再次使用pointer指针
    
    delete pointer;   //别忘记释放内存   
    pointer = 0;   //并且把指针指向一个无效的地址   
    
    return 0;
}


通过这个例子,我们掌握了如何通过指针进行内存的分配和释放。


之后,我们会学习使用Qt来开发图形界面的程序。我们会经常使用new和delete这对组合,例如在创建窗体和销毁窗体的时候。



到底啥时用指针好呢?


到了这一课的最后一小节,我们需要解释一下:何时使用指针呢?


通常在以下三种情况下应该使用指针:


  • 想要自己控制内存空间的分配和释放

  • 在多个代码块中共享一个变量

  • 在多个元素间选择


其他的情况,我们可以不必使用指针。


第一种情况我们已经学习了。来看看后两种吧。


共享一个变量


对于指针的这种用法,暂时我还不给你完整的代码示例。当之后第二部分讲到面向对象的编程时,我们自然就会有实例了。不过,我会给出一个更加视觉化的例子。


你玩过策略游戏吗?如果没玩过,也许听说过吧。


举个例子,这类游戏中有一个很著名的游戏:Warcraft III,暴雪公司的作品。截图如下:


0?wx_fmt=jpeg


要编写这样一个大型的RPG游戏,是非常复杂的。我们此处不讨论游戏,而是借此来思考一下一些用例。


在上图中,我们可以看到红色的人族和蓝色的兽族在交战。游戏中的每一个角色都有一个***目标。


例如,兽族的几乎所有兵力都在***被暗影猎手变成小动物的山丘之王(就是鼠标点中的那个"小动物")。人族的兵力在***剑圣。


那么,在C++中,如何指明红色的人族的***目标呢?当然了,暂时你还不知道怎么用代码实现,但是你应该有点主意了吧?回想一下这一课的主题。


是的,就是用指针。游戏中每一个角色都有一个指针,指向他们***的目标。这样,每个角色才会知道在某一刻该***谁。例如,我们可以写这样的代码:


Personage *target;  //指向***目标的一个指针


当双方没有交战之前,target这个指针指向地址0,也就是没有***目标。一旦两军兵刃相接,target指针就指向***的目标,而且随着游戏进行,target指向的目标是可以切换的。


因此,在这样的游戏中,一个指针就可以将一个角色和他的***目标联系起来了。


之后我们会学习怎么写这样的代码。而且在第二部分我们也可以写一个小型的角色扮演游戏(RPG游戏)。


在多个元素间选择


指针的第三种用途是可以依据用户的不同选择来作出不同的反应。


举个例子,我们给用户出一个多项选择题,有三个选项。一旦用户选择答案之后,我们用指针来显示他选择了哪个答案。代码如下:


#include <iostream>
#include <string>
using namespace std;

int main()
{    
    string responseA, responseB, responseC;    
    
    responseA = "幽闭空间恐惧症";    
    responseB = "老年痴呆症";    
    responseC = "妄想症";    
    
    cout << "阿尔茨海默病是指什么 ? " << endl;  //提问题
    
    //显示答案
    cout << "A) " << responseA << endl;    
    cout << "B) " << responseB << endl;    
    cout << "C) " << responseC << endl;    
    
    char response;    
    cout << "您的答案是 (填写A, B 或 C) : ";    
    cin >> response; //提取用户的答案    
    
    string *userResponse(0); //指向所选答案的指针    
    
    switch(response)    
    {    
        case 'A':        
            userResponse = &responseA;        
            break;    
        case 'B':        
            userResponse = &responseB;        
            break;    
        case 'C':        
            userResponse = &responseC;        
            break;    
    }    
    
    //用指针来显示我们选择的答案    
    cout << "您选择了答案 " << *userResponse << endl;    
    
    return 0;
}


当然了,指针还有很多用处,以后我们会慢慢学习的。



总结


  1. 每一个变量是存储在内存的不同地址空间上。

  2. 每一个地址上一次只能存放一个变量。

  3. 可以借助&符号来获得一个变量的地址,用法是:&variable

  4. 指针是一种特殊的变量类型,这种变量中存储的是另一个变量的地址。

  5. 一个指针是这样声明的:int *pointer; (这种情况下这个指针指向的是一个int型的变量)

  6. 如果我们打印一个指针的内容,那么得到的是其中储存的内存地址。如果我们用*符号来获得指针指向的内存(*pointer),那么打印的是指针指向的地址上存放的变量值:

  7. 我们可以手动申请一块内存地址,用new关键字。如果用了new关键字进行了所谓动态内存分配,之后不用此变量了,我们需要用delete关键字来手动释放这块内存(因为不会被自动释放)。

  8. 初学指针可能会觉得比较难理解。不过不必担心,大可再读一遍这一课。读书百遍,其义自见。不过也要配合练习。不用怕,以后我们会经常使用指针,所以你会慢慢熟练的。



第一部分第十三课预告


今天的课就到这里,一起加油吧!

下一课我们学习:第一部分小测验