关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍

本文节选自文章:
内存泄漏-原因、避免以及定位

本文总结常见内存泄漏的几种方式,留意到这几点,可以避免95+%以上的内存泄漏

未释放

这种是很常见的,比如下面的代码:

int fun() {char * pBuffer = malloc(sizeof(char));/* Do some work */return 0;
}

上面代码是非常常见的内存泄漏场景(也可以使用new来进行分配),我们申请了一块内存,但是在fun函数结束时候没有调用free函数进行内存释放。

在C++开发中,还有一种内存泄漏,如下:

class Obj {public:Obj(int size) {buffer_ = new char;}~Obj(){}private:char *buffer_;
};int fun() {Object obj;// do sthreturn 0;
}

上面这段代码中,析构函数没有释放成员变量buffer_指向的内存,所以在编写析构函数的时候,一定要仔细分析成员变量有没有申请动态内存,如果有,则需要手动释放,我们重新编写了析构函数,如下:

~Object() {delete buffer_;
}

在C/C++中,对于普通函数,如果申请了堆资源,请跟进代码的具体场景调用free/delete进行资源释放;对于class,如果申请了堆资源,则需要在对应的析构函数中调用free/delete进行资源释放。

未匹配

在C++中,我们经常使用new操作符来进行内存分配,其内部主要做了两件事:

  1. 通过operator new从堆上申请内存(glibc下,operator new底层调用的是malloc)
  2. 调用构造函数(如果操作对象是一个class的话)

对应的,使用delete操作符来释放内存,其顺序正好与new相反:

  1. 调用对象的析构函数(如果操作对象是一个class的话)
  2. 通过operator delete释放内存
void* operator new(std::size_t size) {void* p = malloc(size);if (p == nullptr) {throw("new failed to allocate %zu bytes", size);}return p;
}
void* operator new[](std::size_t size) {void* p = malloc(size);if (p == nullptr) {throw("new[] failed to allocate %zu bytes", size);}return p;
}void  operator delete(void* ptr) throw() {free(ptr);
}
void  operator delete[](void* ptr) throw() {free(ptr);
}

为了加深多这块的理解,我们举个例子:

class Test {public:Test() {std::cout << "in Test" << std::endl;}// other~Test() {std::cout << "in ~Test" << std::endl;}
};int main() {Test *t = new Test;// do sthdelete t;return 0;
}

在上述main函数中,我们使用new 操作符创建一个Test类指针

  1. 通过operator new申请内存(底层malloc实现)
  2. 通过placement new在上述申请的内存块上调用构造函数
  3. 调用ptr->~Test()释放Test对象的成员变量
  4. 调用operator delete释放内存

上述过程,可以理解为如下:

// new
void *ptr = malloc(sizeof(Test));
t = new(ptr)Test// delete
ptr->~Test();
free(ptr);

好了,上述内容,我们简单的讲解了C++中new和delete操作符的基本实现以及逻辑,那么,我们就简单总结下下产生内存泄漏的几种类型。

new 和 free

仍然以上面的Test对象为例,代码如下:

Test *t = new Test;
free(t)

此处会产生内存泄漏,在上面,我们已经分析过,new操作符会先通过operator new分配一块内存,然后在该块内存上调用placement new即调用Test的构造函数。而在上述代码中,只是通过free函数释放了内存,但是没有调用Test的析构函数以释放Test的成员变量,从而引起内存泄漏

new[] 和 delete

int main() {Test *t = new Test [10];// do sthdelete t;return 0;
}

在上述代码中,我们通过new创建了一个Test类型的数组,然后通delete操作符删除该数组,编译并执行,输出如下:

in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in ~Test

从上面输出结果可以看出,调用了10次构造函数,但是只调用了一次析构函数,所以引起了内存泄漏。这是因为调用delete t释放了通过operator new[]申请的内存,即malloc申请的内存块,且只调用了t[0]对象的析构函数,t[1…9]对象的析构函数并没有被调用。

虚析构

记得08年面谷歌的时候,有一道题,面试官问,std::string能否被继承,为什么?

当时没回答上来,后来过了没多久,进行面试复盘的时候,偶然看到继承需要父类析构函数为virtual,才恍然大悟,原来考察点在这块。

下面我们看下std::string的析构函数定义:

~basic_string() { _M_rep()->_M_dispose(this->get_allocator());
}

这块需要特别说明下,std::basic_string是一个模板,而std::string是该模板的一个特化,即std::basic_string

typedef std::basic_string<char> string;

现在我们可以给出这个问题的答案:不能,因为std::string的析构函数不为virtual,这样会引起内存泄漏

