释放内存时可以释放非动态申请的内存,在程序中申请使用了内存后有没有释放
计算机程序由代码和数据组成。一个程序占用的内存可以分为代码段和数据段,而数据区又分为常量存储区、静态存储区、堆和栈。这里主要讨论堆内存和栈内存。
堆栈内存是自动请求和释放的。请求的内存在变量范围内有效,当它退出变量范围时被释放。这个过程由编译器完成,安全系数相对较高,效率也比堆内存高。程序员显式地请求和释放堆上的内存。如果只是应用没有释放,就会造成内存泄漏。应用后反复释放会导致程序崩溃。所以显式申请内存更“不安全”。
c可以通过new运算符从堆中申请内存,通过delete显式释放内存。让我们看看下面的程序:
A级
{
公共:
int数据;
};
int main()
{
A *pA=新A;-
STD:cout pA-data STD:endl;-
删除pA;-
返回0;
}
上面的代码表面上看没问题,实际上很不安全。首先,从堆中新内存不一定成功,但直接访问处指针指向的对象是非常危险的,违背了“指针使用前应判断为空”的原则;另外,在释放内存也是一个坏习惯。一个比较好的释放一块内存的习惯是:首先判断指针是否为空,释放(delete),设置为NULL,防止出现野指针。
C delete总共做了两件事:调用对象的析构函数和释放内存。删除指针做的事情无非就是这样:现在有一个指针指向一块内存,调用指针指向的对象的析构函数,将指针指向的内存放入内存释放队列。注:释放一块内存不代表这块内存没有了。唯一的变化是这块内存中的标志位由“使用中”变为“未使用”,这块内存仍然存在。指针仍然指向这块内存的第一个地址。这时,如果你再次释放这个内存,换句话说,再次修改这个标志位,编译器是不会容忍的,再次访问这个指针所指向的内存也会导致意想不到的结果。请看下面的代码:
int main()
{
A *pA=新A;-
STD:cout pA-data STD:endl;-
删除pA;-
STD:cout pA-data STD:endl;-
删除pA;-
返回0;
}
上面的代码在处释放指针,在处访问pA指向内存的一个变量。在这种情况下,如果这个内存没有被占用,被访问的数据可能仍然是正常的,但是如果这个内存被其他线程占用,就会得到意想不到的结果。至于[5]处的操作,相当于把一个标志位从0改成0,这是编译器不允许的,肯定会造成系统内核转储。
因此,对于指针的使用,我们要求指针在使用前应该为空;要严格按照三部曲删除指针。
对于删除,我们可以实现以下宏:
#定义删除(p)
做
{
if(NULL!=p)
{
删除(p);
p=NULL
}
}
while(0)
所以,让我们重写上面的主函数:
int main()
{
A *pA=新A;-
if(NULL!=pA)
{
STD:cout pA-data STD:endl;-
}
删除(pA);-
if(NULL!=pA)
{
STD:cout pA-data STD:endl;-
}
删除(pA);-
返回0;
}
在坚持了上面的应用和释放原则后,和处的代码其实什么都不做,处的代码根本不会被执行,所以相对更安全。
但是我们不是坚持了应用和发布的原则,有了删除之后,一切都好了吗?不,请看下面的代码:
无效函数(A* pA)
{
if(NULL!=pA)
{
STD:cout pA-data STD:endl;
}
删除(pA);-
}
int main()
{
A* pA=新A;
if(NULL!=pA)
{
STD:cout pA-data STD:endl;
}
删除(pA);-
}
上面的代码有问题吗?答案是肯定的。表面上看,用我们自己的宏删除指针应该没有问题。
但实际上程序的执行是这样的:在main函数中pA指向了一个新的内存,但是在调用function方法的时候,指针作为临时变量被再次复制,也就是说两个指针指向了同一个内存区域。我们假设新复制的指针是pB。进入功能函数时,实际效果如下图所示:
退出功能时的实际效果如下:
也就是说pA指向一个已经释放的内存,但是pA不为空。此时,在主函数中删除pA时,判断null无效,从而释放一个已释放的内存,程序核心转储。
另外,我们再来看看复制施工带来的隐患。
对于一个类,应该有一个构造函数、一个析构函数和一个复制构造函数。如果程序员没有定义这三个函数,编译器会默认生成这三个函数。对于复制构造函数,编译器生成的复制构造函数会使用“逐位复制”,即所谓的“浅层复制”。请看下面的代码:
B类
{
公共:
乙()
{
m_pA=新A;
}
~B()
{
删除(m _ pA);
}
私人:
A * m _ pA
}
无效函数
{
}
int main()
{
B* pB=新B;
函数(* pB);
删除(pB);
}
上面的代码有问题吗?可惜也有问题。当进入function函数时,*pB被复制并放入堆栈空间。当function函数退出时,堆栈空间上的对象被释放,B的析构函数被调用,所指向的内存已经被释放。在退出主函数时,再进行一次删除,相当于反复调用B的析构函数来反复释放一块内存,会给程序带来致命的后果。
解决上述问题的一种方法是使用引用传递而不是值传递,这样在堆栈退出时就不会有复制构造和析构。其实我们并不赞成用值传递来处理内部结构,因为这样会做很多复制构造和析构,对程序的性能也会有很大的影响。
解决问题的另一个方法是在B类中添加一个私有的复制构造函数(只能声明,不能实现),这样上面的代码在编译的时候就会报错,不会出现运行时bug。
我们习惯用指针指向一段记忆,却无法被指针华丽的外表所迷惑。我们的目的不是防止指针被重复删除,而是防止同一块内存被重复释放。既要防止显性重复发布,也要防止隐性重复发布。这样,我们的代码更加安全。