Post

智能指针

智能指针

C++11 引入的新特性。 包含三种智能指针:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

对象生命周期

  • 全局对象:程序启动时分配,程序结束时销毁
  • 局部对象:定义时创建,离开块时销毁
  • 局部 static:第一次使用前分配,程序结束时销毁
  • 动态分配:显式创建,显式销毁

RAII 思想

资源获取就是初始化

三种智能指针

std::shared_ptr

是一个模板类,可以指定的对象类型,它的行为像指针,但还记录着有多少个对象(即其它 shared_ptr)共享着它管理的内存对象。 多个 shared_ptr 可以共享一个内存对象,一个 shared_ptr 销毁时,引用计数减 1,最后一个也被销毁时,或者说引用计数变成 0 的时候,自动释放指向的对象。 shared_ptr 可以用 make_shared<T>() 函数来创建,也可以通过拷贝或者赋值另一个 shared_ptr 来创建。 内存对象在堆(heap)上,智能指针如果说是局部变量的话,那就是在栈(stack)上。

底层原理

1
2
element_type*	   _M_ptr;         // Contained pointer.
__shared_count<_Lp>  _M_refcount;    // Reference counter.

即:一个指向内存对象的指针,一个指向引用计数的控制块的指针。 image-20241027231002576 控制块包括:

  • 引用计数
  • weak_ptr 计数
  • 其它数据(删除器、分配器等) 控制块也是在堆上的,因为要共享。

unique_ptr 只有一个指针成员,就是指向内存对象的指针,因此 shared_ptr 大小是 unique_ptr 或者说原始指针的两倍,两个指针的大小。

内置方法

方法作用
make_shared<T>(args)动态分配类型 T 的对象,用 args 初始化,返回一个 shared_ptr
shared_ptr<T> p(q)q 拷贝构造 p,这个操作实际上是增加 q 的引用计数,并不会分配新内存,q 必须能转换成 T*
shared_ptr<T> p = q同上
p.unique()如果引用计数是 1,返回 true,否则返回 false
p.use_count()返回引用计数
p.reset()删除了这个智能指针的指向,所指向的内存对象引用计数减 1
共享资源时候用 shared_ptr,比如文件读写,网络连接,数据库连接等。 

std::unique_ptr

unique_ptr 只包含一个指向内存对象的原始指针。

unique_ptr 独占指向的内存对象。 unique_ptr 没有构造函数和赋值运算符函数,已经删除了,因此无法拷贝和赋值。 使用 make_unique<T>() 函数创建 unique_ptr 使用 p.release() 函数释放所有权,同时返回原始指针,那就得自己释放内存了 使用 p.reset() 重置所有权,释放内存,指向空指针

shared_ptr 和 unique_ptr 共有操作

方法作用
p.get()获取智能指针中保存的原始指针,不影响引用计数
p.reset()重置智能指针为空,指向的内存视情况释放
p.reset(q)重置 p,指向 q,相当于转移了所有权
p.reset(new T)重置 p 并且指向新内存对象
p.swap(q)交换两个智能指针中保存的原始指针
swap(p, q)同上
*p解引用,拿到内存对象本身
p->成员运算符,访问内存对象的成员
if (p)空指针的话就是 false,和常规指针一样
unique_ptr 删掉了赋值运算符和拷贝构造函数,因此不能通过: 
1
unique_ptr<T> p = q;

1
unique_ptr<T> p(q);

来转交所有权,需要用移动构造:

1
unique_ptr<T> p = std::move(q);

unique_ptr 占内存更小,不需要维护引用计数,性能更好,所以如果我们确定一个内存对象是独占的,那优先用 unique_ptr,如果共享那就得用 shared_ptr 了。

weak_ptr

这是个弱引用,可以指向 shared_ptr 管理的对象,不影响引用计数,也就是不影响内存对象生命周期。 weak_ptr 没有对象的所有权,不能解引用,如果要读取对象,需要转换成 shared_ptr 才行。如果 weak_ptr 指向的对象还在(使用 expired() 函数判断是否已经被释放),那么使用 lock() 函数可以返回一个 shared_ptr,否则返回一个空 shared_ptr,然后就可以解引用读取了。 weak_ptr 可以作为 shared_ptr 的构造函数参数,如果 weak_ptr 指向的内存已经释放,weak_ptr 为空,那么会抛出 std::bad_weak_ptr 异常。

方法作用
use_count()返回 shared_ptr 的引用计数
expire()检查所指对象是否已经被释放
lock()返回一个 shared_ptr,如果此时内存对象已经被删除,返回空 shared_ptr
owner_before()提供所有者基于的弱指针的排序?????
reset()重置指向
swap()交换两个 weak_ptr
可以使用 weak_ptr 来缓存对象,类似于一般指针的作用,当内存对象还在,直接用 lock() 返回一个 shared_ptr 来快速访问,如果内存对象已经被销毁,weak_ptr 也自动失效,不会造成野指针,用 lock() 只能拿到一个空 shared_ptr,这时候我们就可以再去重新加载这个资源,并且用一个 shared_ptr 管理新加载的资源,这个 shared_ptr 也可以再去赋值 weak_ptr。 
lock 返回的 shared_ptr 是新创建的一个,也就是引用计数又会加 1。 

