侧边栏壁纸
博主头像
如此肤浅博主等级

但行好事,莫问前程!

  • 累计撰写 24 篇文章
  • 累计创建 11 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录
C++

智能指针

如此肤浅
2023-03-11 / 1 评论 / 0 点赞 / 217 阅读 / 7,376 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-03-11,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
  • 普通指针的不足:
    ① new 和 new[] 的内存需要 delete 和 delete[] 手动释放;
    ② 程序员的主观失误,可能导致漏了释放内存;
    ③ 在多线程编程中,程序员也可能不确定什么时候释放内存。

  • 智能指针的设计思路:
    ① 智能指针是类模板,在栈上创建智能指针对象;
    ② 把普通指针交给智能指针对象;
    ③ 智能指针对象过期时,调用析构函数释放普通指针的内存。

  • 智能指针在头文件<memory>中,有3种:
    ① unique_ptr
    ② shared_ptr
    ③ weak_ptr

独占智能指针unique_ptr

以类自定义的类型 AA 为例:

class AA {
public: 
	string m_name;
    
	AA() {};
    AA(const string& name) : m_name(name) {};
    ~AA() { cout << "析构了" << endl; }

	
}

初始化

  • 方法一:用已存在的地址初始化(不推荐)

    AA* p = new AA("张飞");
    unique_ptr<AA> p1(p);
    
  • 方法二: 分配内存并初始化

    unique_ptr<AA> p(new AA("张飞"));
    
  • 方法三:C++14标准

    unique_ptr<AA> p = make_unique<AA>("你好");
    

使用方法

  1. 智能指针重载了*->操作符,可以像使用普通指针一样使用。
AA* p = new AA("张飞");
unique_ptr<AA> p1 = make_unique<AA>("关羽");

cout << p->m_name << endl;
cout << p1->m_name << endl;
cout << (*p).m_name << endl;
cout << (*p1).m_name << endl;
  1. 不能将普通指针直接传递赋值给智能指针
AA *p = new AA("张飞");
unique_ptr<AA> p1 = p; // 错误
unique_ptr<AA> p2 = new AA("张飞"); // 错误



// 原因:unique_ptr类中声明了不能隐式转换
explicit unique_ptr(T* p) noexcept;
  1. 不能使用拷贝构造函数
unique_ptr<AA> p1 = make_unique<AA>("张飞");
unique_ptr<AA> p2 = p1; // 错误,不能使用拷贝构造

// 原因:unique_ptr类中禁用了拷贝构造函数
unique_ptr(const unique_ptr&) = delete;

一个 unique_ptr 对象只对一个资源负责,如果 unique_ptr 允许复制,就会出现多个 unique_ptr 对象指向同一块内存的情况。当其中一个 unique_ptr 对象过期释放内存,其他的 unique_ptr 对象过期又会释放内存,造成对同一块内存释放多次。

所以不能使用拷贝构造函数和赋值构造函数!

  1. 不能使用赋值构造函数
unique_ptr<AA> p1 = make_unique<AA>("张飞");
unique_ptr<AA> p2;
p2 = p1; // 错误,不能使用赋值构造

// 原因:unique_ptr类中禁用了赋值构造函数
unique_ptr& operator=(const unique_ptr&) = delete;
  1. 不要用同一个裸指针初始化多个 unique_ptr 对象

    因为会出现多次释放同一块内存,导致出错。
    因此,不推荐使用上述的第一种初始化方法,以防出现这种错误。

  2. get() 可以返回裸指针

AA* p = new AA("张飞");
unique_ptr<AA> p1(p);

cout << "裸指针的值:" << p << endl;
cout << "智能指针的值:" << p1 << endl;
cout << "p1.get(): " << p1.get() << endl;
cout << "p1 的地址是: " << &p1 << endl;

/*
输出:
    裸指针的值:00FD9B80
    智能指针的值:00FD9B80
    p1.get(): 00FD9B80
    p1 的地址是: 00BEF968
*/
  1. 不要用 unique_ptr 管理不是 new 分配的内存

  2. 智能指针用于函数参数时要传引用(不能传值,因为没有拷贝构造函数)

  3. 智能指针不支持指针的运算(+、-、++、–)

重要技巧

  1. 将一个 unique_ptr 赋给另一个时,如果源 unique_ptr 是一个临时右值,编译器允许这样做;如果源 unique_ptr 将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
unique_ptr<AA> p;
p = unique_ptr<AA>(new AA("张飞"));
  1. 用 nullptr 给 unique_ptr 赋值将释放对象,空的 unique_ptr==nullptr。
unique_ptr<AA> p(new AA("张飞"));

if (p != nullptr) cout << "p 不为空 " << endl;
p = nullptr;
if (p == nullptr) cout << "p 为空" << endl;

