赞
踩
最近在学C++,总结一下在堆区申请动态内存空间的使用和释放问题。并通过几个示例代码说明在释放内存时容易发生的错误。内容并不涉及深层次的东西。文中可能有不恰当或者错误的地方,欢迎评论指正!
对于新手来说,动态内存的操作真是令人头大。
因为在学习过程中代码量很少,使用的内存也很小,就算不释放内存空间也不会出现什么问题,既不会报错,也不会因为内存泄漏造成什么影响。
但在释放内存之后,很多时候编译可以通过,但是由于操作不当,程序运行时就会崩溃。又不会指出哪里的问题,导致排错的过程又很麻烦。就算找出了错误,又不明白为什么错了。
所以做了以下笔记,记录了正确的操作方法和容易犯的错误。
需要注意的是:
我们申请和释放的一直都是内存空间,而不是那个指针。指针的作用只是方便我们对内存进行操作。
在C/C++中使用指针来保存一块内存的地址,如使用语句int * p;
定义一个指针 p 来指向一个 int 类型的地址,也可以说变量 p 用来保存一个整形数据的地址。
在C++中,关键字 new 会在堆区申请一块动态内存,并返回动态分配内存块的首字节地址,如语句new char;
会申请一块 char 类型的内存,new int;
会申请一块 int 类型的内存。但我们并不知道这块内存在哪,因为动态内存没有名字,所以我们可以定义一个指针来接收这个地址:int * p = new int;
,还可以用语句int * p = new int(10);
来初始化这块内存的值为10,或者用语句int * p = new int[5];
申请一个数组,这时 p 保存数组的首地址。
下图是内存分配的示意图:
第一条语句申请了一个 int 类型的存储空间,这相当于告诉系统:“ 0x5CFC ~ 0x5CFF 这块空间现在归我了,你就别管了!( ^ _ ^ ) ”
第二条语句申请了一个有5个元素的数组,相当于告诉系统:“ 0x5D04 ~ 0x5D17 这一大块空间现在都归我管了。(* ^ o ^) ”(int 类型一般占4字节)
如果申请成功的话,你就得到一块属于自己的空间了。在之前的编译器中,申请失败会返回 NULL,但现在大多数编译器在申请失败时会抛出异常。
这时我们可以使用 *p1 来获取数值10,还可以像指针和数组那样操作 p2 ,如右下角方框中的两条语句。
上面申请的内存空间如果没有释放的话,会在程序结束后由系统回收。这在申请少量内存时,程序运行并不会出现什么影响,只是浪费系统内存。但在大型项目中,如果不及时释放掉不用的内存,就会导致程序运行速度减慢甚至系统崩溃等严重后果。
在C++中,使用关键字 delete 来释放通过 new 申请到的动态内存,如下面两条代码:
delete p1; //释放p1指向的内存
delete[] p2;//释放p2指向的数组内存
释放内存就相当于告诉系统:“这块内存空间我用完了,你也可以使用了。( * v * ) ”
注意:
1、这里 delete 是释放了指针所指向的内存空间,并不是删除了指针。
2、释放数组内存时不加“ [ ] ”只会释放第一个元素所占的空间。
当语句delete p1;
执行完之后,有的编译器 p1 依然指向那块地址,有的编译器会指向其他地方,这样的指针被叫做野指针,当你不小心又用了这个指针后就会出现乱七八糟的东西。所以为了防止这样的事情发生,我们释放内存之后让指针指向 NULL,这样的指针被叫做空指针 。完整的代码如下:
int * p1 = new int(10); //分配1个int型的内存空间
delete p1; //释放p1指向的内存
p1 = NULL; //使指针p1指向NULL
int * p2 = new int[5]; //分配5个int型的内存空间
delete[] p2; ///释放p2指向的数组内存
p2 = NULL; //使指针p2指向NULL
以下的代码都是错误示例,在编写程序时需要注意。
当你写了如下代码:
int * p1 = new int;
delete p1;
delete p1; //释放野指针
编译是可以通过的,但是程序运行时就会崩溃。当然,我们编写代码时并不会连续写两条相同的 delete,但很多时候两条语句并不在一起,或者操作的并不是同一个指针,就像下面这样:
int * p1 = new int;
int * p2 = p1;
delete p1;
delete p2; //重复释放内存
虽然不是同一个指针,但释放的是同一块内存空间,本质上和上面是一样的(也许编译器的处理方法不一样,这里不做讨论)。
C++ 类的拷贝构造函数会涉及到深拷贝和浅拷贝,浅拷贝有时也会出现重复释放内存的问题。
当你释放的内存不是申请的动态内存,也就是非堆区内存,也会造成程序崩溃:
int a;
int * p1 = new int;
p1 = &a;
delete p1; //释放非堆区内存
我们学 C/C++ 的指针就是为了指来指去的,但是当指向动态内存的指针,在释放该内存之前就指向了其他地方,这样最后 delete 释放的并不是动态内存空间,而真正的需要释放的地址已经找不到了。所以在使用指向动态内存的指针时一定要小心,不然程序崩溃了,你也崩溃了。
我们可以使用 const 关键字来修饰指针,如
int * const p1 = new int;
,之后指针再指向其他地址就会报错。只是这样在后面释放内存之后,这个指针也不能再使用了。
在Dev-C++中,执行delete p1;
之后,p1仍指向原来的空间,只是内存中的值改变了,但仍然可以使用 *p1 来读写,只是这样是做是非常危险的,比如下面代码:
#include <iostream>
using namespace std;
int main(void)
{
int * p1 = new int(10);
cout << "1.*p1: " << *p1 << "\tp1: " << p1 << endl;
delete p1;
cout << "2.*p1: " << *p1 << "\tp1: " << p1 << endl;
*p1 = 20;
cout << "3.*p1: " << *p1 << "\tp1: " << p1 << endl;
return 0;
}
运行结果如下:
以上代码在 VS2017 中编译可以通过,但程序运行时会崩溃。暂时不知道原因,可能是编译器不一样(Dev-C++编译器是GCC,Visual Studio 的默认编译器是 MSVC),也可能是开发环境不一样。
C/C++ 的指针给我们带来更多便利的同时,也赋予我们更多的责任。很多代码错误也许编译时并不会报错,但是会造成严重的后果。我们必须对自己申请的内存负责,对我们的代码负责。
注:文中的代码都是在 Windows 7 操作系统下的 Dev-C++ 和 vs2017 中进行测试的,不同的开发环境可能会产生不同的结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。