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的功能,建立自定义的存储分配系统。