/*
输出:
  张飞
  p 不为空
  析构
  p 为空
*/
  1. release() 释放对原始指针的控制权,将 unique_ptr 置为空,返回裸指针。(可用于把 unique_ptr 传递给子函数,子函数将负责释放对象)

  2. std:move() 可以转移对原始指针的控制权。(可用于把 unique_ptr 传递给子函数,子函数形参也是 unique_ptr)

  3. reset() 释放对象

p.reset(); // 释放p对象指向的资源对象
p.reset(nullptr); // 释放p对象指向的资源对象
p.reset(new AA("张飞")); // 释放p对象指向的资源对象,同时指向新的对象
  1. swap() 交换两个 unique_ptr 的控制权
void swap(unique_ptr<T>& _Right);
  1. unique_ptr 也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。

  2. unique_ptr 不是绝对安全,如果程序中调用 exit() 退出,全局的 unique_ptr 可以自动释放,但局部的 unique_ptr 无法释放。

  3. unique_ptr 提供了支持数组的特化版本。数组版本的 unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。

AA* parr1 = new AA[2]{ string("张飞"), string("关羽") };
AA* parr2 = new AA[2]; // 普通指针数组
parr2[0].m_name = "张飞";
parr2[1].m_name = "关羽";

unique_ptr<AA[]> parr3(new AA[2]{ string("张飞"), string("关羽") });
unique_ptr<AA[]> parr4(new AA[2]);  // unique_ptr数组
parr4[0].m_name = "张飞";
parr4[1].m_name = "关羽";

共享智能指针shared_ptr

shared_ptr 共享它指向的对象,多个 shared_ptr 可以指向相同的对象,在内部使用技术机制实现。

当新的 shared_ptr 与对象关联时,引用计数增加 1。

当 shared_ptr 超出作用域时,引用计数减1。当引用计数变为 0 时,则表示没有任何 shared_ptr 与对象关联,则释放该对象。

shared_ptr 的构造函数也是 explicit,但是,没有删除拷贝构造函数和赋值函数。

初始化

  • 方法一:分配内存并初始化

    shared_ptr<AA> p(new AA("张飞"));
    
  • 方法二:(推荐使用)

    shared_ptr<AA> p = make_shared<AA>("张飞");
    
  • 方法三:用已存在的地址初始化

    AA* p = new AA("张飞");
    shared_ptr<AA> p1(p);
    
  • 方法四:用已存在的shared_ptr初始化(拷贝构造、赋值构造)

    shared_ptr<AA> p1(new AA("张飞"));
    shared_ptr<AA> p2(p1); // 计数加1
    shared_ptr<AA> p3 = p1; // 计数加1
    

使用方法

  1. 智能指针重载了*->操作符,可以像使用指针一样使用 shared_ptr。
  2. use_count() 方法返回引用计数器的值。
  3. unique() 方法,如果 use_count() 为1,返回 true,否则返回 false。
  4. 支持普通的拷贝和赋值,左值的 shared_ptr 的计数器将减 1,右值 shared_ptr 的计算器将加 1。
shared_ptr<AA> p1(new AA("张飞"));
shared_ptr<AA> p2 = p1; // 计数加1
shared_ptr<AA> p3(p1); // 计数加1
cout << "p1.use_count() = " << p1.use_count() << endl; // 值为3

shared_ptr<AA> p4(new AA("关羽"));
shared_ptr<AA> p5 = p4;
cout << "p5.use_count() = " << p5.use_count() << endl; // 值为2
p5 = p1; // p5指向的对象变了,所以p4的引用计数-1,p1的引用计数+1
cout << "p1.use_count() = " << p1.use_count() << endl; // 值为4
cout << "p5.use_count() = " << p4.use_count() << endl; // 值为1
  1. get() 方法返回裸指针。
  2. 不要用同一个裸指针初始化多个 shared_ptr。
  3. 不要用 shared_ptr 管理不是 new 分配的内存。
  4. 用作函数的参数,原理与 unique_ptr 相同。
  5. 智能指针不支持指针的运算(+、-、++、–)

重要技巧

  1. 用 nullptr 给 shared_ptr 赋值将把计数减 1,如果计数为 0,将释放对象,空的 shared_ptr==nullptr。
  2. std::move() 可以转移对原始指针的控制权。还可以将 unique_ptr 转移成 shared_ptr,反过来不行。
  3. reset() 改变与资源的关联关系。
