本文为大便一箩筐的原创内容,转载请注明出处,谢谢:http://www.cnblogs.com/dbylk/

最近在为公司的项目写内存泄漏定位工具,遇到一些关于C++构造与析构对象的问题,在此记录一下。


一、不要混用 new/delete 和 new[]/delete[]

在默认情况下,也就是不存在 operator new 的重载时,new一个自定义类型 ClassA 的对象时,C++ 会先调用 malloc 来申请一块 sizeof(ClassA) 大小的内存(操作系统会记录这块内存的首地址与大小),然后调用 ClassA 的构造函数在这块内存上初始化对象。此时,new 关键字会返回 malloc 得到的地址。调用delete时,会首先执行 ClassA 的析构函数,再调用 free 释放 malloc 得到的指针。

而new[]则稍微复杂一点,当你调用 new ClassA[nCount] 申请一个对象个数为 nCount 的 ClassA 数组时,编译器(MSVC)会调用 malloc 申请一块大小为 sizeof(ClassA) * nCount + 4 的内存,多出来的 4 bytes 被放在 new[] 关键字返回地址 ptr 的前面,其中记录了数组中元素的个数。当调用 delete[] 删除数组时,会根据数组首地址前 4 bytes 中记录元素的个数来依次调用数组中对象的析构函数(每次指针偏移 sizeof(ClassA) 大小),再调用 free 释放指针 (ptr - 4)。

因此,混用 new/delete 和 new[]/delete[] 通常会导致内存访问崩溃。然后这里用了“通常”,也就是说在某些特定情况下,混用 new/delete 和 new[]/delete[] 是不会有任何影响的:

  1. 创建和释放 C++ 的内建(build-in)类型时,即 int、char等。
  2. 创建和释放“自身和所有成员变量都不含自定义构造函数和析构函数”的类型。(这一条可能依赖于编译器的实现,至少在 MSVC 中此情况成立)

然而当项目代码一旦复杂起来,要分清什么时候上面两个条件能够成立就不是那么轻松的事了,因此最好的方法就是无论何时何地都不要混用 new/delete 和 new[]/delete[]。

二、不要 delete “void” 指针

在整理公司项目代码的过程中,发现有很多地方出现了类似于下面形式的代码:

// Author :大便一箩筐 2016-04-03

struct StructA
{
char cData;
} void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

可能写过类似代码的同学会觉得这种写法并没有什么问题,事实也是如此,它能够正常工作,既不会产生内存泄漏,也不会运行报错。

但是,上面情况只能说是一种幸运的巧合,如果发生一些微小的改变,结果就会发生意想不到的变化:

// Author :大便一箩筐 2016-04-03

struct StructA
{
string strData;
} void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

细心的同学可能已经看出来了,由于 pBuffer 是 void 指针,delete pBuffer 时,并不会调用 StructA 的析构函数,而这导致了 string 的析构函数也没有被调用,最终产生的结果就是 string 中的字符串缓冲泄漏。

有的同学可能会说,C++ 不是支持多态嘛,我把 StructA 的析构函数定义成虚函数不就好了。然而不幸的是,作为 C++ 的内建类型,void 并没有定义析构函数,因此寄希望于多态是行不通的。

还有的同学可能会说,如果想定义 void 类型的内存缓冲区怎么办?    —— 别忘记我们还有 malloc 和 free。

所以,任何时候都不要尝试 delete void 指针。

三、尽量不要手动调用析构函数

看下面的代码,你能看出程序输出结果是什么吗?

// Author :大便一箩筐 2016-03-31

class Base
{
public:
Base() {};
virtual ~Base()
{
cout << "Base has been destructed. " << endl;
} void Release()
{
this->~Base();
} int baseData;
}; class Derive : public Base
{
public:
Derive() {};
virtual ~Derive()
{
cout << "Derive has been destructed. " << endl;
} void Releaase()
{
this->~Derive();
} int deriveData;
}; int main()
{
Base* pObject = new Derive(); pObject->Release();
delete pObject; system("pause"); return 0;
}

我想很多同学会觉得答案是这个:

Derive has been destructed.

Base has been destructed.

Derive has been destructed.

Base has been destructed.

然而很不幸的,正确答案是这个:

Derive has been destructed.

Base has been destructed.

Base has been destructed.

因为在手动调用 pObject 的析构函数时,虽然 pObject 所指向的内存空间并没有被释放,但执行完 Derive 的析构函数后, pObject 所指向对象的虚函数表指针会从指向派生类 Derive 的虚函数表恢复为指向基类 Base 的虚函数表。

即手动调用析构函数之后,多态指针 pObject 失去了自己的多态特性,此时无法再通过 pObject 直接调用派生类中的虚函数。因此,最后 delete 时只会调用基类的析构函数。

所以,尽量不要在自己写的函数中手动调用析构函数是一个好习惯。

然而上面提到了“尽量”,那就是说事情并没有那么绝对,C++ 支持手动调用析构函数,自然有它的道理:当你需要自己写内存管理器时,手动调用析构函数是必须的。

