Effective C++(四): 资源管理


为了防止忘记调用 delete 造成的内存泄露,我们应该尽可能让对象管理资源,并且采用 RAII 机制(Resource Acquisition is Initialize)机制,让析构函数负责资源的释放。

一、智能指针驱动的RAII

在cpp11中,可以使用unique_ptr 或者 shared_ptr两种智能指针来管理内存。其中 unique_ptr 通过专一所有权来管理 RAII 的对象,而shared_ptr通过引用计数来管理。

std::unique_ptr pUniqueInv1(CreateInvestment());
std::unique_ptr pUniqueInv2(std::move(pUniqueInv1));
std::shared_ptr pSharedInv1(CreateInvestment());

std::shared_ptr pSharedInv2(pSharedInv1); /
std::shared_ptr pSharedInv2(std::move(pSharedInv1))

std::move(pSharedInv1)返回的是pSharedInv1的右值引用,也就是一个std::shared_ptr&&类型,在执行完这句之后,pSharedInv1就变成了一个空指针 nullptr,而pSharedInv2现在拥有原本属于pSharedInv1 的对象。 请注意在调用std::shared_ptr 的移动构造函数的时候,shared_ptr的引用技术不变。

智能指针默认会自动 delete 所持有的对象,我们也可以为智能指针指定所管理对象的释放方式(删除器deleter):

// void GetRidOfInvestment(Investment*) {}

std::unique_ptr<Investment, decltype(GetRidOfInvestment)*> pUniqueInv(CreateInvestment(), GetRidOfInvestment);
std::shared_ptr pSharedInv(CreateInvestment(), GetRidOfInvestment);

在这里decltype的作用是:

二、shared_ptr 和 weak_ptr

一个很常见的面试问题是能否使用 weak_ptr来实现 RAII ? 答案显然是否定的。 首先介绍一下weak_ptr, weak_ptr是一种用于解决 shared_ptr的循环计数死锁的智能指针。一个例子如下:

#include <memory>
#include <vector>

class Child;

class Parent {
public:
    std::vector<std::shared_ptr<Child>> children;

    void addChild(const std::shared_ptr<Child>& child) {
        children.push_back(child);
    }

    ~Parent() {
        // 析构函数
    }
};

class Child {
public:
    std::shared_ptr<Parent> parent;

    Child(const std::shared_ptr<Parent>& p) : parent(p) {}

    ~Child() {
        // 析构函数
    }
};

可以看到 child 和 parent 互相持有对方的shared_ptr, 造成循环引用。 哪怕当这些对象超出作用御的时候他们的析构函数也不会被调用,从而导致内存泄露。

为了解决这个问题,我们可以使用 weak_ptr 解决循环引用,我们可以把 Child 中的shared_ptr 改成 weak_ptr :

class Child {
public:
    std::weak_ptr<Parent> parent; // 使用 weak_ptr 而非 shared_ptr

    Child(const std::shared_ptr<Parent>& p) : parent(p) {} //这里参数仍然是shapred_ptr 因为 1. shared_ptr => weak_ptr 转换是兼容的。 2.确保有一个 shared_ptr<Parent>存在保证 Parent 对象存活。

    ~Child() {
        // 析构函数
    }
};

三、如何复制 RAII 对象

  1. 引用计数
    正如 shared_ptr一样,对于每一个资源对象的每一次复制就让引用计数 + 1, 每一个对象离开定义域调用析构函数使得引用计数 - 1, 直到引用计数为 0 就把资源销毁。

  2. 深拷贝
    在拷贝的时候不但copy 对象,同时 copy 底层资源,比如:
    请注意,在拷贝的时候,有三点是非常需要注意的:
    1.是否拷贝了底层资源对象
    2.是否 handle 了自赋值
    3.是否 handle 了异常处理

#include<iostream>
#include<cstring>

class RAIIArray { 
public:
	RAIIArray(const char* str) {
		if(str) {
			len_ = std::strlen(str) + 1;
			data_ = new char[len];
			strcpy(data_, str);
		} else {
			data_ = nullptr;
			len_ = 0;
		}
	}
	