p.reset(); // 解除与资源的关系,资源的引用计数减1
p.reset(new AA("张飞")); // 解除与资源的关系,资源的引用计数减1,关联新资源
  1. shared_ptr 的线程安全性:
    • shared_ptr 的引用计数本身时线程安全的(引用计数是原子操作)
    • 多个线程对同一个 shared_ptr 对象进行读写,则需要加锁
    • 多个线程对同一个 shared_ptr 对象读,则是线程安全的
    • 多线程读写 shared_ptr 所指向的同一个对象,不管是相同的 shared_ptr 对象,还是不同的 shared_ptr 对象,也需要加锁保护。
  2. 如果 unique_ptr 能解决问题,就不要用 shared_ptr。unique_ptr 的效率更高,占用的资源更少。

智能指针删除器

在默认情况下,智能指针过期的时候,用 delete 原始指针;释放它管理的资源。

程序员可以自定义删除器,改变智能指针释放资源的行为。

删除器可以是全局函数、仿函数和 Lambda 表达式,形参为原始指针。

  • 案例:
    // 对 shared_ptr
    shared_ptr<AA> p1(new AA("张飞1")); // 使用默认的删除器,即delete
    
    shared_ptr<AA> p2(new AA("张飞2"), deleteFunc);
    
    shared_ptr<AA> p3(new AA("张飞3"), deleteClass()); // 仿函数作删除器
    
    auto myLambda = [](AA* a) { // Lambda表达式作删除器
        cout << "自定义删除器(Lambda表达式)" << endl;
        delete a;
    };
    shared_ptr<AA> p4(new AA("张飞4"), myLambda);
    
    
    // 对 unique_ptr 要复杂点
    unique_ptr<AA, decltype(deleteFunc)*> p5(new AA("张飞5"), deleteFunc);
    unique_ptr<AA, deleteClass> p6(new AA("张飞6"), deleteClass());
    unique_ptr<AA, decltype(myLambda)> p7(new AA("张飞7"), myLambda);
    

弱智能指针weak_ptr

weak_ptr 是为了配合 shared_ptr 而引入的,它指向一个由 shared_ptr 管理的资源但不影响资源的生命周期。也就是说,将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。

不论是否有 weak_ptr 指向,如果最后一个指向资源的 shared_ptr 被销毁,资源就会被释放。

  • 循环引用
    shared_ptr 内部维护了一个共享的引用计数器,多个 shared_ptr 可以指向同一个资源。

    但如果出现了循环引用的情况,引用计数永远无法归 0,资源不会被释放。(如图所示)
    智能指针-循环引用

  • 循环引用解决办法
    将类内部的 shared_ptr 换成 weak_ptr 即可。
    智能指针-循环和引用解决方法

使用方法

weak_ptr 没有重载->*操作符,不能直接访问资源。

有以下成员函数:

operator(); // 把shared_ptr或weak_ptr()赋值给weak_ptr
expired(); // 判断它指向的资源是否已经被释放
lock(); // 返回shared_ptr,如果资源已经被释放,返回空的shared_ptr
reset(); // 将当前weak_ptr指针设置为空
swap(); // 交换

重要技巧

  1. weak_ptr 不控制对象的生命周期,但它知道对象是否活着。
  2. 用 lock() 函数可以把 weak_ptr 提升为 shared_ptr,如果对象还或者,返回有效的 shared_ptr,如果对象已经死了,提升会失败,返回一个空的 shared_ptr。
  3. 提升的行为(lock())是线程安全的。
  • 案例:
    多线程下的某个场景为:先判断 weak_ptr 指向的资源是否被释放,如果没有被释放,则将该 weak_ptr 提升为 shared_ptr,代码如下。
shared_ptr<AA> pa = make_shared<AA>("张飞");
shared_ptr<BB> pb = make_shared<BB>("关羽");

// 解决过后的循环引用,m_p 是 weak_ptr
pa->m_p = pb;
pb->m_p = pa;

if(pa->m_p.expired() == true) { // 判断资源是否过期
	cout << "pa->m_p已经过期\n";
}  else {
	pa->m_p.lock(); // 如果没有过期将pa->mp提升为 shared_ptr
}

上述代码的问题是,资源在 expired() 和 lock() 这两步操作之间可能被释放了,因此在 expired() 判断为 true 的情况下可能会去执行 lock() 操作。正确的写法如下。

shared_ptr<AA> pa = make_shared<AA>("张飞");
shared_ptr<BB> pb = make_shared<BB>("关羽");

// 解决过后的循环引用,m_p 是 weak_ptr
pa->m_p = pb;
pb->m_p = pa;

// 把weak_ptr提升为shared_ptr
shared_ptr<BB> pp = pa->m_p.lock();
if(pp == nullptr) { // 提升失败返回空指针
	cout << "pa->m_p已经过期\n";
}  else { // 若资源没被释放则可对资源进行操作了
	cout << pp->m_name << endl;
}
0

评论区