C++学习笔记之八 复合类型---指针和自由存储空间

来源:互联网 发布:境外电视直播软件apk 编辑:程序博客网 时间:2024/05/21 00:45

计算机程序在存储数据时必须跟踪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释放内存空间。












0 0
原创粉丝点击