C++ 智能指针 std::shared_ptr 的指针引用和指针有效性

文章探讨了C++中std::shared_ptr在多线程环境下的行为,尤其是在线程2修改并删除元素后,线程1如何访问已被删除的元素。实验表明,即使智能指针被销毁,引用仍可访问同一地址,直到引用计数为0。
摘要由CSDN通过智能技术生成

基础代码

t1线程首先执行 DoIt ,并将链表最后一个元素传入进去,然后轮询is_l,

t2线程首先休眠3秒(保证1线程先跑),然后将链表中值为3和5的元素删除,置is_l为1。

t1此时轮询结束,查看i1的值,由于t2已经将5给删除了,那么t1在这之后访问值为5的元素还能成功吗?

bool is_l = false;

void DoIt(std::shared_ptr<int> &i1) {
  while (!is_l) {
  }
  std::cout << "loop: " << i1 << std::endl; // loop: 0x562e787763b0
  std::cout << *i1 << std::endl;
}
/** IS, IS, X integration test */
void MyPointTest() {
  // 定义一个 全局链表
  std::list<std::shared_ptr<int>> l1;
  // 插入数据
  l1.push_back(std::make_shared<int>(1));
  l1.push_back(std::make_shared<int>(2));
  l1.push_back(std::make_shared<int>(3));
  l1.push_back(std::make_shared<int>(4));
  l1.push_back(std::make_shared<int>(5));

  // 定义两个线程
  std::thread t_1([&]{
    DoIt(l1.back());
  });
  std::thread t_2([&]{
    // 线程2 休眠一会,然后再启动并且执行一些操作
    std::this_thread::sleep_for(std::chrono::seconds(3));
    for(auto it=l1.begin();it!=l1.end();++it){
      if(**it==3){
        l1.erase(it);
        break;
      }
    }
    for(auto it=l1.begin();it!=l1.end();++it){
      if(**it==5){
        std::cout << "5: " << *it << std::endl; // 5: 0x562e787763b0
        l1.erase(it);
        break;
      }
    }
    is_l = true;
  });

  t_1.join();
  t_2.join();

}
TEST(LockManagerTest, PointTest) { MyPointTest(); }  // NOLINT

运行结果

         DoIt 函数输出了 5 ,这表示另一个线程销毁了这个智能指针之后,这个智能指针的其他引用还能正常访问到这个地址。

        这里就得到一个初略的 结论,那么下面我们测试一下,这里的智能指针的引用计数是如何变化的,以确定其是否在被线程2 真的销毁还是引用计数并没有归零。     

基础知识

这里有两个基础概念需要了解:

  1. 关于push_back的行为: 在这个例子中,l1.push_back(std::make_shared<int>(1)); 调用涉及到 std::shared_ptr 的处理。std::make_shared<int>(1) 创建一个新的 shared_ptr 实例。然后,这个新创建的 shared_ptr 被传递给 push_back 方法。在 C++11 及更高版本中,由于 shared_ptr 支持移动语义,push_back 方法会优先使用移动构造函数,如果可行的话。这意味着,在这种情况下,移动构造器将被调用来将 shared_ptr 添加到列表中,而不是拷贝构造器。移动构造允许资源(在这个例子中是一个指向 int 的指针)从源 shared_ptr(临时创建的)转移到列表中的 shared_ptr,这通常比拷贝构造更高效。

  2. 关于使用back方法得到的 shared_ptr: 当调用 l1.back() 时,返回的是列表中最后一个元素的引用。但是,由于列表的元素是 std::shared_ptr<int> 类型,所以 l1.back() 返回的是对 std::shared_ptr<int> 的引用。这意味着,如果你将这个返回值存储在另一个 shared_ptr 实例中,将会创建一个新的 shared_ptr 实例,该实例与列表中的 shared_ptr 共享对同一个 int 对象的所有权。这是 shared_ptr 的设计目的之一:允许多个 shared_ptr 实例共享同一个对象,同时管理其生命周期。在这个过程中,引用计数会适当地增加,以反映共享对象的新所有者。

所以,其实这里的 .back() 和 引用传参 都属于引用 ,线程1 获取到智能指针的过程不应该增加引用计数。