	//Deepcopy for Copy Constructor 
	RAIIArray(const RAIIArray& other) { 
		size_ = other.size_;
		if(size_ > 0) {
			data_ = new char[size_];
			std::strcpy(data_, other.data_);
		}  else { 
			data_ = nullptr;
		}
	}
	
	//Deepcopy for operator= 
	RAIIArray& operator=(const RAIIArray& other) { 
	    //Identity Judgement
		if(this != &other) {
			delete data;
			size = other.size;
			if(size > 0) {
				data = new char[size];
				std::strcpy(data, other.data); 
			} else {
				data = nullptr;
				size = 0;
			}
		} 
		return *this;
	
	}
	//You can also use this : first explicit call copy constructor then copy & swap
	RAIIArray& operator=(const RAIIArray& other) { 
		if(this != other) {
			RAIIArray temp(other); //显shi 调用了拷贝构造函数
			swap(*this, other);
		}
		return *this;
	}
	
	//use friend to access the private variables in first and second 
	friend void swap( RAIIArray& first,  RAIIArray& second) {
		using std::swap;
		swap(first.size, second.size);
		swap(first.data, second.data);
	}
	//Deepcopy with copy-and-swap
	RAIIArray& operator=(RAIIArray other) { 
		swap(*this,other);
		return *this;
	}


}
  1. 转移底层资源所有权
    和std::unique_ptr类似,永远保持只有一个对象拥有对资源的管理权,当需要复制对象的时候转移资源的管理权。

四、在资源管理类中应该提供对原始资源的访问函数

和所有的智能指针一样,stl 中的智能指针也提供了对原始资源的隐藏访问和显示访问
Investment* pRaw = pSharedInv.get(); // 显示访问
Investment raw = *pSharedInv; //隐式访问
这里,pSharedInv是一个 shared_ptr, 指向Investment类型

这里给出一个例子:


#include<iostream>
#include<memory>
#include<string>


//在资源管理类中,我们应该同时声明显示访问资源的方法和隐式访问资源的方法
class Font { 
private:
  int* handle;
public: 
  Font() { 
    handle = new int(10);
  }
  ~Font() {
    delete handle; 
  }
  //显示访问原始资源
  //在非常量对象上优先调用上面的函数,如果上面的函数没有被定义就会去调用下面的函数
  int* Get() { return handle; }
  int* Get() const { return handle; }
  //隐式访问原始资源
  operator int*() const { return handle; }
};

int main() {
  Font font; 
  int* fontHandleExplicit = font.Get();
  int* fontHandleImplicit = font; 
  std::cout << "Font handle (explicit): " << *fontHandleExplicit << "\n";
  std::cout << "Font handle (implicit): " << *fontHandleImplicit << "\n";

  int* array = new int[10];
  int* object = new int;

  delete[] array; //正确
  delete object; //正确

  //使用 typedef 定义的数组类型
  typedef std::string AddressLines[4];

  //注意这个 new 返回的是 string 的指针,而不是 string 数组的指针 
  std::string* pal = new AddressLines;

  //std::string* pal = new std::string[4];

  // 使用 pal...
  (pal)[0] = "Line 1";
  (pal)[1] = "Line 2";
  (pal)[2] = "Line 3";
  (pal)[3] = "Line 4";

    // 输出数组中的元素
  for (int i = 0; i < 4; ++i) {
        std::cout << (pal)[i] << std::endl;
  }
  //delete pal; //行为未定义
  delete[] pal;//正确

  auto pUnique = std::make_unique<int>(5);
  auto pShared = std::make_shared<int>(10);

  std::cout<<"unique : "<<*(pUnique.get())<<", unique : "<<*pUnique<<std::endl; 
  std::cout<<"shared : "<<*(pShared.get())<<",shared  : "<<*pShared<<std::endl;
  return 0;

}

指的注意的是在搭配使用 typedef 和 new 使用的时候,要关注 new 返回的是 传入 new 这个 operator 的参数的指针, 对于 new int[4], 应该看作是先执行了先返回了一个 int*的指针,然后这个指针指向了 4 块包含 int 元素的内存。不能把其类型看作是int** .