释放内存时可以释放非动态申请的内存,在程序中申请使用了内存后有没有释放

  释放内存时可以释放非动态申请的内存,在程序中申请使用了内存后有没有释放

  计算机程序由代码和数据组成。一个程序占用的内存可以分为代码段和数据段,而数据区又分为常量存储区、静态存储区、堆和栈。这里主要讨论堆内存和栈内存。

  堆栈内存是自动请求和释放的。请求的内存在变量范围内有效,当它退出变量范围时被释放。这个过程由编译器完成,安全系数相对较高,效率也比堆内存高。程序员显式地请求和释放堆上的内存。如果只是应用没有释放,就会造成内存泄漏。应用后反复释放会导致程序崩溃。所以显式申请内存更“不安全”。

  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。

  我们习惯用指针指向一段记忆,却无法被指针华丽的外表所迷惑。我们的目的不是防止指针被重复删除,而是防止同一块内存被重复释放。既要防止显性重复发布,也要防止隐性重复发布。这样,我们的代码更加安全。

释放内存时可以释放非动态申请的内存,在程序中申请使用了内存后有没有释放