计算机程序在存储数据时必须跟踪3种基本属性:信息存储的何处、存储的值为多少以及存储的信息是什么类型。
我们使用过一种策略来达到上述目的:定义一个简单变量。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。
这一节我们介绍另一种策略,这种策略以指针为基础,指针是一个变量,其存储的是值的地址,而不是值本身。
1、运用地址运算符找到常规变量的地址
对变量应用地址运算符(&)就可以获得它的位置,看下面的例子:
#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 = 6 and donuts address = 0x0065fd40;
cups value = 4.5 and cups address = 0x0065fd44;
显示地址时,该实现的cout使用十六进制表示法,因为这是常用于描述内存的表示法(有些实现可能使用十进制表示法)。
对于地址存储位置需要解释一下,在该实现中,donuts的存储位置比cups要低,两个地址的差为0x0065fd44-0x0065fd40=4,这是因为先存储的是donuts,而donuts的类型为int,占用4个字节。当然,不同系统给定的地址值可能不同,有些系统可能先存储cups,在存储donuts,这样两个地址的差就是8个字节,因为cups的类型为double。另外,有些系统可能不会将两个变量存储在相邻的内存单元中。
这里需要注意,使用常规变量时,值是指定的,而地址为派生量。
2、指针与C++基本原理
面向对象的编程与传统的过程性编程的区别在于,OPP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段是指程序正在运行时,编译阶段是指编译器将程序组合起来时。编译阶段决策就是指在程序编译之前把一切都设定好了,不管以后什么情况都按照预先设定安排执行;而运行阶段决策是指可以在运行时根据情况进行选择执行,显然,后者的灵活性更大。
举个经常会遇到的例子:考虑为数组分配内存的情况。传统的方法是声明一个数组,而要在C++中声明数组,必须指定数组的长度。因此数组长度在程序编译时就设定好了;这就是编译阶段决策。那么问题就来了,可能在80%的情况下,一个包含20个元素的数组就够了,但是程序有时需要处理200个元素,为了安全起见,使用了一个包含200个元素的数组。这样,在大多数情况下都是在浪费内存。
OPP正是为了解决这个问题,将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后可以这次告诉它只需要20个元素,而还可以下次告诉它需要205个元素。为了使用这种方法,语言必须允许在程序运行时创建数组,C++正是通过指针解决了这个问题。
3、指针
上面提到,常规变量是指定值,系统为其派生地址。而指针存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生量。指针正是这样一种特殊的用于存储值的地址的变量。
指针名表示的是地址,*运算符被称为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值。例如,假设manly是一个指针,则manly表示的是一个地址,而*manly表示存储在该地址处的值。*mainly与常规int变量等效。
#include <iostream>
int main()
{
using namespace std;
int updates = 6;
int* p_updates;
p_updates = &updates;
//表达值的两种方法
cout<<"Value: 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 = 0x0065fd48, p_updates = 0x0065fd48
Now updates = 7
从上面的程序可以看出,int变量updates和指针变量p_updates只不过是同一枚硬币的两面。变量updates表示值,并使用&运算符来获得地址;而变量p_updates表示地址,并使用*运算符来获得值。由于p_updates指向updates,因此*p_updates和updates完全等价。
4、声明和初始化指针
指针声明必须指定指针指向的数据的类型。例如:int* p_updates;
这表明,p_updates变量本身必须是指针,虽然这个地方的说法很多,但本质都是一样的,我们就按照一种来说,以免大家混乱。p_updates这个变量的类型是指向int的指针,或int* 。即p_updates是指针(地址),而*p_updates是int,而不是指针。
需要说明的是,*运算符两边的空格是可选的。传统上C程序员使用这种格式: int *ptr;这强调的是*ptr是一个int类型的值。但是我们C++程序员使用这种格式int* ptr;这强调的是int*是一种类型----指向int的指针类型。
还有要知道,对每一个指针变量名,都需要使用一个*,而不能这样: int* p1,p2;这表示声明创建一个指针(p1)和一个int变量(p2)。
关于指针的初始化:
可以在声明语句中初始化指针,但注意被初始化的是指针,而不是它指向的值。也就是说下面的语句将pt(而不是*pt)的值设置为&higgens:
int higgens = 5;
int * pt = &higgens;
5、指针的危险
使用指针可能会存在一些危险,而且这种危险是很难被发现的,所以一定要仔细认证的使用指针。在C++中创建指针时,计算机将分配用来存储地址的内存(因为创建的是指针变量,创建变量的时候计算机当然会给分配内存),但是这个指针指向哪一个内存并没有声明。这时候我们必须给指针初始化一个地址,否则会很麻烦!看看下面这个例子:
long* fellow;
*fellow = 223323;
fellow确实是一个指针,但它指向哪里呢?上述代码并没有将地址赋给fellow。那么223323将被放在哪里呢?我们不知道。由于fellow没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储223323的地址。如果fellow的值碰巧是1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址。fellow指向的地方很可能并不是所要存储223323的地方。这种错误可能会导致一些最隐匿、最难以跟踪的bug。
警告:一定要在对指针应用解除运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。
6、指针和数字
指针不是整型,虽然计算机通常把地址当作整数来处理。但是从概念上看,指针与整数是截然不同的类型。整数是可以执行加、减、乘除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以对整数和指针执行的操作上看,它们也是彼此不同的。
不能简单的把整数赋给指针:
int* pt;
pt = 0xB8000000;
在这里,左边是指向int的指针,因此可以把它赋给地址,但右边是一个整数。即便你自己知道这个整数就是某一个地址,但是这条语句并没有告诉程序,这个数字就是一个地址。在C++中编译器将显示一条错误信息,通告类型不匹配。要将数字值作为地址来使用,应使用强制类型转换将数字转换为适当的地址类型:
int * pt;
pt = (int*)0xB8000000;
这样,赋值语句两边都是整数的地址,因此这样赋值有效。
7、使用new来分配内存
前面我们都将指针初始化为变量的地址:变量在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。但是,其实指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下只能通过指针来访问内存。动态分配内存可以通过new运算符实现。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName * pointer_name = new typeName;
解释一下:
举个例子,在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。关键就是new运算符。
int * pn = new int;
new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少个字节的内存。然后,它找到这样的内存,并返回其地址。接下来,将地址赋给pn,pn是被声明为指向int的指针。现在,pn是地址,而*pn是存储在那里的值。
我们还是通过一个例程演示一下:
#include <iostream>
int main()
{
using namespace std;
int nights = 1001;
int * pt = new int; //为Int分配一个空间
*pt = 1001; //在这个分配的空间里存储一个值
cout << " nights value = ";
cout << nights << ": location " << &nights << endl;
cout << " int ";
cout << "value = " *pt << ": location = " << pt <<endl;
double * pd = new double; //分配一个double类型的空间
*pd = 10000001.0; //在这个空间中存储一个double值
cout << " double value = "<<*pd<< ": location = "<<pd <<endl; //输出double指针地址和存储的数据
cout<<"location of pointer pd: "<<&pd <<endl; //指针(存放地址信息)的内存的地址
cout<<"size of pt = " << sizeof(pt);
cout << "size of *pt = "<<sizeof(*pt)<<endl;
`
cout<<"size of pd = " << sizeof(pd);
cout << "size of *pd = "<<sizeof(*pd)<<endl;
return 0;
}
下面是该程序的输出:
nights value = 1001: location 0028F7F8
int value = 1001: location = 00033A98
double value = le+007: location = 000339B8
location of pointer pd: 0028F7FC
size of pt = 4: size of *pt = 4
size of pd = 4: size of *pd = 8
程序说明:
该程序使用new分别为int类型和double类型的数据对象分配内存。有了这两个指针,就可以像使用变量那样使用*pt和*pd了。另外,地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数),这也是必须声明指针所指向类型的原因之一。
还有一点需要指出,new分配的内存块通常与常规变量声明分配的内存块不同。变量nights和pd的值都存储在被称为栈(stack)内存中,而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。
8、使用delete释放内存
计算机的内存是有限的,我们可以很方便的使用new为变量分配内存,但是如果这些内存不及时的释放的话,我们的内存就可能会被耗尽。所以在使用new运算符的时候,最好配合着delete运算符。
delete运算符使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一步。归还或释放(free)的内存可供程序的其它部分使用。使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的):
int * ps = new int;
. . .
delete ps;
这将释放ps指向的内存,但不会删除指针ps本身。例如,可以将ps重新指向另一个新分配的内存块。一定要配对的使用new和delete;否则将发生内存泄漏(memory leak),也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断的寻找更多的内存而终止。
不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete来释放声明变量所获得的内存:
int* ps = new int; //正确
delete ps; //正确
delete ps; //错误
int jugs = 5;
int* pi = &jugs;
delete pi; //错误,不能使用delete释放声明变量获得的内存
9、使用new来创建动态数组
对于一个小型数据对象来说,可能声明一个简单变量比使用new和指针更简单,但是,对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地。
例如,假设要编写一个程序,它是否需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编,意味着数组是在编译时加入到程序中的。但是使用new时,如果在运行阶段使用数组,则创建它,如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编。意味着数组是在程序运行时创建的。这种数组叫做动态数组。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。
下面是关于动态数组的两个问题:
(1)使用new创建动态数组
在C++中创建动态数组时,只要将数组的元素类型和元素数目告诉new即可。必须在类型名后加上方括号,其中包含元素的数目。例如,要创建一个包含10个int元素的数组,可以这样做:
int* psome = new int [10];
new运算符返回第一个元素的地址。这个例子中,该地址被赋给指针psome。
对应的delete释放数组内存: delete [] psome;
方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
为数组分配内存的通用格式如下:
type_name* pointer_name = new type_name [num_elements];
使用new运算符可以确保内存块足以存储num_elements个类型为type_name的元素,而pointer_name将指向第1个元素。
(2)使用动态数组
如何访问动态数组中的元素呢?只要把指针当作数组名使用即可。也就是说,对于第一个元素,可以使用psome[0],而不是*psome;对于第二个元素,可以使用psome[1],依次类推。这样,使用指针来访问动态数组就非常简单了,虽然还不知道为何这种方法管用。可以这么做的原因是,C和C++内部都是使用指针来处理数组的。
我们仍然用程序来练习一下:
#include <iostream>
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[0]为第一个元素,依次类推。
下面这行代码指出了数组名和指针之间的根本差别:
p3 = p3 + 1;
数组名是不能修改的,但是指针是变量,因此可以修改它的值。注意此处p3加1的效果。表达式p3[0]现在指的是数组的第二个值。因此,将p3加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给delete[]提供正确的地址。特别注意,必须指针回到原来的位置才能调用delete释放内存空间。