C++语言程序设计
上QQ阅读APP看书,第一时间看更新

2.4 指针

2.4.1 内存和地址

前面提到过,变量其实就是用来放置数值等内容的“盒子”,每个盒子都可以容纳数据,并通过一个编号来标识。盒子也有自己的地址,计算机要找到某个盒子,必须知道该盒子的地址。

计算机的内存是以字节为单位的一段连续的存储空间,每个字节单元都有一个唯一的编号,这个编号就称为内存的地址。接下来用一张图来展示机器中的一些内存位置,如图2.14所示。

图2.14 机器中的内存位置

图2.14中每个盒子的存储类型为字节,每个字节都包含了存储一个字符所需要的位数。在许多现代的机器上,每个字节包含8个位,可以存储无符号值0~255,或有符号值-128~127。图2.14中并没有显示这些位置的内容,但内存中的每个位置总是包含一些值。每个字节通过地址来标识,如图2.14中方框上面的数字所示。

为了存储更大的值,可以把两个或更多个字节合在一起作为一个更大的内存单位。例如,许多机器以字为单位存储整数,每个字一般由2个或4个字节组成。接下来用一张图来表示4个字节的内存位置,如图2.15所示。

图2.15 机器中4字节的内存位置

举一个例子,这次盒子里显示了内存中4个整数的内容,如图2.16所示。

图2.16 内存中存放了4个整数

这里显示了4个整数,每个整数都位于对应的盒子中。如果大家记住了一个值的存储地址,那么以后可以根据这个地址取得这个值。

但是,要记住所有这些地址实在太烦琐了,因此高级语言所提供的特性之一就是通过名字而不是地址来访问内存的位置。接下来使用名字来代替地址,如图2.17所示。

图2.17 用变量来存储整数

当然,这些名字就是变量。有一点非常重要,必须记住,名字与内存位置之间的关联并不是硬件提供的,它是由编译器为人们实现的。所有这些变量正是为了人们而提供的一种更方便用来记住地址的方法——硬件仍然通过地址访问内存位置。在C++语言中可以通过取地址运算符&来获取系统将某种数据存放在内存中的位置。

2.4.2 指针的定义与使用

在旅店住宿时,前台服务员通常会提供给客户一个房间号,然后客户通过房间号就可以很方便地找到自己预定的房间。在C++中,指针就起到房间号的作用,它指向另一个内存地址,这个地址对应的内存空间就是编程者真正需要操作的数据。

1. 指针的定义与初始化

指针的定义是使用一个特殊的符号∗来区别的,其语法格式如下:

     数据类型 *指针变量名;

具体示例如下:

上面示例中,p、p1、p2、str都是指针类型的变量,int、float、char是指针所指向内存空间中的数据类型,它决定了指针所指向的内存空间的大小,int∗、float∗、char∗是指针数据类型,用来定义p、p1、p2、str这样的指针变量。指针的值是另外一个变量的内存地址,如定义一个整型变量,将它的地址赋值给一个整型指针变量,具体示例如下:

     int a= 1;
     int *p=&a;

上述语句中,定义指针变量p时,初始化为&a,此时p的值就是a的地址,一般称为指针变量p指向变量a。由于指针变量的值是某个内存空间的首地址,而地址的长度都是一样的,因此指针变量所占的内存空间大小都是相同的。

2. 指针的赋值

由于指针也是一个变量,因此它的值是可以改变的,需要注意的是,在给指针赋值时,指针变量的值必须是与其类型相对应的内存地址,具体示例如下:

引入指针的目的是操作指针指向的内存空间,完成指针定义及初始化后,在指针变量前加∗,表示对指针所指的内存空间的引用,这时就可以通过指针间接地操作这块内存空间,需要注意此处∗与定义时的∗之间的区别。接下来演示指针的用法,如例2-7所示。

例2-7

运行结果如图2.18所示。

图2.18 例2-7运行结果

在例2-7中,定义了3个变量,其中a、b是整型变量,p是指向整型的指针。从运行结果可以看出,3个变量的内存地址是连在一起的,首先p的值是a的地址,∗p的值是a的值;接着使p指向b,p的值是b的地址,∗p就是b的值;最后给∗p赋值为4,b的值变为4。由此可见,对∗p操作,与对p指向的变量b做操作是一样的。

3. void指针

有一种特殊类型的指针,这种指针指向的数据类型是void。在C++中,void是空的意思,但void指针的含义是不确定类型的指针,简单理解为通用类型指针,即它可以指向任何数据类型的内存空间,具体示例如下:

     void *p;

上述语句仅表示p是一个指针,可以存放一个内存地址,但是这个内存地址所指的空间中存放的数据类型还不能确定,当需要使用这个指针时,可以通过对指针进行强制类型转换的方法确定其具体表示的类型。接下来演示void指针的使用,如例2-8所示。

例2-8

运行结果如图2.19所示。

图2.19 例2-8运行结果

在例2-8中,第7行通过强制类型转换将void∗类型指针转换为int∗型,再通过∗p输出指针指向的内存空间的值。同理,第8行通过强制类型转换将void∗类型指针转换为char∗型,再通过∗p输出指针指向的内存空间的值。

void指针经常用在能支持多种数据类型的数据操作函数中,读者在以后深入学习C++时需要理解这种指针的用法。

4. NULL指针值

当一个指针定义时没有为其初始化,且使用前也没有为其赋值,那么这个指针的值是随机的,它可以指向一个随机的内存,通常把这种指针称为野指针。在程序中如果不小心使用了野指针,很容易造成程序混乱。为避免这种情况发生,当一个指针不指向任何内存地址时,必须用NULL作为指针的值。在使用指针前,有时也需要判断一下指针是否为NULL,如果不是NULL,则该指针指向一个内存空间;如果是NULL,则该指针没有具体指向。具体示例如下:

2.4.3 指针与数组

一维数组名本质上说是一个地址常量,这个地址是一维数组中第一个元素的内存地址,因此可以将一维数组名赋值给一个指针变量,当指针指向一个一维数组时,指针也可以当成数组名使用。具体示例如下:

有了上面的定义,a和p就可以互换,如果要访问数组的第一个元素,a[0]、∗a、p[0]、∗p这4种方式都是正确的,p可以看成数组的名字a,访问数组的第i+1个元素,可以用a[i],也可以用p[i]。但p与a是有本质的区别的:a是地址常量,不能被赋予其他地址值;而p是变量,可以被重新赋予地址值。

C++中使用行指针来访问二维数组,设有如下定义:

     int a[3][4];

在形式上,可以将a[3]看成一个一维数组,在定义指针变量时可以将a[3]用∗p来替换,具体示例如下:

     int(*p)[4];

由于二维数组中定义的最高维(a[3])用来确定二维数组的行,因此把p称为行指针。接下来p的初值就可以用a来设定,具体示例如下:

     p=a;

这样就可以通过p来访问数组a中的元素了,如访问a[2][3],就可以通过p[2][3]、∗(p[i]+j)、∗(∗(p+i)+j)这3种方式访问。

使用行指针也可以访问多维数组的元素,读者可以根据二维数组的方式类似推出多维数组,此处不再赘述。

2.4.4 指针运算

指针的运算主要是指指针的移动,即通过指针递增、递减、加上或者减去某个整数值来移动指针指向的内存位置。此外,两个指针在有意义的情况下,还可以做关系运算,如比较运算。

指针变量的值实际上是内存中的地址,因此,一个指针加减整数相当于对内存地址进行加减,其结果依然是一个指针。然而,尽管内存地址是以字节为单位增长的,指针加减整数的单位却不是字节,而是指针指向数据类型的大小。具体示例如下:

     int a=0;
     int *p=&a;
     p++;

第3行将指针变量p存储的内存地址自加,由于p指向的是int型变量,因此执行自加操作会将原来的内存地址增加4个字节(此处是int型占用4个字节的系统)。接下来演示指针的加减运算,如例2-9所示。

例2-9

运行结果如图2.20所示。

图2.20 例2-9运行结果

在例2-9中,指针p中存储的是变量a的地址0X0018FF44,然后让p自减,此时p的值为0X0018FF40,从0X0018FF44到0X0018FF40需要移动4个字节,而这正好是int型变量所占用的内存字节数。同理,指针q中存储的是变量b的地址0X0018FF38,然后让q自加2,此时q的值为0X0018FF48,从0X0018FF38到0X0018FF48需要移动16个字节,而这正好是两个double型变量所占用的内存字节数。

2.4.5 动态内存管理

在C语言中,用于管理动态内存的方法主要是malloc()和free()函数,使用时需要注意很多细节,一不小心就会造成内存泄漏。在C++中,更常用的动态内存管理是new和delete操作符,它们一般配套使用,new表示从堆内存中申请一块空间,delete表示将申请的空间释放。

1. new申请内存

用new运算符申请堆内存空间有3种格式,其语法格式如下:

     new数据类型;
     new数据类型(初始值);
     new数据类型[常量表达式];

第一种格式是申请一个指定数据类型的内存空间,可以将该操作结果赋值给一个相应数据类型的指针变量,具体示例如下:

第二种格式是在申请空间的同时,为空间赋初值,具体示例如下:

第三种格式是动态申请数组空间,中括号内的常量表达式代表申请空间的大小,这个空间大小是以数组类型大小为单位,具体示例如下:

     int *p3=new int[10]; //申请一个可以存放10个整型数的数组,然后赋值给整型指针变量

因为系统资源是有限的,所以不是在任何情况下都能申请到足够的空间,如果申请失败,将返回NULL指针。由于内存操作失败是非常危险的,因此在申请内存时,一般需要在程序中判断是否申请成功,如果成功就继续执行程序;如果失败,立即抛出异常或直接结束程序。

2. delete释放内存

运算符delete用来释放new申请的内存,其语法格式如下:

     delete指针名;
     delete[]指针名;

当new使用前两种形式时,即申请的是一个数据类型空间时,对应的delete为第一种格式,即只释放指针所指的空间,具体示例如下:

当new使用第三种形式时,即申请的是动态数组,对应的delete为第二种格式,即释放数组的全部空间,具体示例如下:

     int *p3=new int[10];
     delete[]p3;

程序中一旦执行delete后,指针指向的可能是原来的值,也可能是其他的值,这取决于编译器对其处理的结果,因此出于对程序健壮性的考虑,一定要在使用delete后,将指针置为NULL。

接下来演示new与delete的用法,如例2-10所示。

例2-10

运行结果如图2.21所示。

在例2-10中,第3~7行定义了一个结构体Student,第14行通过new运算符申请内存,第15~19行申请内存失败返回-1,第32行释放申请的内存。

此外,使用new与delete时,还需注意以下几点:

图2.21 例2-10运行结果

  • new与delete必须配对使用,即用new为指针分配内存,使用完后,一定要用delete来释放已分配的内存空间。
  • NULL指针是不能释放的,即用new申请内存失败时,就不能再释放了。
  • 用new给指针变量分配一个有效指针后,必须用delete先释放,然后再用new重新分配或改变指向,否则先前分配的内存空间因无法被程序引用而造成内存泄漏。