C++面向对象程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 指针

2.3.1 指针概念的回顾

指针用于存放一个对象在内存中的地址,通过指针能够间接地操作这个对象。指针的典型用法是建立链接的数据结构,如树(tree)和链表(list),并管理在程序运行过程中动态分配的对象,或者用于函数参数,以便传递数组或大型的类对象。

对于类型T,T*是到T的指针,即一个类型为T*的变量能够保存一个类型为T的对象的地址。例如:

int i=10;     //L1
int *p;      //L2
p=&i;       //L3

语句L2定义了一个整型指针变量,它表示p能够存放一个整型变量的内存地址。L3将变量i的地址存放到指针变量p所代表的内存单元中,如下所示:

有人说,指针代表了两个变量:一个是指针变量本身,另一个是它所指向的变量。对于语句L2,就可以认为它定义了两个变量p和*p,p是一个地址变量,只能存放整型变量在内存中的地址;*p是一个整型变量,只能存放一个整数。

指针是一个复杂的概念,它能够指向(保存)不同类型变量的内存地址。例如:

int *pi;        // pi是指向int的指针
int **pc;        // pc是指向int指针的指针
int *pA[10];      // pA是指向int的指针数组
int (*f)(int,char);  // f是指向具有两个参数的函数的指针
int *f(int)       // f是一个函数,返回一个指向int的指针

2.3.2 指针与0和void*

没有任何变量会被分配到地址0,所以0就可以作为一个指针常量,与NULL的含义相同,表明指针当时没有指向任何变量。例如,对于前面的指针定义,以下赋值都是正确的:

pc=0;
pA=0;
f=0;

指针具有两个属性:地址和长度。地址大小固定,与类型无关;长度则与指针类型相关,这个类型指示编译器怎样解释它所指定内存区域的内容,以及该内存区域应该跨越多少个内存单元。例如,在32位微机上,int、float或double类型的指针都是4字节的地址,但int型的指针会指示编译器存取连续的4字节的内存单元作为一个整数,double型的指针则会指示编译器连续存取8字节的内存单元作为一个双精度浮点数……

因此,指针变量本身存储的仅是一个内存地址,其大小是固定的,而指针类型用于确定如何存取其所指内存区域的数据。鉴于此,C++提供了一种无类型指针void *。两个void*指针可以相互赋值、比较相等与否。

任何类型的指针都是大小相同的地址,而void*指针仅表示与之相关的值是个地址,因此能够接收任何数据类型(除函数指针外)的指针。但是,由于void*无法确定其所指内存数据的类型,因此只有显式地将一个void*转换成某种数据类型的指针之后,才能访问其所指内存区域的数据,其他操作都是不允许的。

例2-3】 void*指针的应用。

//Eg2-3.cpp
#include<iostream.h>
void main(){
    int i=4,*pi=&i;
    void* pv;
    double d=9,*pd=&d;
    pv=&i;       //L1:正确
    pv=pi;       //L2:正确
//  cout<<*pv<<endl;   //L3:错误
    pv=pd;       //L4:正确
    cout<<*(double*)pv;   //L5:正确,输出9
}

因为pv是void*指针,无法确定*pv所指内存区域的大小和类型,无法访问,必须像L5语句那样经过强制类型转换之后才能访问。

void*最重要的用途是作为函数的参数,以便向函数传递一个类型可变的对象。另一种用途就是从函数返回一个无类型的对象,在使用时再将它显式转换成适当的类型。

2.3.3 new和delete

指针常与堆(heap)空间的分配有关。所谓堆,就是一块内存区域,它允许程序在运行时以指针的方式从其中申请一定数量的存储单元(其他存储空间的分配是在编译时完成的),用于程序数据的处理。堆内存也称动态内存。

堆内存的管理由程序员完成。在C语言中,如果需要使用堆内存,程序员可以用函数malloc()从堆中分配指定大小的存储区域,用完之后必须用函数free()将之归还系统。如果用完之后没有用函数free()将它释放,就会造成内存泄漏(也就是自己不用了,其他程序也无法使用)。因此,函数malloc()和free()在C程序中总是成对出现的。例如:

#include<stdlib.h>       //malloc()和free()定义于此头文件中
main(){
    int *p;
    //从堆中分配1个int对象需要的内存,并将之转换为int类型
    p=(int*)malloc(sizeof(int));
    *p=23;
    free(p);          //释放堆内存
}

malloc( )的使用比较麻烦,除了要计算需求的内存大小之外,还必须对获得的内存区域进行类型转换才能使用。为此,C++提供了new和delete两个运算符进行堆内存的分配与释放,它们分别与malloc( )和free( )相对应,但使用起来更简单。

(1)new的用法

new的功能类似于malloc,用于从堆内存中分配指定大小的内存区域,并返回获得的内存区域的首地址。

用法1:p=new type;
用法2:p=new type(x);
用法3:p=new type[n];

其中,p是指针变量,type是数据类型。用法1只分配堆内存,用法2将分配到的堆内存初始化为x,用法3分配具有n个元素的数组。new能够根据type自动计算分配的内存大小,不需要用sizeof()计算。如果分配成功,会将得到的堆内存的首地址存放在指针变量p中;如果分配不成功,则返回空指针(0),在程序中可以此作为判断内存分配成功与否的依据。

(2)delete的用法

delete的功能类似于free,用于释放new分配的堆内存,以便它被其他程序使用。

用法1:delete p;
用法2:delete []p;

其中,p是用new分配的堆空间指针变量。用法1用于释放动态分配的单个指针变量,用法2用于释放动态分配的数组存储区域。

例2-4】 用new和delete分配与释放堆内存。

//Eg2-4.cpp
#include <iostream.h>
int main(){
    int *p1,*p2,*p3;
    p1=new int;    //分配一个能够存放int类型数据的内存区域
    p2=new int(10);  //分配一个int类型大小的内存区域,并将10存入其中
    p3=new int[10];  //分配能够存放10个整数的数组区域
    if(!p3){               //程序中常会见到这样的判定
        cout<<"allocation failure"<<endl; //分配不成功,就显示错误信息
        return 1;            //终止程序,并返回错误代码
    }
    *p1=5;
    *p3=1;
    p3[1]=2;        //访问指向数组的数组元素
    p3[2]=3;
    cout<<"p1  address: "<<p1<<" value: "<<*p1<<endl;
    cout<<"p2  address: "<<p2<<" value: "<<*p2<<endl;
    cout<<"p3[0] address: "<<p3<<" value: "<<*p3<<endl;
    cout<<"p3[1] address: "<<&p3[1]<<" value: "<<p3[1]<<endl;
    delete p1;       //释放p1指向的内存
    delete p2;
    delete p3;     //错误,只释放了p3指向数组的第1个元素
    delete []p3;     //释放p3指向的数组
    return 0;
}

delete p3和delete []p3是有区别的,前者只释放了第一个数组元素,即p[0],而没有释放其他数组元素p[1]~p[9],会造成内存泄漏;后者则将p3指向的数组区域全部归还系统。

(3)new、delete和malloc、free的区别

在C++程序中,仍然可以使用malloc、free进行动态存储空间的管理,但它们没有用new、delete方便。主要体现在以下几方面。

① new能够自动计算要分配的内存大小,不必用sizeof()计算所要分配的内存字节数,减少了出错的可能性。

② new不需要进行类型转换,能够自动返回正确的指针类型。

③ new可以对分配的内存进行初始化。

④ new和delete可以被重载,程序员可以借此扩展new和delete的功能,建立自定义的存储分配系统。