循环引用

两个共享指针分别管理一个内存对象,这两个内存对象内部又分别有一个共享指针指向对方,那么,这两个内存对象的引用计数就都是 2,任意一个共享指针作用域结束时,所管理的内存对象的引用计数减 1,也就是说,当这两个共享指针都销毁后,他们管理的两块内存对象,内部的共享指针还分别指向着对方,引用计数都 1,不为 0,内存无法释放,造成内存泄露。

解决方法是,内部的共享指针不要用 shared_ptr,用 weak_ptr,初始化 weak_ptr 的时候,是用管理着另一个内存对象的 shared_ptr 来初始化,由于 weak_ptr 不影响引用计数,内存对象会在 shared_ptr 销毁后直接释放,weak_ptr 并不知道它所指向的内存对象是否存在,因此无法直接访问对象,需要调用 lock 接口来提升为一个 shared_ptr,此时内存对象的引用计数也会 +1,使用完毕后应当及时销毁 shared_ptr,如果内存对象不存在了,那么提升会失败,这时候该从文件、数据库还是什么其它地方读取就去读取,属于是一种常见的缓存机制,如果内存对象在(也就是说有其他地方在用它,引用就是不为 0)就直接使用,不在就去创建,这也比较适合创建单例模式,开始使用时创建对象,没有任何访问时,自动销毁,但是这对可能频繁出现 0 引用也就是空闲的资源则不友好,可以在主函数中用一个智能指针先创建好???

两个或多个对象之间通过 shared_ptr 相互引用时,就会形成一个环,导致引用计数都不为 0,都不释放,从而导致内存泄漏。 避免方法: 其中一个改成 weak_ptr,慎用 shared_ptr weak_ptr 可用来实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
public:
    static std::shared_ptr<Singleton> getInstance() {
        std::shared_ptr<Singleton> instance = m_instance.lock();
        if (!instance) {
            instance.reset(new Singleton());
            m_instance = instance;
        }
        return instance;
    }
private:
    Singleton() {}
    static std::weak_ptr<Singleton> m_instance;
};

std::weak_ptr<Singleton> Singleton::m_instance;

优点是:

  • 避免循环引用,避免内存泄漏
  • 可以访问对象,但不会延长对象生命周期
  • 单例对象不被使用时,自动释放 用 make_shared<T>() 更异常安全,而 std::shared_ptr<T>(new T) 不够安全,因为 new 给 T 分配一次内存,然后再去给 shared_ptr(控制块)分配内存,使用移动构造完成 shared_ptr 的初始化,如果 new 发生了异常,那么 shared_ptr 不会被创建,对应 new 分配的内存也就不会被释放,导致内存泄露。 而 make_shared<T>() 只分配一次内存并初始化。(也会调用构造函数) shared_ptr 占更多内存,还要维护引用计数,效率会低一点,unique_ptr 几乎没有额外开销,效率和 new delete 几乎一样,还能自动管理内存,推荐使用 多个线程拷贝 shared_ptr,引用计数是线程安全的 多个线程修改 shared_ptr,不是线程安全的,还是需要加互斥量 引用计数的更新是线程安全的,因为更新引用计数是个原子操作 但是如果修改内存对象,那就不是线程安全的了 修改 shared_ptr 的指向,是线程不安全的,因为可能导致给真正析构的 shared_ptr 再次调用 shared_ptr,内存错误,就会报错:
1
2
pure virtual method called
terminate called without an active exception

加锁可以。

自定义删除器

可以在内存释放的时候打印一些日志,处理文件句柄、数据库连接等。 自定义删除器可以是:

  • 函数:
1
2
3
4
void free_memory(int *p) {
    ...;
    delete p;
}
  • 类对象:
1
2
3
4
5
6
7
class FreeMemory {
public:
    void operator()(int* p) {
        std::cout << "delete memory" << std::endl;
        delete p;
    }
};
  • lambda 表达式
1
2
3
4
auto free_memory_lambda = [](int* p) {
    std::cout << "delete memory" << std::endl;
    delete p;
}

定义方法:

1
2
3
std::shared_ptr<int> sp1(new int(0), free_memory);
std::shared_ptr<int> sp2(new int(0), FreeMemory());
std::shared_ptr<int> sp3(new int(0), free_memory_lambda);

这不会改变 shared_ptr 大小,都是两个字长,因为删除器是定义在控制块中的。

不要用同一个 raw pointer 初始化多个 shared_ptr 因为会产生独立的引用计数控制块,多个 shared_ptr 在析构时,都会释放所管理的内存,就会出现未定义行为。

解决方法: enable_shared_from_this 模板类 本质上就是里面维护了一个 weak_ptr

数组

C++17 增加了对数组的支持:

1
2
shared_ptr<int[]> sp1(new int[10]);
unique_ptr<int[]> up1(new int[10]);

可以用 make_shared 吗?????没找到方法,可以创建一定位数,但是没法给初值。 不建议使用,vector 更香。

This post is licensed under CC BY 4.0 by the author.