Memory 2

内存泄漏

  内存泄漏就是不再使用的内存没有被释放掉。
  最基础的情况,像在C++里的new和delete没有对应使用就一定会发生内存泄漏。C++中进行内存管理有一个通用的做法叫做“RAII”,基本原理就是把内存或是其它要进行“获取”,“使用”,“释放”的资源,封装成C++对象进行管理。利用C++对象的构造函数获取要管理的资源,析构函数释放资源,只要对象不被销毁,资源就一直可以使用。
  在实际场景的C++中对内存的使用是很复杂的,复杂在内存或者说是对象的引用关系上。

引用关系和内存管理

  在C++中引入面向对象以后,我们一般都使用对象来组织代码。然后我们知道对象之间存在着两种引用关系,聚合关系(aggregation)和组合关系(composition)。聚合关系的对象各有独立的生命周期,而组合关系的对象的生命周期未必同时开始但一定会同时结束。比如一部手机和它的CPU,坏了的话肯定是手机和它的CPU一起扔掉,是组合关系;图书馆和里面的书,拆了图书馆,书可以拿到别的地方看而不用被同时烧毁处理,所以是聚合的关系。

Class relation

对象的引用关系

  这些引用关系也是对C++的对象进行内存管理的依据。一般存在引用关系的对象,因为他们还在被使用,所以不应该被销毁;而失去所有被引用关系的对象,不再被使用了就应该被销毁。比如上图中的手机对象销毁的话,CPU、GPU、内存和存储对象,由于组合关系,只有手机对象引用它们,所以都将失去所有引用而一同被销毁;而图中的三种书本对象,由于是聚合关系,只有图书馆和读者都销毁以后书本才会不再被使用而需要被销毁。   这些对象关系和内存管理规则,只用new和delete关键字是不足以进行表达的。

智能指针

  从C++11开始,官方增加了新的内存管理工具到C++语言特性中,它们被叫做智能指针。我们之前所说的对象关系正是可以用智能指针来进行表达。还是手机和图书馆的例子:

#include <iostream>
using std::cout;
#include <memory>
using std::unique_ptr;
using std::shared_ptr;

class CPU
{
public:
	~CPU() { cout << "destroy CPU\n"; }
};

class CellPhone
{
public:
	CellPhone():_cpu(new CPU()) { }
	~CellPhone() { cout << "destroy CellPhone\n"; }
private:
	unique_ptr<CPU> _cpu; //组合关系,CPU只被手机引用
};

class Book
{
public:
	~Book() { cout << "destroy Book\n"; }
};

class Library
{
public:
	Library(const shared_ptr<Book>& book):_book(book) { }
	~Library() { cout << "destroy Library\n"; }
private:
	shared_ptr<Book> _book; //聚合关系
};

class Reader
{
public:
	Reader(const shared_ptr<Book>& book):_book(book) { }
	~Reader() { cout << "destroy Reader\n"; }
private:
	shared_ptr<Book> _book; //聚合关系
};

int main()
{
	/* 手机只被主程序引用 */
	unique_ptr<CellPhone> cellPhone(new CellPhone());

	/* 同书本被主程序引用 */
	shared_ptr<Book> book(new Book());
	/* 图书馆和读者都引用了书本,并都只被主程序引用 */
	unique_ptr<Library> library(new Library(book));
	unique_ptr<Reader> reader(new Reader(book));

	book = nullptr; //解除主程序对book的引用
	library = nullptr; //解除主程序对library的引用,导致library的销毁并解除对book的引用
	reader = nullptr; //解除主程序对reader的引用,导致reader的销毁并解除对book的引用
	cellPhone = nullptr; //解除主程序对cellPhone的引用

	cout << "end\n";
	return 0;
}

/*
输出:
	destroy Library
	destroy Reader
	destroy Book
	destroy CellPhone
	destroy CPU
    end
*/

  上面程序最重要的就是book的对象是在主程序、Library、Reader等都解除对它的引用之后才销毁,而CPU和cellPhone是一起销毁的。完全符合我们设计的对象引用关系以及预期的内存的管理结果。所以以后写C++程序只要认真分析对象间的引用关系,做好设计并正确使用对应的智能指针,我们的内存一定会得到滴水不漏的管理和释放。

组合关系的指针

  unique_ptr指针非常特别,因为它会严格地遵守对象组合关系的特性,当父对象用一个unique_ptr来hold住另一个对象以后,另一个对象的生杀大权就完全被父对象控制了。unique_ptr不能被拷贝,只能用特殊方法来转移引用对象的所有权。但是除非你非常清楚自己要做什么,一般不建议使用转移所有权的功能。

