指针和自由存储空间
在第3章的开头,提到了计算机程序在存储数据时必须跟踪的3种基木属性。为了方便,这里再次列
了这些属性:
- 信息存储在何处;
- 存储的值为多少;
- 存储的信息是什么类型。
您使用过一种策略来达到上述目的:定义一个简单变量。声语句指出了值的类型和符号名,还让程
序为值分配内存,并在内部跟踪该内存单元。
下面来看一看另一种策略,它在开发C++类时非常重要。这种策略以指针为基础,指针是一个变量,
其存储的是值的地址,一面不是值本身。在讨论指针之的,我们先看一看如何找到常规变量的地址。只需对变量应用地址运算符(&),就可以获得它的位置;例如,如果home是一个变量,则&home是它的地址。
程序清单4.14演示了这个运算符的用法。
// 指针和自由存储空间.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> int main() { using namespace std; int donuts = 6; double cups = 4.5; cout << "donuts value = " << donuts; cout << "and donuts address = " << &donuts << endl; cout << "cups value =" << cups; cout << "and cups address = " << &cups << endl; return 0; }运行结果
donuts value = 6and donuts address = 000000019E92F6A4 cups value =4.5and cups address = 000000019E92F6C8显示地址时,该实现的cout使用十六进制表示法,因为这是常用于描述内存的表示法(有些实现可能
使用十进制表示法)。在该实现中,donuts的存储位置比cups要低。两个地址的差为0x0065fd44一0x0065fd6540
(即4)。这是有意义的,因为donuts的类型为int,而这种类型使用4个字节。当然,不同系统给定的地址
值可能不同。有些系统可能先存储cups,再存储donuts,这样两个地址值的差将为8个字节,因为cups
的类型为double。另外,在有些系统中,可能不会将这两个变量存储在相邻的内存单元中。
使用常规变量时,值是指定的量,面地址为派生量。下面来看看指针策略,它是C++内存管理编程理
念的核心(参见旁注“指针与C++基本原理”)。
指针与C+基本原理
面向对象编程与传统的过程性煸程的区在于,强调的是在运行阶段(而不是编译阶段)一进行决
策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。运行阶段决策就好比度
假时,选择参观哪些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先
设定的日程安排。
运行阶段决策提供了灵活性,可以根据当时的情况进行调整。例如,考虑为数组分配内存的情况。传
统的方法是声明一个数组。要在C++中声明数组,必须指定数组的长度。因此,数组长度在程序编译时就
设定好了;这就是编译阶段决策。您可能认为,在80%的情况下,一个包含20个元素的数组足够了,但
程序有时需要处理200个元素。为了安全起见,使用了一个包含2个元素的数组。这样,程序在大多数
情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后,可以
这次告诉它只需要20个元素,而还可以下次告诉它需要205个元素。
总之,使用OOP时,您可能在运行阶段确定数组的长度。为使用这种方法,言必须允许在程序运行
时创建数组。稍后您看会到,C++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟
踪新分配的存的位置。
在运行阶段做决并非OOP独有的,但使用C++编写这样的代码比使用C语言简单。
处理存储数据的新略刚好相反,将地址视为指定的量,而将值视为派生量,一种特殊类型的变量——指针用于存储值的地址。因此,指针名表示的是地址。运算符被称为间接值(indirect velue)或解除引用(derefercncing)
运算符,将其应用于指针,可以得到该地址处存储的值(这和乘法使用的符号相同:C++根据上下文来确定所指
的是乘法还是解除引用)。例如,假设manly是一个指针,则manly表示的是一个地址,而manJy表示存储在该
地址处的值。*manly与常规int变量等效。程序清单4.15说明了这几点,它还演示了如何声明指针。
// 指针和自由存储空间415.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> int main() { using namespace std; int updates = 6; int* p_updates; p_updates = &updates; cout << "values:updates =" << updates; cout << ",p_updates =" << *p_updates << endl; cout << "Addresses:&updates = " << &updates; cout << ",p_updates = " << p_updates << endl; *p_updates = *p_updates + 1; cout << "Now updates = " << updates << endl; return 0; }运行结果
values:updates =6,p_updates =6 Addresses:&updates = 0000001E00CFF654,p_updates = 0000001E00CFF654 Now updates = 7从中可知,int变量updates和指针变量p_updates只不过是同一枚硬币的两面。变量updates表示值,
并使用&运算符来获得地址;而变量p-updates表示地址,并使用运算符来获得值(参见图4.8)。由于
p-updates指向updates,因此p-updates和updates完全等价。可以像使用int变量那样使用p_updates。正
如程序清单4.15表明的,甚至可以将值赋给pupdates。这样做将修改指向的值,即updates。
声明和初始化指针
我们来看看如何声明指针。计算机需要指针指卣的值的类型。例如,char的地址与double地址看上去没有两样,但double和char使用的字节数是不同,它们的存储值时使用的内部式也不同。因此,指针声明必须指定指针指向的数据的型。
例如,前一个示例包含这样的声明:
int *p_updates;这表明,p_updates的类型为int。由于运筧符被用于指针,因此p_updates变量本身必须是指针。我们说p_updates指向int类型,我们还说p_updates的类型是指向int的指针,或后int*,可以这样说,p_updates
是指针(地址),*p_updates是int,而 不是指针(见图4.9)。
顺便说一句,*运算符两边的空格是可选的。传统上,C 程序员使用这种格式:
int *ptr;这强调*ptr 是一个int 类型的值。而很多C++程序员使用这种格式:int* ptr;
这强调的是:int是一种类型—指向int 的指针。在哪里添加空格对于编译器来说没有任何区别,您
甚至可以这样做:int* ptr;
但要知道的是,下面的声明创建一个指针(p1)和一个int 变量(p2):int*p1,p2;对每个指针变量名,都需要使用一个。
注意:在C++中,int *是一种复合类型,是指向int 的指针。
可以用同样的句法来声明指向其他类型的指针:
double * tax_ptr; //tax_ptr points to type double char *str; //str points to type char由于己将tax_ptr声明为一个指向double的指针,因此编译器知道冰tax一ptr是一个double类型的值。也
就是说,它知道tax-ptr是一个以浮点格式存储的值,这个值(在大多数系统上)占据8个字节。指针变
量不仅仅是指针,而且是指向特定类型的指针。tax-ptr的类型是指向double的指针(或double类型),str
是指向char的指针类型(或char*),尽管它们都是指针,却是不同类型的指针。和数组一样,指针都是基
于其他类型的。
虽然tax-ptr和str指向两种长度不同的数据类型,但这两个变量本身的长度通常是相同的。也就是说,
char的地址与double的地址的长度相同,这就好比1016可能是超市的街道地址,而1024可以是小村庄的
街道地址一样。地址的长度或值既不能指示关于变量的长度或类型的任何信息,也不能指示该地址上有什
么建筑物。一般来说,地址需要2个还是4个字节,取决于计算机系统(有些系统可能需要更大的地址,
系统可以针对不同的类型使用不同长度的地址)。
可以在声明语句中初始化指针。在这种情况下,被初始化的是指针,而不是它指向的值。也就是说,
下面的语句将pt(而不是*pt)的值设置为&higgens:
int higgens=5; int *pt=&higgens;程序清单4.16演示了如何将指针初始化为一个地址。
// 如何将指针初始化为一个地址.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> int main() { using namespace std; int higgens = 5; int* pt = &higgens; cout << "Value of higgens = " << higgens << "; Address of higgens =" << &higgens << endl; cout << "Value of *pt = " << *pt << "; Value of pt = " << pt << endl; return 0; }运行结果
Value of higgens = 5; Address of higgens =000000B5410FF764 Value of *pt = 5; Value of pt = 000000B5410FF764从中可知,程序将pi(而不是*pi)初始化为higgens 的地址。在您的系统上,显示的地址可能不同,
显示格式也可能不同。
指针的危险
指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上看,指针与整数是截然不同的类型。
整数是可以执行加、减、除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以
对整数和指针执行的操作上看,它们也是彼此不同的。因此,不能简单地将整数赋给指针:
int *pt; pt=0xB8000000;//type mismatch在这里,左边是指向int 的指针,因此可以把它赋给地址,但右边是一个整数。您可能知道,0xB8000000
是老式计算机系统中视频内存的组合段偏移地址,但这条语句并没有告诉程序,这个数字就是一个地址。
在C99 标准发布之前,C 允许这样赋值。但C++在类型一致方面的要求更严格,编译器将显示一条
错误消息,通告匹配。要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地
址类型:
int *pt; pt=(int*)0xB8000000;//type mismatch这样,赋值语句的两边都是整数的地址,因此这样赋值有效。注意,pt 是int 值的地址并不意味着pt
本身的类型是int。例如,在有些平台中,int 类型是个2 字节值,而地址是个4 字节值。
指针还有其他一些有趣的特性,这将在合适的时候讨论。下面看看如何使用指针来管理运行阶段的内
存空间分配。
使用new 来分配内存
对指针的工作方式有一定了解后,来看看它如何实现在程序运行时分配内存。前面我们都将指针初始
化为变量的地址;变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供
了一个别名。指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通
过指针来访问内存。在C 语言中,可以用库函数malloc( )来分配内存;在C++中仍然可以这样做,但C++
还有更好的方法—new 运算符。
下面来试试这种新技术,在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里
的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据类型分配内存,new将找到一个长度
正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。下面是一个这样的示例:
int *pn=new int;new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,
它找到这样的内存,并返回其地址。接下来,将地址赋给pn,pn是被声明为指向int的指针。现在,pn是
地址,而*pn是存储在那里的值。将这种方法与将变量的地址赋给指针进行比较:
int higgens; int * pt=&higgens;在这两种情况(pn 和pt)下,都是将一个int 变量的地址赋给了指针。在第二种情况下,可以通过名
称higgens 来访问该int,在第一种情况下,则只能通过该指针进行访问。这引出了一个问题:pn 指向的内
存没有名称,如何称呼它呢?我们说pn 指向一个数据对象,这里的“对象”不是“面向对象编程”中的对
象,而是一样“东西”。术语“数据对象”比“变量”更通用,它指的是为数据项分配的内存块。因此,变
量也是数据对象,但pn 指向的内存不是变量。乍一看,处理数据对象的指针方法可能不太好用,但它使程
序在管理内存方面有更大的控制权。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName *pointer_name=new typeName;需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。当然,如果已经
声明了相应类型的指针,则可以使用该指针,而不用再声明一个新的指针。程序清单4.17 演示了如何将new
用于两种不同的类型。
#include <iostream> int main() { using namespace std; int nights = 1001; int* pt = new int; //allocate space for an int *pt = 1001; //store a value there cout << "nights value ="; cout << nights << ":location" << &nights << endl; cout << "int "; cout << "value = " << *pt << ":location = " << pt << endl; double* pd = new double; //allocate space for a double *pd = 10000001.0; //store a double there cout << "double"; cout << "value =" << *pd << ":location = " << pd << endl; cout << "location of pointer pd:" << &pd << endl; cout << "size of pt =" << sizeof(pt); cout << "size of pd = " << sizeof pd; cout << ":size of *pd = " << sizeof(*pd) << endl; return 0; }运行结果
nights value =1001:location000000E91756FB84 int value = 1001:location = 000002375D54DC40 doublevalue =1e+07:location = 000002375D560E70 location of pointer pd:000000E91756FBC8 size of pt =8size of pd = 8:size of *pd = 8当然,内存位置的准确值随系统而异。
程序说明
该程序使用new 分别为int 类型和double 类型的数据对象分配内存。这是在程序运行时进行的。指针
pt 和pd 指向这两个数据对象,如果没有它们,将无法访问这些内存单元。有了这两个指针,就可以像使用变量那样使用pt和pd了。将值赋给pt和pd,从而将这些值赋给新的数据对象。同样,可以通过打印
pt和pd来显示这些值。
该程序还指出了必须声明指针所指向的类型的原因之一。地址本身只指出了对象存储地址的开始,而
没有指出其类型(使用的字节数)。从这两个值的地址可以知道,它们都只是数字,并没有提供类型或长度
信息。另外,指向int的指针的长度与指向double的指针相同。它们都是地址,但由于usenew.cpp声明了
指针的类型,因此程序知道*pd是8个字节的double值,pt是4个字节的int值。usenew.cpp打印pd的
值时,cout知道要读取多少字节以及如何解释它们。
对于指针,需要指出的另一点是,new分配的内存块通常与常规变量声明分配的内存块不同。变量nights
和pd的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区(free store)
的内存区域分配内存。第9章将更详细地讨论这一点。
使用delete 释放内存
当需要内存时,可以使用new 来请求,这只是C++内存管理数据包中有魅力的一个方面。另一个方面
是delete 运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一
步。归还或释放(free)的内存可供程序的其他部分使用。使用delete 时,后面要加上指向内存块的指针(这
些内存块最初是用new 分配的):
int * ps=new int; //allocate memory with new ... delete ps; //free memory with delete when done这将释放ps 指向的内存,但不会删除指针ps 本身。例如,可以将ps 重新指向另一个新分配的内存块。
一定要配对地使用new 和delete;否则将发生内存泄漏(memory leak),也就是说,被分配的内存再也无
法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。
不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都
可能发生。另外,不能使用delete 来释放声明变量所获得的内存:
int *ps=new int; //ok delete ps; //ok delete ps; //not ok now int jugs=5; //5 int *pi=&jugs; //ok delete pi; //not allowed,memory not allocated by new警告:只能用delete 来释放使用new 分配的内存。然而,对空指针使用delete 是安全的。
注意,使用delete 的关键在于,将它用于new 分配的内存。这并不意味着要使用用于new 的指针,而
是用于new 的地址:
int *ps=new int; //allocate memory int *pq=ps; //set second pointer to same block delete pq; //delete with second pointer一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可
能性。但稍后您会看到,对于返回指针的函数,使用另一个指针确实有道理。
使用new 来创建动态数组
如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做
比使用new和指针更简单,尽管给人留下的印象不那么深刻。通常,对于大型数据(如数组、字符串和结
构),应使用new,这正是new的用武之地。例如,假设要编写一个程序,它是否需要数组取决于运行时
用户提供的信息。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否
使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。但使用new时,如果在运行阶段需要数组,则创建它;如果不需要,
则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在
程序运行时创建的。这种数组叫作动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数
组的长度;使用动态联编时,程序将在运行时确定数组的长度。
下面来看一下关于动态数组的两个基本问题:如何使用C++的new运算符创建数组以及如何使用指针
访问数组元素。
1.使用new 创建动态数组
在C++中,创建动态数组很容易;只要将数组的元素类型和元素数目告诉new即可。必须在类型名后
加上方括号,其中包含元素数目。例如,要创建一个包含10个int元素的数组,可以这样做:
int *psome=new int[10];//get a block of 10 ints;new 运算符返回第一个元素的地址。在这个例子中,该地址被赋给指针psome。
当程序使用完new 分配的内存块时,应使用delete 释放它们。然而,对于使用new 创建的数组,应使
用另一种格式的delete 来释放:
delete [] psome; //free a dynamic array方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。请注意delete 和指针之间的方括号。
如果使用new 时,不带方括号,则使用delete 时,也不应带方括号。如果使用new 时带方括号,则使用delete
时也应带方括号。C++的早期版本无法识别方括号表示法。然而,对于ANSI/ISO 标准来说,new 与delete
的格式不匹配导致的后果是不确定的,这意味着程序员不能依赖于某种特定的行为。下面是一个例子:
int *pt=new int; short *ps=new short[500]; delete []pt;//effect is undefined,don't do it delete ps; //effect is undefined,don't do it总之,使用new 和delete 时,应遵守以下规则。
- 不要使用delete 来释放不是new 分配的内存。
- 不要使用delete 释放同一个内存块两次。
- 如果使用new [ ]为数组分配内存,则应使用delete [ ]来释放。
- 如果使用new [ ]为一个实体分配内存,则应使用delete(没有方括号)来释放。
- 对空指针应用delete 是安全的。
现在我们回过头来讨论动态数组。psome是指向一个(数组第一个元素)的指针。您的责任是跟踪
内存块中的元素个数。也就是说,由于编译器不能对psome是指向10个整数中的第1个这种情况进行跟
踪,因此编写程序时,必须让程序跟踪元素的数目。
实际上,程序确实跟踪了分配的内存量,以便以后使用delete[]运算符时能够正确地释放这些内存。
但这种信息不是公用的,例如,不能使用sizeof运算符来确定动态分配的数组包含的字节数。
为数组分配内存的通用格式如下:
type_name *pointer_name=new type_name[num_elements];使用new 运算符可以确保内存块足以存储num_elements 个类型为type_name 的元素,而pointer_name
将指向第1 个元素。下面将会看到,可以以使用数组名的方式来使用pointer_name。
使用动态数组
创建动态数组后,如何使用它呢?首先,从概念上考虑这个问题。下面的语句创建指针psome,它指
向包含10 个int 值的内存块中的第1 个元素:
int *psome=new int[10]; //get a block of 10 ints可以将它看作是一根指向该元素的手指。假设int 占4 个字节,则将手指沿正确的方向移动4 个字节,
手指将指向第2 个元素。总共有10 个元素,这就是手指的移动范围。因此,new 语句提供了识别内存块中
每个元素所需的全部信息。
现在从实际角度考虑这个问题。如何访问其中的元素呢?第一个元素不成问题。由于psome指向数组
的第1个元素,因此psome是第1个元素的值。这样,还有9个元素。如果没有使用过C语言,下面这
种最简单的方法可能会令您大吃一惊:只要把指针当作数组名使用即可。也就是说,对于第个元素,可
以使用psome[0],而不是psome;对于第2个元素,可以使用psome[1],依此类推。这样,使用指针来访
问动态数组就非常简单了,虽然还不知道为何这种方法管用。可以这样做的原因是,C和C++内部都使用
指针来处理数组。数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题,但这是另一码事
稍后将更详细地介绍这种等同性。首先,程序清单4.18演示了如何使用new来创建动态数组以及使用数组
表示法来访问元素:它还指出了指针和真正的数组名之间的根本差别。
#include <iostream> int main() { using namespace std; double* p3 = new double[3]; //space for 3 doubles p3[0] = 0.2; p3[1] = 0.1; p3[2] = 0.8; cout << "p3[1] is" << p3[1] << ".\n"; p3 = p3 + 1; //increment the pointer cout << "Now p3[0] is" << p3[0] << "and"; cout << "p3[1] is " << p3[1] << ".\n"; p3 = p3 - 1; //point back beginning delete[] p3; return 0; }运行结果
p3[1] is0.1. Now p3[0] is0.1andp3[1] is 0.8.从中可知,arraynew.cpp 将指针p3 当作数组名来使用,p3[0]为第1 个元素,依次类推。下面的代码行
指出了数组名和指针之间的根本差别:
p3=p3+1; //okay for pointers,wrong for array names不能修改数组名的值。但指针是变量,因此可以修改它的值。请注意将p3加1的效果。表达式p3[0]
现在指的是数组的第2个值。因此,将p3加1导致它指向第2个元素而不是第1个。将它减1后,指针将
指向原来的值,这样程序便可以给delete[]提供正确的地址。
相邻的int地址通常相差2个字节或4个字节,而将p3加1后,它将指向下一个元素的地址,这表明
指针算术有一些特别的地方。情况确实如此。