本文为大便一箩筐的原创内容,转载请注明出处,谢谢: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. Oracle Union Union All 对查询结果集操作

    在Oracle中提供了三种类型的集合操作: 并(UNION).交(INTERSECT).差(MINUS) Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序: Union Al ...

  2. 什么是阻塞式和非阻塞io流?

    阻塞IO:socket 的阻塞模式意味着必须要做完IO 操作(包括错误)才会返回. 非阻塞IO:非阻塞模式下无论操作是否完成都会立刻返回,需要通过其他方式来判断具体操作是否成功. 两者区别: 所谓阻塞 ...

  3. SpringBoot入门学习(二)

    第一讲我们已经讲解了入门Demo,这一讲我们主要讲解包含以下内容 项目内一些属性配置 自定义属性配置 ConfigurationProperties配置 (1)第一个工程创建的时候会自动在工程下创建a ...

  4. MongoDB使用中的一些问题

    1.count统计结果错误 这是由于分布式集群正在迁移数据,它导致count结果值错误,需要使用aggregate pipeline来得到正确统计结果,例如: db.collection.aggreg ...

  5. FFmpeg 入门(4):线程分治

    本文转自:FFmpeg 入门(4):线程分治 | www.samirchen.com 概览 上一节教程中,我们使用 SDL 的音频相关的函数来支持音频播放.SDL 起了一个线程来在需要音频数据的时候去 ...

  6. web worker 的传值方式以及耗时对比

    背景 前一阵子开发的项目 pptx 导入, 由于自己的代码问题,引起了个性能问题,一个 40p 的 pptx 文件,转换成 json 数据,大概要耗时 60s+ ,虽然后面发现是某个使用频率非常高的函 ...

  7. javascript 判断数据类型的几种方法

    javascript 判断数据类型的几种方法一.typeof 直接返回数据类型字段,但是无法判断数组.null.对象 typeof 1 "number" typeof NaN &q ...

  8. 关于office word 应用程序下载配置

    Retrieving the COM class factory for component with CLSID {000209FF-0000-0000-C000-000000000046} fai ...

  9. 访问ashx一般应用程序

    浏览器中的地址栏键入要访问页面的地址:回车(是和服务器软件打交道)----向服务器发送请求(以http协议为基础,服务器按照此协议解释理解接收到的数据),服务器接收到发送的请求,根据请求信息知道当前所 ...

  10. v4l2的学习建议和流程解析

    v4l2,一开始听到这个名词的时候,以为又是一个很难很难的模块,涉及到视频的处理,后来在网上各种找资料后,才发现其实v4l2已经分装好了驱动程序,只要我们根据需要调用相应的接口和函数,从而实现视频的获 ...