使用C++为对象分配与释放内存时的几个好习惯的更多相关文章

  1. Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  2. 晓说智能指针shared_ptr为何可以实现跨模块分配和释放内存

    最近做项目, 有个地方是外包人员写的, 其中有个函数,大致这样 void getInfo(std::shared_ptr<Info>& outInfo); 这个函数是一个dll(链 ...

  3. c++ 分配与释放内存

    教学内容: calloc分配内存 calloc与malloc的区别 memset函数初始化内存 free释放动态分配的内存 一.calloc函数分配内存 void *calloc( size_t nu ...

  4. 析构函数释放内存时出现_BLOCK_TYPE_IS_VALID错误

    错误信息截图: 原因: 1.内存泄漏:所以当程序退出时,系统会收回分配的内存,于是调析构函数,由于内存已被错误地释放,于是就会出现"Debug Assertion Failed"的 ...

  5. C++学习011-常用内存分配及释放函数

    C++用有多种方法来分配及释放内存,下面是一些经常使用的内存分配及释放函数 现在我还是一个技术小白,一般用到也指示 new+delete 和 malloc和free 其他的也是在学习中看到,下面的文字 ...

  6. JVM学习-之对象的创建和内存分配

    最近看JVM内存模型,看了很多文章,大都讲到JVM将内存区域划分分:Mehtod-Area(No heap) 方法区,Heap(堆)区,Program Counter Register(程序计数器), ...

  7. DLL函数中内存分配及释放的问题

    DLL函数中内存分配及释放的问题 最近一直在写DLL,遇到了一些比较难缠的问题,不过目前基本都解决了.主要是一些内存分配引起问题,既有大家经常遇到的现象也有特殊的 情况,这里总结一下,做为资料. 错误 ...

  8. Com组件的内存分配和释放,CredentialProvider SHStrDup 字符串拷贝问题

    一.简单介绍 熟悉CredentialProvider的同学应该知道,他为一个Com组件,于是,在这里的内存分配(字符串拷贝)的一系列操作就要依照con的标准来. 二.Com组件的内存分配和释放 CO ...

  9. 如何在MD(d)和MT(d)工程间正确分配和释放动态内存

    MD(d)和MT(d) MD(d)和MT(d)是windows下VC开发的两个编译选项,表示程序的运行时库编译选项. /MT是"multithread, static version&quo ...

随机推荐

  1. 关于cgi、FastCGI、php-fpm、php-cgi(复制)

    首先,CGI是干嘛的?CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者. web server(比如说nginx)只是内容的分发者.比如,如果请求/index.h ...

  2. tensorboard实现训练的可视化

    tensorboard是tensorflow自带的可视化工具 输入命令可以启动tensorboard服务. tensorboard --logdir=your log dir 通过浏览器localho ...

  3. 20145316 《Java程序设计》第1周学习总结

    20145316 <Java程序设计>第1周学习总结 教材学习内容总结 一.了解java语言: 1.Java是一种可以撰写跨平台应用程序的面向对象的程序设计语言. Java 技术具有卓越的 ...

  4. LINUX SHELL 笔记 01: 脚本

    root@iZwz:~/labs# vim myfirst root@iZwz:~/labs# cat myfirst #!/bin/bash clear echo "this is my ...

  5. Linux各目录缩写含义

    Unix已经有35年历史了.许多人认为它开始于中世纪,这个中世纪是相对于计算机技术的产生和发展来说的.在过去的时间里,Unix和它的子分支Linux收集有许多的历史和一些完全古老的语言.在这篇技巧文章 ...

  6. [pixhawk笔记]7-MAVLink添加自定义消息

    前一篇学习了uORB,用于px4中各个模块的进程间通信,下来学习MAVLink,用于飞控和地面站之间的通信.教程中主要给出了使用MAVLink的发送和接收消息的方法.完整的MAVLink消息列表见该网 ...

  7. C/C++中RAND_MAX的用法

    RAND_MAX是C中stdlib.h中宏定义的一个字符常量: #define RAND_MAX Ox7FFF 其值最小为32767,最大为2147483647 通常在产生随机小数时可以使用RAND_ ...

  8. ImportError: No module named Crypto.PublicKey

    答: sudo apt-get install python-pip  (如果没有安装pip的话,需要这一操作) pip install pycrypto

  9. linux网络连接的查看和端口的监听

    网络软件都是由客户端和服务端组成,由服务端在服务器上监听指定的端口处理接收到的数据,而客户端是向服务器端监听的端口发送数据,并由服务器端对该数据进行处理,然后将处理结果返回给客户端. 那么我们在lin ...

  10. RDB

    在运行情况下, Redis 以数据结构的形式将数据维持在内存中, 为了让这些数据在 Redis 重启之后仍然可用, Redis 分别提供了 RDB 和 AOF 两种持久化模式. 在 Redis 运行时 ...