unique_ptr<int> uptr(new int(998));
//unique_ptr<int> uptr2 = uptr; //不能通过编译
unique_ptr<int> uptr2 = std::move(uptr); //显式转移所有权
cout << "uptr: " << uptr << '\n';
cout << "uptr2: " << uptr2 << '\n';
/*
输出:
	uptr: 0x0
	uptr2: 0x1001054a0
*/

引用计数,强引用和弱引用

  shared_ptr的内部实现是通过叫做引用计数的机制(Reference Counting)实现的,简单说就是记录一个对象被引用的次数,当它的被引用次数降为0时就销毁对象。对象的引用行为是通过赋值行为来进行的,包括拷贝构造函数以及赋值函数等等。智能指针重载这些方法,并在方法内进行引用计数的增减判断。引用计数的方法比较简单,对内存管理消耗的运算会比较平均地分摊到程序的各个部分,不容易导致程序在某一时刻因为内存管理而出现卡顿。但是这个方法也有个致命问题,就是循环引用的存在。
  循环引用就是当对象的引用关系成为一个环以后,对象的计数因为之间环形的引用而无法降为0,出现内存泄露(每个对象只会等待上级的对象主动断开对自己的引用,让自己失去引用而被销毁,一旦上级对象又被自己引用了的话,上级对象则不会被销毁而断开自己持有的引用关系,这个对它的等待就遥遥无期了)。

/* 示例,比如书本需要引用自己所属的图书馆,
 这是一个很合理的需求
*/
#include <iostream>
using std::cout;
#include <memory>
using std::shared_ptr;
using std::unique_ptr;

class Library;

class Book
{
public:
	void setOwner(const shared_ptr<Library>& library)
    {
    	_library = library;
    }
    const shared_ptr<Library>& getOwner() const
    {
    	return _library;
    }
	~Book() { cout << "destroy Book\n"; }
private:
	/* 通过智能指针表达引用关系 */
	shared_ptr<Library> _library;
};

class Library
{
public:
	Library(const shared_ptr<Book>& book):_book(book) { }
	~Library() { cout << "destroy Library\n"; }
private:
	shared_ptr<Book> _book;
};

class Reader
{
public:
	Reader(const shared_ptr<Book>& book):_book(book) { }
	~Reader() { cout << "destroy Reader\n"; }
private:
	shared_ptr<Book> _book;
};

int main()
{
	/* 同一本书将被主程序、图书馆和读者引用 */
	shared_ptr<Book> book(new Book());
	/* 图书馆引用了书本,并被主程序引用 */
	shared_ptr<Library> library(new Library(book));
    /* 书本引用它所属的图书馆 */
    book->setOwner(library);
	/* 读者引用了书本,并只被主程序引用 */
	unique_ptr<Reader> reader(new Reader(book));

	book = nullptr; //解除主程序对book的引用
	library = nullptr; //解除主程序对library的引用
	reader = nullptr; //解除主程序对reader的引用
	cout << "end\n";
	return 0;
}

/*
输出:
	destroy Reader
	end
*/

  上面程序显示了循环引用导致的内存泄露。解决办法是通过引入弱引用指针weak_ptr来处理。weak_ptr表达的也是对象的聚合关系,只是弱引用不会增加引用对象的引用计数,引用对象被销毁时自己会自动断开引用。上述程序在使用弱引用指针下,应该将Book修改为:

class Book
{
public:
	void setOwner(const shared_ptr<Library>& library)
    {
    	_library = library;
    }
	shared_ptr<Library> getOwner() const
    {
    	return _library.lock();
    }
	~Book() { cout << "destroy Book\n"; }
private:
	weak_ptr<Library> _library;
};

  这样程序就可以正确运行了,你也可以继续尝试一下先解除程序对library的引用,然后从book获取Owner会怎么样。

	cout << "Book Owner Address: " << book->getOwner() << '\n';
	library = nullptr; //解除主程序对library的引用
	cout << "Book Owner Address: " << book->getOwner() << '\n';
	reader = nullptr; //解除主程序对reader的引用
	book = nullptr; //解除主程序对book的引用
	cout << "end\n";
/*
输出:
	Book Owner Address: 0x1001054a0
	destroy Library
	Book Owner Address: 0x0
	destroy Reader
	destroy Book
	end
*/

  以上就是C++内存管理的工具unique_ptr、shared_ptr、weak_ptr。建议一般C++程序可以完全用它们来取代基础的指针类型,以使对象的生命周期得到自动管理。而在Cocos2d-x中有通过另一种方式实现了引用计数的管理方法,在Dorothy中我也添加了辅助Cocos2d-x对象使用的智能指针oOwn、oRef、oWRef,分别对应C++11中的三种智能指针的功能。

标题目录