仍然以一个例子来进行证明。

class Base {public:Base(){buffer_ = new char[10];}~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}
private:char *buffer_;};class Derived : public Base {public:Derived(){}~Derived() {std::cout << "int Derived::~Derived" << std::endl;}
};int main() {Base *base = new Derived;delete base;return 0;
}

上面代码输出如下:

in Base::~Base

可见,上述代码并没有调用派生类Derived的析构函数,如果派生类中在堆上申请了资源,那么就会产生内存泄漏

为了避免因为继承导致的内存泄漏,我们需要将父类的析构函数声明为virtual,代码如下(只列了部分修改代码,其他不变):

~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}

然后重新执行代码,输出结果如下:

int Derived::~Derived
in Base::~Base

借助此文,我们再次总结下存在继承情况下,构造函数和析构函数的调用顺序。

派生类对象在创建时构造函数调用顺序:

  1. 调用父类的构造函数
  2. 调用父类成员变量的构造函数
  3. 调用派生类本身的构造函数

派生类对象在析构时的析构函数调用顺序:

  1. 执行派生类自身的析构函数
  2. 执行派生类成员变量的析构函数
  3. 执行父类的析构函数

为了避免存在继承关系时候的内存泄漏,请遵守一条规则:无论派生类有没有申请堆上的资源,请将父类的析构函数声明为virtual

循环引用

在C++开发中,为了尽可能的避免内存泄漏,自C++11起引入了smart pointer,常见的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已经被废弃),其中weak_ptr是为了解决循环引用而存在,其往往与shared_ptr结合使用。

下面,我们看一段代码:

class Controller {public:Controller() = default;~Controller() {std::cout << "in ~Controller" << std::endl;}class SubController {public:SubController() = default;~SubController() {std::cout << "in ~SubController" << std::endl;}std::shared_ptr<Controller> controller_;};std::shared_ptr<SubController> sub_controller_;
};int main() {auto controller = std::make_shared<Controller>();auto sub_controller = std::make_shared<Controller::SubController>();controller->sub_controller_ = sub_controller;sub_controller->controller_ = controller;return 0;
}

编译并执行上述代码,发现并没有调用Controller和SubController的析构函数,我们尝试着打印下引用计数,代码如下:

int main() {auto controller = std::make_shared<Controller>();auto sub_controller = std::make_shared<Controller::SubController>();controller->sub_controller_ = sub_controller;sub_controller->controller_ = controller;std::cout << "controller use_count: " << controller.use_count() << std::endl;std::cout << "sub_controller use_count: " << sub_controller.use_count() << std::endl;return 0;
}

编译并执行之后,输出如下:

controller use_count: 2
sub_controller use_count: 2

通过上面输出可以发现,因为引用计数都是2,所以在main函数结束的时候,不会调用controller和sub_controller的析构函数,所以就出现了内存泄漏

上面产生内存泄漏的原因,就是我们常说的循环引用

为了解决std::shared_ptr循环引用导致的内存泄漏,我们可以使用std::weak_ptr来单面去除上图中的循环。

class Controller {public:Controller() = default;~Controller() {std::cout << "in ~Controller" << std::endl;}class SubController {public:SubController() = default;~SubController() {std::cout << "in ~SubController" << std::endl;}std::weak_ptr<Controller> controller_;};std::shared_ptr<SubController> sub_controller_;
};

在上述代码中,我们将SubController类中controller_的类型从std::shared_ptr变成std::weak_ptr,重新编译执行,结果如下:

controller use_count: 1
sub_controller use_count: 2
in ~Controller
in ~SubController

从上面结果可以看出,controller和sub_controller均以释放,所以循环引用引起的内存泄漏问题,也得以解决。

可能有人会问,使用std::shared_ptr可以直接访问对应的成员函数,如果是std::weak_ptr的话,怎么访问呢?我们可以使用下面的方式:

std::shared_ptr controller = controller_.lock();

即在子类SubController中,如果要使用controller调用其对应的函数,就可以使用上面的方式。

关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍

常见的【内存泄漏】姿势相关推荐

  1. UWP开发入门(十六)——常见的内存泄漏的原因

    原文:UWP开发入门(十六)--常见的内存泄漏的原因 本篇借鉴了同事翔哥的劳动成果,在巨人的肩膀上把稿子又念了一遍. 内存泄漏的概念我这里就不说了,之前<UWP开发入门(十三)--用Diagno ...

  2. 5 个 Android 开发中比较常见的内存泄漏问题及解决办法

    Android开发中,内存泄漏是比较常见的问题,有过一些Android编程经历的童鞋应该都遇到过,但为什么会出现内存泄漏呢?内存泄漏又有什么影响呢? 在Android程序开发中,当一个对象已经不需要再 ...

  3. 常见的内存泄漏原因及解决方法

    常见的内存泄漏原因及解决方法 参考文章: (1)常见的内存泄漏原因及解决方法 (2)https://www.cnblogs.com/leeego-123/p/12187677.html 备忘一下.

  4. 【Android 内存优化】垃圾回收算法 ( 内存优化总结 | 常见的内存泄漏场景 | GC 算法 | 标记清除算法 | 复制算法 | 标记压缩算法 )

    文章目录 一. 内存优化总结 二. 常见的内存泄漏场景 三. 内存回收算法 四. 标记-清除算法 ( mark-sweep ) 五. 复制算法 六. 标记-压缩算法 一. 内存优化总结 内存泄漏原理 ...

  5. Android 系统(87)---常见的内存泄漏原因及解决方法

    常见的内存泄漏原因及解决方法 (Memory Leak,内存泄漏) 为什么会产生内存泄漏? 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被 ...

  6. Android常见的内存泄漏分析

    内存泄漏原因 当应用不需要在使用某个对象时候,忘记释放为其分配的内存,导致该对象仍然保持被引用状态(当对象拥有强引用,GC无法回收),从而导致内存泄漏. 常见的内存泄漏源头 泄漏的源头有很多,有开源的 ...

  7. Dreamwear如何创建javascript_内存管理+如何处理4种常见的内存泄漏

    JavaScript是如何工作的:内存管理+如何处理4种常见的内存泄漏 潮水自会来去,但心志得坚若磐石.即便成不了那根定海神针,也至少不是那随意被拍上岸的野鬼游魂.by 一枚热汤圆 几周前,我们开始了 ...

  8. JS中常见的内存泄漏及识别方式

    JavaScript常见的内存泄漏及识别方式 1.什么是内存 2.什么是内存泄漏 3.内存泄漏导致的后果 4.常见的内存泄漏 (1)全局变量引起的内存泄漏 (2)闭包引起的内存泄漏 (3)被遗忘的定时 ...

  9. java内部类内存泄漏,Android中常见的内存泄漏和解决方案

    什么是内存泄漏? 简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏. 为什么会产生内存泄漏,内存泄漏会导致什么问题? 相比C++需要手动去 ...

  10. JS中4种常见的内存泄漏

    一.什么是内存泄漏 本质上讲,内存泄漏是当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或空闲内存池的现象. 二.几种常见的内存泄漏 1.意外的全局变量 一个未声明变量的引 ...

最新文章

  1. 2021年大数据基础(一):大数据概念
  2. 【抬杠】在某些时候不希望用户缩小浏览器的宽度,因为咳咳~会导致你的布局混乱,那么这个代码就是帮助你如何限制浏览器宽度的
  3. 5.Ubuntu下的GIF录制软件peek安装
  4. mxnet 配置gpu
  5. 7.STM32中对DMA_Config()函数的理解(自定义)测试DMA传输数据时CPU还可继续工作其他的事
  6. js字符串slice_JavaScript子字符串示例-JS中的Slice,Substr和Substring方法
  7. Visual Studio .Net团队开发[转]
  8. 重学C++语言之路:C++语言学习工具和环境
  9. [设计] - 判断LOGO好坏的12条参考标准
  10. 职场这样发邮件,你死定了!
  11. iOS中Lua脚本应用笔记一:脚本概念相关
  12. 论文阅读_TASE: Reducing Latency of Symbolic Execution with Transactional Memory
  13. 电阻元件、电感元件、电容元件
  14. html按钮位置设置吗,html改变button按钮位置
  15. 统一网关Gateway、路由断言工厂、路由过滤器及跨域问题处理
  16. 半导体车间净化工程的空气洁净度划分等级
  17. wkhtmltopdf(thead)分页问题
  18. c语言中at指令的比较,AT指令(中文详解版)(二)
  19. ⚡️狂神Linux学习笔记
  20. 嵌入式基础01【转载】详解大端模式和小端模式

热门文章

  1. ubuntu 16.04彻底卸载nginx
  2. 复制文件提示一个意外错误使您无法复制该文件win7?
  3. 修改Cmder默认命令提示符
  4. java--跳跃游戏
  5. Android之---仿斗鱼直播弹屏效果的实现
  6. 蓝桥杯———数字三角形(JAVA)
  7. 基于51单片机智能台灯pwm调光强光控方案原理图设计
  8. Python断点调试方法
  9. 瀛海威/瀛海威:Internet先烈
  10. select2 如何自定义提示信息