C++智能指针

由于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;
}

上面的函数运行会发现析构函数并没有被调用,这是因为循环引用造成了内存泄漏

相关推荐
测试19986 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
马剑威(威哥爱编程)7 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
独行soc9 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
理想不理想v9 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb123410 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺10 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉10 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船10 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端11 小时前
Content Security Policy (CSP)
前端·javascript·面试
测试界萧萧11 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展