声明
文章内容来自于《C++ Primer Plus》,本文重新组织语言,选取书本部分内容重新组合。

一、使用new来分配内存

前面我们都将指针初始化为变量的地址;变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供 了一个别名。指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C语言中,可以用库函数malloc( )来分配内存;在 C++中仍然可以这样做,但C++ 还有更好的方法——new运算符。

下面来试试这种新技术,在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。下面是一个这样的示例:

int  *pn=new int;

new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来, 将地址赋给pnpn是被声明为指向int的指针。现在, pn是地址,而*pn是存储在那里的值。将这种方法与将变量的地址赋给指针进行比较:

int higgens;
int *pt=&higgens;

在这两种情况(pnpt)下,都是将一个int变量的地址赋给了指针。在第二种情况下,可以通过名称higgens来访问该int,在第一种情况下,则只能通过该指针进行访问。这引出了一个问题:pn指向的内存没有名称,如何称呼它呢?我们说pn指向一个数据对象,这里的“对象”不是“面向对象编程”中的对象,而是一样“东西”。术语“数据对象”比“变量”更通用,它指的是为数据项分配的内存块。因此, 变量也是数据对象,但pn指向的内存不是变量。乍一看,处理数据对象的指针方法可能不太好用,但它使程序在管理内存方面有更大的控制权。

为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:

typeName  * pointer_name = new typeName;

需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。当然,如果已经声明了相应类型的指针,则可以使用该指针,而不用再声明一个新的指针。

#include<iostream>
using namespace std;
int main(){
    int *pt=new int;
    *pt=1;
    cout<<pt<<endl<<*pt<<endl;
    pt=new int;
    *pt=2;
    cout<<pt<<endl<<*pt<<endl;
    return 0;
}
//输出:
//0x234e288bfa0
//1
//0x234e28a9fa0
//2

当然,内存位置的准确值随系统而异。

对于指针,需要指出的另一点是,new分配的内存块通常与常规变量声明分配的内存块不同。一般变量的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区(free store) 的内存区域分配内存。

二、使用delete释放内存

当需要内存时,可以使用new来请求,这只是C++内存管理数据包中有魅力的一个方面。另一个方面是delete运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一 步。归还或释放(free)的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)

int *p=new int;
delete p;

这将释放p指向的内存,但不会删除指针p本身。例如, 可以将p重新指向另一个新分配的内存块。一定要配对地使用newdelete;否则将发生内存泄漏(memory leak),也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete来释放声明变量所获得的内存。但是对空指针应用delete是安全的。

int *p=new int; //可行
delete p;       //可行
delete p;       //二次删除不可行
int a=1;
int *p=&a;      //可行
delete pi;      //不可行

注意,使用delete的关键在于,将它用于new分配的内存。这并不意味着要使用用于new的指针,而是用于new的地址。一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。

三、使用new来创建动态数组

如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做比使用new和指针更简单,尽管给人留下的印象不那么深刻。通常,对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地。例如,假设要编写一个程序,它是否需要数组取决于运行时 用户提供的信息。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。但使用new时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫作动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。

在C++中,创建动态数组很容易;只要将数组的元素类型和元素数目告诉new即可。必须在类型名后加上方括号,其中包含元素数目。

int *a=new int [10];
//创建一个包含十个int元素的数组

new运算符返回第一个元素的地址。在这个例子中,该地址被赋给指针a

当程序使用完new分配的内存块时,应使用delete释放它们。然而, 对于使用new创建的数组,应使用另一种格式的delete来释放。

delete [] a;

方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。请注意delete和指针之间的方括号。 如果使用 new时,不带方括号,则使用delete时,也不应带方括号。如果使用 new时带方括号,则使用delete时也应带方括号。

程序跟踪了创建动态数组时分配的内存量,以便以后使用delete []运算符时能够正确地释放这些内存。 但这种信息不是公用的,例如,不能使用sizeof运算符来确定动态分配的数组包含的字节数。

仍然使用本章的例子,可以将a看作是一根指向该元素的手指。假设int占4个字节,则将手指沿正确的方向移动4个字节, 手指将指向第2个元素。总共有10个元素,这就是手指的移动范围。

现在从实际角度考虑这个问题。如何访问其中的元素呢?第一个元素不成问题。由于a指向数组的第1个元素,因此*a是第1个元素的值。这样,还有9个元素,只要把指针当作数组名使用即可。也就是说,对于第1个元素,可以使用a[0],而不是一定是*a;对于第2个元素,可以使用a[1],依此类推。这样,使用指针来访 问动态数组就非常简单了,虽然还不知道为何这种方法管用。可以这样做的原因是,C和C++内部都使用指针来处理数组。数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题,但这是另一码事)。

#include<iostream>
using namespace std;
int main(){
    double *a=new double [3];
    a[0]=0.1,a[1]=0.2,a[2]=0.3;
    cout<<a[0]<<" "<<*a<<endl;
    cout<<a[1]<<" "<<a[2]<<endl;
    a=a+1;
    cout<<*a<<endl;
    return 0;
}
//输出:
//0.1 0.1
//0.2 0.3
//0.2

上面的程序将指针a当作数组名来使用,a[0]为第1个元素,依次类推。下面的代码行指出了数组名和指针之间的根本差别。

a=a+1;

不能修改数组名的值。但指针是变量,因此可以修改它的值。请注意将a1的效果。表达式a[2]现在指的是数组的第2个值。因此, 将a1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给delete[]提供正确的地址。

相邻的int地址通常相差2个字节或4个字节,而将a加1后,它将指向下一个元素的地址,这表明指针算术有一些特别的地方。情况确实如此。

打赏
评论区
头像
文章目录