指针和自由存储空间
计算机程序在存储数据时必须跟踪的三种基本属性:
- 信息存储在何处
- 存储的值为多少
- 存储的信息是什么类型
您使用过一种策略达到上述目的:定义一个简单变量。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。
下面来看另一种策略。这种策略以指针为基础,指针是一个变量,其存储的是值的地址,而不是值本身。
运行阶段决策提供了灵活性,可以根据当时的情况进行调整。例如,考虑为数组分配内存的情况。传统的方法是声明一个数组。要在c++中声明数组,必须指定数组的长度。因此数组的长度在程序编译时就决定好了,这就是编译阶段决策。您可能认为在80%的情况下,数组长为20就足够了,但程序有时需要处理200个元素。为了安全起见使用了一个长为200的数组。这样程序在大多数情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段,使程序更加灵活。在程序运行后可以告诉它这次使用20个元素,下次还可以告诉它使用202个元素。
总之,使用OOP时,您可以在运行阶段确定数组长度。为使用这种方法,语言必须允许在程序运行时创建数组。c++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存位置。
在运行阶段做决策并非OOP所独有,但c++编写这样的代码比c简单。
指针用于存储值的地址,将地址视为指定的量,而将值视为派生量。因此指针名表示的是地址。*运算符被称为间接值或解除引用运算符,将其应用于指针可以得到该地址存储的值。
指针的声明和初始化
int ducks = 12 ;
int *birddog = &ducks;
指针的危险
在c++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向数据的内存。为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦,如下所示:
long *p;
*p=123;
fellow确实是一个指针,但它指向哪里呢?上述代码没有将地址赋给fellow。那么123将被放在哪里呢?我们不知道。由于fellow没有被初始化,他可能有任何值。不管值是什么,程序都将它解释为储存123的地址。假设fellow的值恰好时1200,计算机将把数据放在1200地址上,即使这段地址是我们不应当使用的。fellow指向的地方很可能不是所要存储123的地方。这种错误可能会导致一些最隐匿、最难以跟踪的bug。
警告:一定要在对指针应用解引用运算符*之前,将指针初始化为一个确定的、适当的地址。
在“使用new来创建动态数组”一节中有更多内容。
使用new来分配内存
前面我们将指针初始化为变量的地址。变量是在编译时分配的有名称的地址,而指针只为可以通过名称直接访问的内存提供了一个别名。指针的真正用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在c语言中用malloc,在c++中仍然可以用,但是c++还有更好的–new。
int *pn = new int;
new int 告诉程序,需要存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后他找到这样的内存,并返回其地址。
pn指向一个数据对象,术语“数据对象”,指为数据项分配的内存。变量是有名称的内存,因此变量也是数据对象,但pn指向的内存不是变量。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName * pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。
为什么必须声明指针所指向的类型的原因:地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。指向double的指针的长度和指向int的指针相同。他们都是地址,但是声明了指针的类型,因此程序数据对象有几个字节以及如何解释它。
对于指针,new分配的内存块和常规变量声明分配的内存块不同。变量的值存储在栈stack中,而new从堆heap或自由存储区free store 的内存区域分配内存。
使用delete释放内存
int * ps = new int;
delete ps;
delete ps; //not ok
int jugs = 5;
int * pi = &jugs;
delete pi; //not allowed
delete ps 将释放ps指向的内存,但不会删除ps指针本身。不要尝试释放已释放的内存,虽然不会报错,但这样做的结果是不确定的,这意味着什么情况都有可能发生。只能用delete来释放new分配的内存。对空指针使用delete是安全的。
使用new来创建动态数组
对于管理小型数据对象来说,使用简单变量就足够了,但是对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地。 例如假设程序是否 需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序编译时将为他分配内存空间。不管程序最终是否需要,他都将存在,占用内存。在编译时给数组分配内存称为静态联编(static binding),意味着数组是在编译时加入程序的。但是使用new时,如果在运行阶段需要数组,就创建它;否则不创建。还可以在程序运行时选择数组的长度。这称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫做动态数组。使用静态联编,必须在编写程序时指定数组的长度;使用动态联编,程序将在运行时确定数组的长度。
使用new创建动态数组
int * psome = new int [10];
delete [] psome;
注意delete的方式。方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
int * pt = new int;
short * ps = new short [500];
delete [] pt; //effect is undefined,而且不报错,所以难以发现错误
delete ps; //effect is undefined,而且不报错,所以难以发现错误
new和delete的格式不匹配导致的后果是不确定的。
使用new和delete时,应遵循以下规则。
- 不要使用delete来释放不是new分配的内存
- 不要使用delete释放同一个内存块2次
- 如果使用new[ ]为数组分配内存,则应使用delete[ ]来释放
- 如果使用new为数组分配内存,则应使用delete(没有方括号)来释放
程序跟踪了分配的内存量,以便以后使用delete[ ]运算符时能够正确释放内存。但这种信息不是公用的,例如不能使用sizeof运算符来确定动态分配的数组包含的字节数。
为数组分配内存的通用格式是:
typeName * pointer_name = new typeName [num_elems];
使用动态数组
使用动态数组和普通数组类似。例如对于第一个元素,可以使用psome[0].
# include <iostrem>
int main()
{
using namespace std;
double *p3 = new double [3];
p3[0] = 0.2;
p3[1] = 0.5;
p3[2] = 0.8;
cout << "p3[1] is " << p3[1] << ".\n";
p3 = p3 + 1;
cout << "Now p3[0] is " << p3[0] << " and ";
cout << "p3[1] is " << p3[1] << ".\n";
p3 = p3 -1;
delete [] p3;
return 0;
}
输出:
p3[1] is 0.5.
Now p3[0] is 0.5 and p3[1] is 0.8.
下边的代码指出了数组名和指针的根本差别:
p3 = p3 + 1;
不能修改数组名的值。但是指针是变量,因此可以修改它的值。