Effective C++学习笔记(三)

条款13

用对象来管理资源

  • 为防止资源泄露,使用资源管理对象来构造资源和析构资源,最好在获取资源的同时进行初始化(RAII)
  • 最常见的资源管理对象为智能指针,std::auto_ptrstd::shared_ptr,通常 std::shared_ptr会更好。但是在使用时一定要将对象指针存储于智能指针对象内,智能指针只能对阵堆上的内存管理
class Investment {
    ...
};
Investment * createInvestment() {	// 对象创建函数,返回一个在堆上创建后的Investment指针,此时需要使用delete释放
    Investment* inv = new Investment();
    return inv;
}

void f() {
    Investment* pInv = createInvestment();
    ...		
    delete pInv;
}
// 若早 ... 之间发生return或异常跳出,此时pInv指向的空间就不会被释放,发生内存泄露
// 因此我们需要构造一种对象来构造和释放资源,使得程序在任何地方跳出时都能够保证内存被析构
// 此时就引出了经典的智能指针

void f2() {
    std::shared pInv(createInvestment()) ;
    ...
}
// 此时不需要delete来进行析构,智能指针会在当前对象的计数变为0时自动析构,这算是智能指针最经典的用法

智能指针 shared_ptr的实现

template 
class SharedPtr {
public:
	SharedPtr() : ptr(nullptr), count(nullptr) { }
	SharedPtr(T *p, std::size_t n = 1) : ptr(p), count(new std::size_t(n)) { }	// 无法防止循环引用,若用同一个普通指针去初始化两个shared_ptr,此时两个shared_ptr均指向同一片区域,但引用计数都为1,使用时需注意
	SharedPtr(const SharedPtr& p) : ptr(p.ptr), count(&(++(*p.count))) { }

	// 析构函数
	~SharedPtr() {
		if (ptr && --*count == 0) {
			delete ptr;
			delete count;
		}
		ptr = nullptr;
		count = nullptr;
	}

	// 赋值运算符1,首先this的引用计数减一,p的引用计数加1
	//SharedPtr& operator=(const SharedPtr& p) {
	//	if (*this == p) {
	//		return *this;
	//	}

	//	if (ptr && --*count == 0) { // -1之后若为0,则释放
	//		delete ptr;
	//		delete count;
	//	}
	//	++(*p.count);
	//	ptr = p.ptr;
	//	count = p.count;
	//}
	// 赋值运算符2
	SharedPtr& operator=(const SharedPtr &p) {
		SharedPtr(p).swap(*this);
		return *this;
	}

	std::size_t size() const {
		return *count;
	}

	bool unique() const {
		return *count == 1;
	}

	T* get() const {
		return ptr;
	}
	// reset 次数
	void reset(T* ptr, std::size_t co) {
		SharedPtr(ptr, co).swap(*this);
	}

	void swap(SharedPtr &p) {
		using std::swap;
		swap(count, p.count);
		swap(ptr, p.ptr);
	}

	// *运算符
	T& operator*() const {
		return *ptr;
	}
	// ->运算符
	T* operator->() const {
		return ptr;
	}

private:
	T *ptr;
	std::size_t *count;	// 计数器,使用int* 比 int 好
};

条款14

资源管理类中小心 copy行为

由于智能指针只能针对堆上内存管理,并非所有的资源都是堆上资源,所以很多时候我们都需要建立自己的资源管理类。

  • 当复制资源管理类时必须一并复制它所管理的资源,资源的拷贝行为决定资源管理类对象的拷贝行为
  • 常见的资源管理类的拷贝行为是:抑制拷贝(禁止拷贝行为),使用引用计数法(最常见的是在资源管理类中使用shared_ptr作为成员变量)

一个典型的例子为互斥锁的锁定与解锁行为,我们不想在程序释放时还有资源处在上锁状态,则需要保证程序跳出时会解锁所有的上锁资源

互斥器对象 Mutex有两个函数用于锁定与解锁,void lock(Mutex *pm)void unlock(Mutex *pm)

class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm) {
        lock(mutexPtr);		// 加锁
    }
    ~Lock() {
        unlock(mutexPtr);	// 解锁
    }
private:
    Mutex *mutexPtr;
};

这种情况下是能够实现资源的加锁与解锁问题,但是当Lock对象被复制时,会出现问题。
我们想要的是锁被多个内容占用时,不是立刻解除所有的锁,而是等程序都处理完之后是才会解除锁定

此时可能又需要引用计数来进行加锁。但只是将指针改为智能指针后,当计数为0是,智能指针会删除锁,而我们想要的是解锁,此时需要使用智能指针指定所谓的删除器(deleter),他是一个函数或函数对象,当引用次数为0时就要被调用,默认是删除指针。

class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) {	// 用unlock()指定删除器
        lock(mutexPtr);		// 加锁
    }
private:
    std::shared_ptr mutexPtr;
};

本例中没有必要指定析构函数。可以使用默认的析构函数用来释放智能指针,而此时会调用智能指针的析构函数,从而满足需求。

条款15

  • 在资源管理类中应该提供对原始资源的访问
  • 对原始资源的访问可能经由显示转换或隐式转换。显示转换比较安全,隐式转换比较方便(类型转换函数)
  • 提供对原始资源的访问,某方面来讲会破坏类的封装性
  • 但是我们设计类的目的是为了方便,良好的类隐藏了需要隐藏的部分,但一定要管理好用户需要的部分

隐式转换(类型转换函数)

类型转换函数的实现,以智能指针为例
类型转换函数的语法为 operator name() {},其中 name是要转换的目的类型

class SharedPtr {
    ...
    operator pointer() const {	// 当要将其转化为pointer类型的对象时,会隐式调用该类型转换
        return ptr;
    }
    ...
}

显示转换

如上面智能指针实现过程中的 get()函数,调用它可以获得原始的指针。

条款16

成对地使用 newdelete,并要采取相同的形式 new [] 对应delete [],实际上是通过 operator newoperator delete函数实现的。

条款17

以独立的一条语句将 new对象存储于只能指针中,若不这样做,可能会导致难以察觉的资源泄露

// 函数
void processWidget(std::shared w, int priority);

// processWidget(std::shared(new Widget), priority()); // 可能会导致内存泄露
// 因为我们不知道priority()在什么时候执行,万一在执行 new Widget 后执行则会出现异常,导致内存泄露

// 因此我们要采用多条语句的方式
std::shared w(new Widget);	// 先创建只能指针对象
processWidget(w, priority());	// 这样就不会导致内存泄露了

你可能感兴趣的:(C++学习)