由于C++是需要程序员手动管理内存,在大型项目中很容易发生对忘记释放资源或者对已经delete的指针再次删除。即使开发人员成对的使用了new/delete,也有可能中间的代码发生异常,而调用delete的该层并没有捕获异常,导致跳过了delete语句。
go
int* p = new int(9);
....
//下面的函数抛出异常
func();
...
delete p;
如果func()函数是调用其他人写的代码,你很难确定它会不会抛出异常,为了确保在发生异常的时候依然可以争取的释放资源,你只能书写复杂的try/catch/语句了,整个程序的逻辑也会变的复杂。
RAII即:资源获取即初始化,把资源的生命周期和对象的生命周期绑定到一块。避免了程序员忘记释放资源或因为异常发生而导致资源泄漏的情况。
auto_ptr
auto_ptr是一个失败设计,它被废弃的主要原因是它在使用拷贝构造函数/拷贝赋值运算符的时候,会发生对象所有权的转移,被拷贝的指针会置空,失去对象的所有权。这种隐式的所有权的转移使用起来容易出错。
unique_ptr
unique_ptr
正如它的名字所言,它唯一拥有对象的所有权,它的拷贝构造函数和拷贝赋值运算符都被设置为删除函数。
arduino
template<typename T>
class unique_ptr {
public:
///构造函数
unique_ptr(T* ptr_ = nullptr):ptr(ptr_) { };
///析构函数
~unique_ptr() {
delete ptr;
}
///拷贝构造函数和拷贝赋值运算符设置为delete
unique_ptr(unique_ptr<T>& rhs) = delete;
unique_ptr& operator=(unique_ptr<T>& rhs) = delete;
///需要重载一下两个运算符
T& operator*() {
return *ptr;
}
T* operator->() {
return ptr;
}
private:
T* ptr;
};
unique_ptr
不支持对资源的拷贝,但它可以使用移动语义转移资源,实现如下:
c
///类内实现
///移动构造函数
unique_ptr(unique_ptr<T>&& rhs):ptr(rhs.ptr) {
rhs.ptr = nullptr;
}
///主函数测试
unique_ptr<int> p(new int(9));
unique_ptr<int> p1(std::move(p));
std::cout << *p1 << std::endl; //输出9
shared_ptr
unique_ptr
使用方便,可以做到正确的释放资源。但它的语义是独占资源,无法直接拷贝资源。而shared_ptr
实现了资源的共享,它并不直接拷贝资源而是通过引用计数来管理资源。当引用计数为0的时候,才会真正地销毁资源。需要注意的是,shared_ptr内部对资源浅拷贝,所以在不确定自己是资源的独享者的情况下,不要擅自修改资源。
arduino
template<typename T>
class shared_ptr {
public:
///构造函数
shared_ptr(T* ptr_ = nullptr):ptr(ptr_), refCnt(new int(1)) { };
///析构函数
~shared_ptr() {
if(--(*refCnt) == 0) {
delete ptr;
delete refCnt;
}
}
///拷贝构造函数
shared_ptr(shared_ptr<T>& rhs):ptr(rhs.ptr), refCnt(rhs.refCnt) {
++(*refCnt);
}
///拷贝赋值运算符
shared_ptr& operator=(shared_ptr<T>& rhs) {
//先增加右边的,再减去左边的,防止自我赋值
++(*rhs.refCnt);
if(--(*refCnt)) {
delete ptr;
delete refCnt;
}
ptr = rhs.ptr;
refCnt = rhs.refCnt;
}
int use_cout() {
return *refCnt;
}
///需要重载一下两个运算符
T& operator*() {
return *ptr;
}
T* operator->() {
return ptr;
}
private:
T* ptr;
int* refCnt;
};
但这里的实现并不是线程安全的,当我们在两个线程中,分别拷贝某个共享智能指针,由于它在拷贝构造函数/析构函数中修改引用计数的过程并不是原子的,所以会引发计数错误。示例:
c
int main () {
int n = 10000;
shared_ptr<int> p1(new int(9));
std::thread t1([&]{
for(int i = 0; i < n; i++)
shared_ptr<int> tmp(p1);
});
std::thread t2([&] {
for(int i = 0; i < n; i++)
shared_ptr<int> tmp1(p1);
});
t1.join();
t2.join();
std::cout << "use_count: " << p1.use_cout() << std::endl;
}
我们在两个线程中分别拷贝了p1指针10000次,拷贝一次引用计数+1,随即该对象销毁,引用计数-1。最后输出的use_count应该是1才对,由于多线中引用计数的原子性得不到保证,故本实现并不是线程安全的。 我们可以将计数值变量改为原子变量以实现修改的原子性,示例如下:
arduino
template<typename T>
class shared_ptr {
public:
///构造函数
shared_ptr(T* ptr_ = nullptr):ptr(ptr_), refCnt(new std::atomic<int>(1)) { };
///析构函数
~shared_ptr() {
if(--(*refCnt) == 0) {
delete ptr;
delete refCnt;
}
}
///拷贝构造函数
shared_ptr(shared_ptr<T>& rhs):ptr(rhs.ptr), refCnt(rhs.refCnt) {
++(*refCnt);
}
///拷贝赋值运算符
shared_ptr& operator=(shared_ptr<T>& rhs) {
++(*rhs.refCnt);
if(--(*refCnt)) {
delete ptr;
delete refCnt;
}
ptr = rhs.ptr;
refCnt = rhs.refCnt;
}
int use_cout() {
return *refCnt;
}
///需要重载一下两个运算符
T& operator*() {
return *ptr;
}
T* operator->() {
return ptr;
}
private:
T* ptr;
//修改为指向原子变量的指针
std::atomic<int>* refCnt;
};
经过多线程测试,最后输出的use_count始终为1。当然了,通过加锁的方式也是可以做到,需要注意:类内的锁必须是定义在堆上的变量,这样才能让多个线程共享一个互斥锁。
weak_ptr
弱指针类似一个观察者,它不管理所引用的对象。构造过程不过增加引用计数,析构也不会减少引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象。故在使用弱指针之前,最好使用w.lock()函数将弱指针提升为共享指针,如果对象已经被释放则是空指针。 弱指针的出现主要是为了解决共享指针循环引用的问题,示例如下:
c
class B;
class A {
public:
~A() {
std::cout << "A dtor is called" << std::endl;
}
std::shared_ptr<B> ptr;
};
class B {
public:
~B() {
std::cout << "B dtor is called" << std::endl;
}
std::shared_ptr<A> ptr;
};
int main() {
std::shared_ptr<A> p1(new A);
std::shared_ptr<B> p2(new B);
p1->ptr = p2;
p2->ptr = p1;
}
上面的函数运行会发现析构函数并没有被调用,这是因为循环引用造成了内存泄漏