验证1


bool is_l = false;

void DoIt(const std::shared_ptr<int>& i1) {
  std::cout << "loop_before: " << i1.use_count() << std::endl;

  while (!is_l) {
  }
  std::cout << "loop: " << i1 << std::endl; // 0x55ccc9b303b0
  std::cout << "loop_after: " << i1.use_count() << std::endl;
  std::cout << *i1 << std::endl; // 5 , 线程2 修改就是 7
}
/** IS, IS, X integration test */
void MyPointTest() {
  // 定义一个 全局链表
  std::list<std::shared_ptr<int>> l1;
  // 插入数据
  l1.push_back(std::make_shared<int>(1));
  l1.push_back(std::make_shared<int>(2));
  l1.push_back(std::make_shared<int>(3));
  l1.push_back(std::make_shared<int>(4));
  l1.push_back(std::make_shared<int>(5));
  std::cout<<"外面: "<< ((l1.back())).use_count()<< std::endl;
  // 定义两个线程
  std::thread t_1([&]{
    DoIt(l1.back());
  });
  std::thread t_2([&]{
    // 线程2 休眠一会,然后再启动并且执行一些操作
    std::this_thread::sleep_for(std::chrono::seconds(3));
    for(auto it=l1.begin();it!=l1.end();++it){
      if(**it==3){
        l1.erase(it);
        break;
      }
    }
    for(auto it=l1.begin();it!=l1.end();++it){
      if(**it==5){
        std::cout << "T2: " << *it << std::endl;  // 0x55ccc9b303b0
        std::cout << "T2_erase_before: " << (*it).use_count() << std::endl;  
        l1.erase(it);
        std::cout << "T2_erase_after: " << (*it).use_count() << std::endl;  
        **it = 7;
        break;
      }
    }
    is_l = true;
  });

  t_1.join();
  t_2.join();

}
TEST(LockManagerTest, PointTest) { MyPointTest(); }  // NOLINT

运行结果

  外面: 1 ————说明初始只有 list 中有这个指针

  loop_before: 1 ————说明l1.back()是一个引用,传引用参数进入loop不会增加计数

  T2: 0x556b687723b0 ———— 地址相同

  T2_erase_before: 1 ———— t2 没有erase之前,引用计数依旧是1

  T2_erase_after: 1281334311 ———— t2 erase 之后,这个智能指针被销毁

  loop: 0x556b687723b0 ———— 地址相同

  loop_after: 1281334311 ———— 线程2 销毁,线程1 引用的智能指针也同步销毁、

  7                               —————— 但是线程2 的修改,线程1 依旧可以读到

验证2

 修改代码,让 DoIt 的传参不是一个引用,运行结果为

外面: 1

loop_before: 2 ————此时 传参 就是构造一个拷贝,引用计数增加

T2: 0x55c53c6b83b0

T2_erase_before: 2

T2_erase_after: 1

loop: 0x55c53c6b83b0

loop_after: 1

7

 探究3

这里进一步看看单线程下销毁智能指针时引用的指针是否还能使用:

 这里设置一个 sp 对 智能指针进行引用,在销毁后查看其是否能访问到这个地址。

    for(auto it=l1.begin();it!=l1.end();++it){
      if(**it==5){
        std::shared_ptr<int> &sp = *it;
        std::cout << "T2: " << *it << std::endl;  // 0x55ccc9b303b0
        std::cout << "T2_erase_before: " << (*it).use_count() << std::endl;  
        l1.erase(it);
        std::cout << "T2_erase_after: " << (*it).use_count() << std::endl;  
        **it = 7;
        std::cout<<"T2_2: "<< *sp << std::endl;
        break;
      }
    }

运行结果

T2_2: 7   -------------所以即使是单线程下,引用计数减少为0,引用依旧可以使用。

结论与思考

        无论多线程时还是在单线程,可以通过智能指针的引用获得到同一片地址,并按照智能指针的规则共享的增加或者减少引用计数。但是 当一个唯一的 shared_ptr 被销毁时,其他对这个 智能指针 的引用依旧可以正常访问这个地址。

(有点像内存泄漏的感觉)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值