首页 > 编程语言 >c++ 智能指针

c++ 智能指针

时间:2024-05-27 14:55:23浏览次数:32  
标签:std 对象 c++ 智能 shared unique ptr 指针

 

需要头文件  <memory>

 

勿在散沙筑高台 勿在散沙筑高台

万字长文全面详解现代C++智能指针:原理、应用和陷阱

现代C++智能指针详解:原理、应用和陷阱

智能指针是C++11引入的新特性。本篇文章详细介绍了C++智能指针的原理、应用与陷阱,通过丰富的代码实例介绍了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr的原理、使用方法和适用场景,还介绍了智能指针的线程安全性、使用陷阱以及自定义删除器的使用等。本文较长,阅读需要花较长时间,但读者若能认真学习此文一定可以对智能指针有一个更为全面且深刻的认识,可以更为安全高效地使用智能指针。

1. 简述对象生命周期

1.1 程序对象的生存期

  • 全局对象在程序启动时分配,在程序结束时销毁。
  • 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
  • 局部static对象在第一次使用前分配,在程序结束时销毁。
  • C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

1.2 动态内存管理

在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

2. RAII的应用——三种智能指针

为了更容易,同时也更安全地使用动态内存,基于RAII的思想,新的标准库提供了 std::unique_ptrstd::shared_ptr类型来管理动态对象。智能指针的行为类似常规指针, 重要的区别是它负责自动释放所指向的对象 。

2.1 std::shared_ptr

2.1.1 std::shared_ptr<T>是什么

std::shared_ptr<T>是一个类模板,它的对象行为像指针,但是它还能记录有多少个对象共享它管理的内存对象。多个std::shared_ptr<T>可以共享同一个对象。当最后一个std::shared_ptr<T>被销毁时,它会自动释放它所指向的对象。一个shared_ptr<T>指针可以通过make_shared<T>函数来创建,也可以通过拷贝或赋值另一个shared_ptr来创建。如图所示,sp1和sp2指向同一个对象,内存对象的引用计数为2。当sp1被销毁时,引用计数减为1,sp2仍然指向该对象。当sp2被销毁时,引用计数减为0,内存对象被销毁。

2.1.2 std::shared_ptr的底层原理:


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

std::shared_ptr在内部维护一个引用计数,其只有两个指针成员,一个指针是所管理的数据的地址;还有一个指针是控制块的地址,包括引用计数、weak_ptr计数、删除器(Deleter)、分配器(Allocator)。因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在堆上的。而unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是raw_pointer大小的两倍。

// 32位编译器下
std::cout<<sizeof(std::shared_ptr<int>)<<std::endl; // 8
std::cout<<sizeof(std::unique_ptr<int>)<<std::endl; // 4

2.1.3 std::shared_ptr<T>的简单实现

我们通过下面这个简单的类来模拟std::shared_ptr<T>的实现,来理解引用计数的实现原理。这里我们为了简单,只实现了shared_ptr的拷贝构造函数、析构函数和赋值运算符函数,引用计数只是简单地用了一个int类型的内存空间,省略了weak_ptr的计数、删除器和分配器,不考虑多线程的情况。

  • 当我们销毁一个shared_ptr时,引用计数减1。当引用计数减为0时,我们删除指向实际数据的指针和指向引用计数的指针。
  • 当我们拷贝一个shared_ptr时,引用计数加1。
  • 当我们赋值一个shared_ptr时,我们首先递减左侧运算对象的引用计数。如果引用计数变为0,我们就释放左侧运算对象分配的内存以及引用计数的内存。然后拷贝右侧运算对象的数据指针和引用计数指针,最后递增引用计数。
template<typename T>
class shared_ptr {
public:
// constructor
shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_refCount(new int(1)) {}

// copy constructor
shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_refCount(other.m_refCount) {
    // increase the reference count
    (*m_refCount)++;
}

// destructor
~shared_ptr() {
    // decrease the reference count
    (*m_refCount)--;
    // if the reference count is zero, delete the pointer
    if (*m_refCount == 0) {
        delete m_ptr;
        delete m_refCount;
    }
}

// overload operator=()
shared_ptr& operator=(const shared_ptr& other) {
    // check self-assignment
    if (this != &other) {
        // decrease the reference count for the old pointer
        (*m_refCount)--;
        // if the reference count is zero, delete the pointer
        if (*m_refCount == 0) {
            delete m_ptr;
            delete m_refCount;
        }
        // copy the data and reference pointer and increase the reference count
        m_ptr = other.m_ptr;
        m_refCount = other.m_refCount;
        // increase the reference count
        (*m_refCount)++;
    }
    return *this;
}

private:
    T* m_ptr;            // points to the actual data
    int* m_refCount;     // reference count
};

2.1.4 std::shared_ptr<T>的内置方法

方法用途
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptr<T>p(q) p是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。
shared_ptr<T>p = q p是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。
p.unique() 如果p.use_count()为1,返回true,否则返回false。
p.use_count() 返回与p共享对象的智能指针数量。

程序示例:


std::shared_ptr<int> sp1 = std::make_shared<int>(42);
std::cout<<sp1.unique()<<std::endl; // 1
std::shared_ptr<int> sp2 = sp1;
std::shared_ptr<int> sp3(sp1);
std::shared_ptr<int> sp4(new int(44)); // Not recommended
std::cout<<sp1.use_count()<<std::endl; // 3
sp1.reset();
std::cout<<sp1.use_count()<<std::endl; // 0
std::cout<<sp2.use_count()<<std::endl; // 2

2.1.4 什么时候用std::shared_ptr<T>

通常用于一些资源创建昂贵比较耗时的场景, 比如涉及到文件读写、网络连接、数据库连接等。当需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr<T>

2.2 std::unique_ptr

unique_ptr“独占”所指向的对象。

2.2.1 std::unique_ptr<T>的原理和使用

  • std::unique_ptr独占性的实现

其不能拷贝和赋值,对应拷贝构造函数和赋值运算符函数已定义删除。

// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

  • C++14通过std::make_unique创建unique_ptr,是一种更加异常安全的做法。

  • 释放所有权 sp.release(), 返回raw pointer

unique_ptr<int> p1 = make_unique<int>(1);
int* a = p1.release();
std::cout<<*a<<std::endl;
delete a; // you need to delete it manually
  • 重置所有权 sp.reset(), 释放所有权,指向空指针
unique_ptr<int> p1 = make_unique<int>(1);
p1.reset();
std::cout<<p1.get()<<std::endl; // 0

2.2.2 std::shared_ptr 和 std::unique_ptr共有操作

方法用途
p.get() 返回p中保存的指针,不会影响p的引用计数。
p.reset() 释放p指向的对象,将p置为空。
p.reset(q) 释放p指向的对象,令p指向q。
p.reset(new T) 释放p指向的对象,令p指向一个新的对象。
p.swap(q) 交换p和q中的指针。
swap(p, q) 交换p和q中的指针。
p.operator*() 解引用p。
p.operator->() 成员访问运算符,等价于(*p).member。
p.operator bool() 检查p是否为空指针。

程序示例:

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::make_unique<int>(44);
int* p = p1.get();
std::cout<<*p<<std::endl; // 42
p1.swap(p2);
std::cout<<*p1<<std::endl; // 44
std::cout<<*p2<<std::endl; // 42
p1.reset();
std::cout<<p1.get()<<std::endl; // 0, first call get(), then call operator bool()

2.2.4 如何转移控制权?

std::move() 可以将一个unique_ptr转移给另一个unique_ptr或者shared_ptr。转移后,原来的unique_ptr将不再拥有对内存的控制权,将变为空指针。

std::unique_ptr<int> p1 = std::make_unique<int>(0);
std::unique_ptr<int> p2 = std::move(p1); 
// now, p1 is nullptr

2.2.5 什么时候用std::unique_ptr<T>

std::unique_ptr<T>std::shared_ptr<T>具有更小的内存,而且不需要维护引用计数,因此它的性能更好。当我们需要一个独占的指针时,应该优先使用std::unique_ptr<T>

2.3 std::weak_ptr

标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。

2.3.1 如何读取引用对象?

weak_ptr对它所指向的shared_ptr所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换成shared_ptr。 C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr

2.3.2 如何判断weak_ptr指向对象是否存在呢?

weak_ptr提供了一个成员函数expired()来判断所指对象是否已经被释放。如果所指对象已经被释放,expired()返回true,否则返回false。

程序示例:

std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // point to sp1
std::cout<<wp.use_count()<<std::endl; // 2
if(!wp.expired()){
    std::shared_ptr<int> sp3 = wp.lock();
    std::cout<<*sp3<<std::endl; // 22
}

2.3.3 std::weak_ptr也可以作为std::shared_ptr的构造函数参数

std::weak_ptr可以作为std::shared_ptr的构造函数参数,但如果std::weak_ptr指向的对象已经被释放,那么std::shared_ptr的构造函数会抛出std::bad_weak_ptr异常。

std::shared_ptr<int> sp1(new int(22));
std::weak_ptr<int> wp = sp1; // point to sp1
std::shared_ptr<int> sp2(wp);
std::cout<<sp2.use_count()<<std::endl; // 2
sp1.reset();
std::shared_ptr<int> sp3(wp); // throw std::bad_weak_ptr

2.3.4 std::weak_ptr 一些内置方法

方法用途
use_count() 返回与之共享对象的shared_ptr的数量
expired() 检查所指对象是否已经被释放
lock() 返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr
owner_before() 提供所有者基于的弱指针的排序
reset() 释放所指对象
swap() 交换两个weak_ptr对象

2.3.5 std::weak_ptr的使用场景

2.3.5.1 用于实现缓存

weak_ptr可以用来缓存对象,当对象被销毁时,weak_ptr也会自动失效,不会造成野指针。

假设我们有一个Widget类,我们需要从文件中加载Widget对象,但是Widget对象的加载是比较耗时的。

std::shared_ptr<Widget> loadWidgetFromFile(int id); 
// a factory function which returns a shared_ptr, which is expensive to call
// may perform file or database I/O

因此,我们希望Widget对象可以缓存起来,当下次需要Widget对象时,可以直接从缓存中获取,而不需要重新加载。这个时候,我们就可以使用std::weak_ptr来缓存Widget对象,实现快速访问。如以下代码所示:


std::shared_ptr<Widget> fastLoadWidget(int id) {
    static std::unordered_map<int, std::weak_ptr<Widget>> cache;
    auto objPtr = cache[id].lock(); 
    if (!objPtr) {
        objPtr = loadWidgetFromFile(id);
        cache[id] = objPtr; // use std::shared_ptr to construct std::weak_ptr
    }
    return objPtr;
}

当对应id的Widget对象已经被缓存时,cache[id].lock()会返回一个指向Widget对象的std::shared_ptr,否则cache[id].lock()会返回一个空的std::shared_ptr,此时,我们就需要重新加载Widget对象,并将其缓存起来,这一步会由std::shared_ptr构造std::weak_ptr

为什么不直接存储std::shared_ptr呢?因为这样会导致缓存中的对象永远不会被销毁,因为std::shared_ptr的引用计数永远不会为0。而std::weak_ptr不会增加对象的引用计数,因此,当缓存中的对象没有被其他地方引用时,std::weak_ptr会自动失效,从而导致缓存中的对象被销毁。

2.3.5.2 避免循环引用问题

  • 什么是循环引用问题 ?

循环引用是指两个或多个对象之间通过shared_ptr相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。

在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都不为0,从而导致内存泄漏。

class IObserver {
public:
    virtual void update(const string& msg) = 0;
};

class Subject {
public:
    void attach(const std::shared_ptr<IObserver>& observer) {
        observers_.emplace_back(observer);
    }
    void detach(const std::shared_ptr<IObserver>& observer) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
    }
    void notify(const string& msg) {
        for (auto& observer : observers_) {
            observer->update(msg);
        }
    }
private:
    std::vector<std::shared_ptr<IObserver>> observers_;
};

class ConcreteObserver : public IObserver {
public:
    ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
    void update(const string& msg) override {
        std::cout << "ConcreteObserver " << msg<< std::endl;
    }
private:
    std::shared_ptr<Subject> subject_;
};

int main() {
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();
    std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
    subject->attach(observer);
    subject->notify("update");
    return 0;
}
  • 避免循环引用的方法

将Observer类中的subject_成员变量改为weak_ptr,这样就打破循环引用,不会导致内存无法正确释放了。

2.3.5.3 用于实现单例模式

单例模式是指一个类只能有一个实例,且该类能自行创建这个实例的一种模式。单例模式的实现方式有很多种,其中一种就是使用std::weak_ptr

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;

std::weak_ptr实现单例模式的优点:

  1. 避免循环应用:避免了内存泄漏。
  2. 访问控制:可以访问对象,但是不会延长对象的生命周期。
  3. 可以在单例对象不被使用时,自动释放对象。

3. 关于智能指针的更多问题

3.1 尽量使用std::make_shared<T>而不是shared_ptr<T>(new T)

std::make_shared<T>是更异常安全的做法。std::make_shared<T>是一个函数模板,它在动态内存中分配一个对象并初始化它,返回指向此对象的std::shared_ptr<T>std::make_shared<T>的好处是它只进行一次内存分配,而std::shared_ptr<T>(new T)则进行两次内存分配,一次是为T分配内存,另一次是为std::shared_ptr<T>的控制块分配内存。因此,std::make_shared<T>是更好的选择。

例如:

std::shared_ptr<int> sp(new int(42)); // exception unsafe

当new int(42)抛出异常时,sp将不会被创建,从而对应new分配的内存也不会释放,从而导致内存泄漏。

3.2 智能指针SmartPointer与裸指针RawPointer的性能对比[3][4]

  • shared_ptr由于占据更多内存,且需要通过原子操作维护引用计数,因此效率是比较慢的。在不开启编译器优化的时候,是比new操作慢10倍,此时不应该使用make_shared、shared_ptr。开启优化后,也大概慢2-3倍。
  • unique_ptr、make_unique、带少许偏差的make_shared几乎和new、delete具有一样的性能。
  • unique_ptr自动管理内存资源,而几乎没有额外开销。因此效率和new、delete几乎一样。

3.3 shared_ptr的线程安全问题

如果多个线程同时拷贝同一个 shared_ptr 对象,不会有问题,因为 shared_ptr 的引用计数是线程安全的。但是如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr 对象,并且有写操作,需要使用互斥量来保护。

  • 引用计数更新,线程安全

这里我们讨论对shared_ptr进行拷贝的情况,由于此操作读写的是引用计数,而引用计数的更新是原子操作,因此这种情况是线程安全的。下面这个例子,两个线程同时对同一个shared_ptr进行拷贝,引用计数的值总是20001。

std::shared_ptr<int> p = std::make_shared<int>(0);
constexpr int N = 10000;
std::vector<std::shared_ptr<int>> sp_arr1(N);
std::vector<std::shared_ptr<int>> sp_arr2(N);

void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) {
    for (int i = 0; i < N; i++) {
        sp_arr[i] = p;
    }
}

std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
t1.join();
t2.join();
std::cout<< p.use_count() << std::endl; // always 20001
  • 同时修改内存区域,线程不安全

下面这个例子,两个线程同时对同一个shared_ptr指向内存的值进行自增操作,最终的结果不是我们期望的20000。因此同时修改shared_ptr指向的内存区域不是线程安全的。


std::shared_ptr<int> p = std::make_shared<int>(0);
void modify_memory() {
    for (int i = 0; i < 10000; i++) {
        (*p)++;
    }
}

std::thread t1(modify_memory);
std::thread t2(modify_memory);
t1.join();
t2.join();
std::cout << "Final value of p: " << *p << std::endl; // possible result: 16171, not 20000

  • 直接修改shared_ptr对象本身的指向,线程不安全。下面这个程序示例,两个线程同时修改同一个shared_ptr对象的指向,程序发生了异常终止。

std::shared_ptr<int> sp = std::make_shared<int>(1);
auto modify_sp_self = [&sp]() {
    for (int i = 0; i < 1000000; ++i) {
        sp = std::make_shared<int>(i);
    }
};

std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
    threads.emplace_back(modify_sp_self);
}
for (auto& t : threads) {
    t.join();
}

报错为:

pure virtual method called
terminate called without an active exception

用gdb查看函数调用栈,发现是在调用std::shared_ptr<int>::~shared_ptr()时出错,

(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7bc7859 in __GI_abort () at abort.c:79
#2  0x00007ffff7e73911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7e7f38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7e7f3f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7e80155 in __cxa_pure_virtual () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00005555555576c2 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() ()
#7  0x00005555555572fd in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() ()
#8  0x0000555555557136 in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() ()
#9  0x000055555555781c in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::operator=(std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>&&) ()
#10 0x00005555555573d0 in std::shared_ptr<int>::operator=(std::shared_ptr<int>&&) ()
#11 0x000055555555639f in main::{lambda()#1}::operator()() const ()
... 

其原因为:在并发修改的情况下,对正在析构的对象再次调用析构函数,导致了此异常。

对程序加锁后,程序可正常运行:

std::shared_ptr<int> sp = std::make_shared<int>(1);
std::mutex m;
auto modify = [&sp]() {
    // make the program thread safe
    std::lock_guard<std::mutex> lock(m);
    for (int i = 0; i < 1000000; ++i) {
        sp = std::make_shared<int>(i);
    }
};

std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
    threads.emplace_back(modify);
}
for (auto& t : threads) {
    t.join();
}
std::cout << *sp << std::endl;  // running as expected, result: 999999

3.4 自定义删除器 Custom Deleter

3.4.1 自定义删除器的使用场景

自定义删除器的作用是在智能指针释放所管理的对象时,执行一些特殊的操作,比如:

  • 内存释放时打印一些日志。

  • 管理除内存以外的其它资源,例如文件句柄、数据库连接等。

  • 与自定义分配器(Allocator)配合使用,将资源释放给自定义分配器。

3.4.2 自定义删除器的使用

自定义删除器可以是一个函数,也可以是一个类的对象, 也可以是一个lambda表达式。

如果是一个函数,它的形式如下:

void free_memory(int* p) {
    std::cout << "delete memory" << std::endl;
    delete p;
}

如果是一个类的对象,它的形式如下:

class FreeMemory {
public:
    void operator()(int* p) {
        std::cout << "delete memory" << std::endl;
        delete p;
    }
};

如果是一个lambda表达式,它的形式如下:

auto free_memory_lambda = [](int* p) {
    std::cout << "delete memory" << std::endl;
    delete p;
}
  • shared_ptr自定义删除器的使用:

    对于shared_ptr, 不管删除器什么类型,是否有状态都不会增加shared_ptr的大小, 均为两个字长。因为删除器是存储在控制块中,而控制块的大小为两个字长。

std::shared_ptr<int> sp1(new int(0), free_memory); // size: 8
std::shared_ptr<int> sp2(new int(0), FreeMemory()); // size: 8
std::shared_ptr<int> sp3(new int(0), free_memory_lambda); // size: 8
  • unique_ptr自定义删除器的使用:

    • unique_ptr的删除器类型是一个模板参数,因此需要指定删除器类型。
    • 如果删除器是函数指针类型,std::unique_ptr大小从1个字长增长到2个字长,因为需要存储函数指针。
    • 如果删除器是无状态删除器(stateless function),比如不进行捕获的lambda表达式,std::unique_ptr大小不变,因为无状态删除器不需要存储任何成员变量。
std::unique_ptr<int, FreeMemory> up1(new int(0)); // size: 4
std::unique_ptr<int, void(*)(int*)> up2(new int(0), free_memory);  // size: 8
std::unique_ptr<int, decltype(free_memory)*> up3(new int(0), free_memory); // size: 4

3.4.3 有状态删除器和无状态删除器

什么是有状态删除器?什么是无状态删除器?有状态删除器是指删除器类中包含有成员变量,无状态删除器是指删除器类中不包含有成员变量。

如果std::unique_ptr的函数对象删除器是具有扩展状态的,其大小可能会非常大。如果大得无法接受,可能需要设计一个无状态删除器。

下面是一个有状态删除器的例子:

class DeleteObject {
public:
    DeleteObject(int n) : n_(n) {}
    void operator()(int* p) {
        std::cout << "delete memory " << n_ << std::endl;
        delete p;
    }
private:
    int n_;
};

3.5 避免用同一个raw pointer初始化多个shared_ptr

3.5.1 为什么不要用同一个raw pointer初始化多个shared_ptr

因为多个shared_ptr由同一个raw pointer创建时会导致生成两个独立的引用计数控制块,从以下程序可见sp1、sp2的引用计数都为1。

int* p = new int(0);
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p);
std::cout<<sp1.use_count()<<std::endl; // 1
std::cout<<sp2.use_count()<<std::endl; // 1

当sp1、sp2销毁时会产生未定义行为,因为shared_ptr的析构函数会释放它所管理的对象,当sp1析构时,会释放p指向的内存,当sp2析构时,会再次释放p指向的内存。

3.5.2 enable_shared_from_this模板类

  • 作用:用于在类对象的内部中获得一个指向当前对象的 shared_ptr 对象

  • 解决问题: 如果通过this指针创建shared_ptr时,相当于通过一个裸指针创建shared_ptr,多次创建会导致多个shared_ptr对象管理同一个内存。当shared_ptr对象销毁时,会释放this指向的内存,但是this指针可能还会被使用,导致程序崩溃。

class A {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return std::shared_ptr<A>(this); // error
    }
};

  • 使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。
class A : public std::enable_shared_from_this<A> {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return shared_from_this();
    }
};
  • 原理:在类中维护一个weak_ptr,将weak_ptr作为参数传入shared_ptr的构造函数,返回一个shared_ptr对象。 [8]
template<typename _Tp>
class enable_shared_from_this
{
protected:
    constexpr enable_shared_from_this() noexcept = default;
    enable_shared_from_this(const enable_shared_from_this&) noexcept = default;
    enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept = default;
    ~enable_shared_from_this() = default;
public:
    shared_ptr<_Tp> shared_from_this()
    {
        shared_ptr<_Tp> __p(_M_weak_this);
        return __p;
    }

    shared_ptr<const _Tp> shared_from_this() const
    {
        shared_ptr<const _Tp> __p(_M_weak_this);
        return __p;
    }

    weak_ptr<_Tp> weak_from_this() noexcept // C++17
    {
        return _M_weak_this;
    }

    weak_ptr<const _Tp> weak_from_this() const noexcept // C++17
    {
        return _M_weak_this;
    }

    template<typename _Up> friend class shared_ptr;
};
  • 限制:只能用于继承自enable_shared_from_this的类。

  • 适用场景:在类的内部需要获得一个指向当前对象的shared_ptr对象时,可以使用enable_shared_from_this模板类。

3.6 智能指针模板中的类型可以是数组吗?

std::shared_ptrstd::unique_ptr都可以指向数组。在C++17后,std::shared_ptr也提供了operator[]操作符,可以像访问数组一样访问std::shared_ptr指向的数组。 [5]

std::shared_ptr<int[]> sp1(new int[10]);
std::unique_ptr<int[]> up1(new int[10]);
for (int i = 0; i < 10; i++) {
    sp1[i] = i;
    up1[i] = i;
}

数组类型的std::shared_ptrstd::unique_ptr是一种知识性的兴趣,因为C++中有更好的容器类型std::vectorstd::arraystd::string来替代原始数组。[2]

参考资料

  1. C++ primer 5th. Stanley B. Lippman, Josée Lajoie, Barbara E. Moo. 2012.
  2. Effective Modern C++. Scott Meyers. 2014.
  3. memory-and-performance-overhead-of-smart-pointer
  4. how-much-is-the-overhead-of-smart-pointers-compared-to-normal-pointers-in-c
  5. https://en.cppreference.com/w/cpp/memory/shared_ptr
  6. https://en.cppreference.com/w/cpp/memory/unique_ptr
  7. https://en.cppreference.com/w/cpp/memory/weak_ptr
  8. https://en.cppreference.com/w/cpp/memory/enable_shared_from_this

你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、软件架构等计算机基础知识。希望我们能一起探索程序员修炼之道,最终能站得更高,走得更远。如果你有任何问题或者建议,欢迎随时与我交流。如果我的创作内容对您有帮助,请点赞关注。感谢你的阅读。

微信、知乎搜索:七昂的技术之旅

 

=============

本文介绍c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。

为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。下面我们逐个介绍。

auto_ptr (官方文档

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Test { public:     Test(string s)     {         str = s;        cout<<"Test creat\n";     }     ~Test()     {         cout<<"Test delete:"<<str<<endl;     }     string& getStr()     {         return str;     }     void setStr(string s)     {         str = s;     }     void print()     {         cout<<str<<endl;     } private:     string str; };     int main() {     auto_ptr<Test> ptest(new Test("123"));     ptest->setStr("hello ");     ptest->print();     ptest.get()->print();     ptest->getStr() += "world !";     (*ptest).print();     ptest.reset(new Test("123"));     ptest->print();     return 0; }

运行结果如下

image

如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();

当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL),如下代码                                                           本文地址

1 2 3 4 5 6 7 8 9 int main() {     auto_ptr<Test> ptest(new Test("123"));     auto_ptr<Test> ptest2(new Test("456"));     ptest2 = ptest;     ptest2->print();     if(ptest.get() == NULL)cout<<"ptest = NULL\n";     return 0; }

image

还有一个值得我们注意的成员函数是release,这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权,从下面的代码执行结果可以看出,析构函数没有被调用。

1 2 3 4 5 6 int main() {     auto_ptr<Test> ptest(new Test("123"));     ptest.release();     return 0; }

image

那么当我们想要在中途释放资源,而不是等到智能指针被析构时才释放,我们可以使用ptest.reset(); 语句。


unique_ptr (官方文档) 

 

 

unique_ptr,是用于取代c++98的auto_ptr的产物,在c++98的时候还没有移动语义(move semantics)的支持,因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把auto_ptr作为函数参数,传进去的时候控制权转移,转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的auto_ptr已经失效了.在c++11当中有了移动语义,使用move()把unique_ptr传入函数,这样你就知道原先的unique_ptr已经失效了.移动语义本身就说明了这样的问题,比较坑爹的是标准描述是说对于move之后使用原来的内容是未定义行为,并非抛出异常,所以还是要靠人肉遵守游戏规则.再一个,auto_ptr不支持传入deleter,所以只能支持单对象(delete object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等.

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

1、拥有它指向的对象

2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

1、为动态申请的内存提供异常安全

2、讲动态申请的内存所有权传递给某函数

3、从某个函数返回动态申请内存的所有权

4、在容器中保存指针

5、auto_ptr 应该具有的功能

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unique_ptr<Test> fun() {     return unique_ptr<Test>(new Test("789")); } int main() {     unique_ptr<Test> ptest(new Test("123"));     unique_ptr<Test> ptest2(new Test("456"));     ptest->print();     ptest2 = std::move(ptest);//不能直接ptest2 = ptest     if(ptest == NULL)cout<<"ptest = NULL\n";     Test* p = ptest2.release();     p->print();     ptest.reset(p);     ptest->print();     ptest2 = fun(); //这里可以用=,因为使用了移动构造函数     ptest2->print();     return 0; }

image

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值


share_ptr (官方文档)

从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。具体的成员函数解释可以参考 here

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main() {     shared_ptr<Test> ptest(new Test("123"));     shared_ptr<Test> ptest2(new Test("456"));     cout<<ptest2->getStr()<<endl;     cout<<ptest2.use_count()<<endl;     ptest = ptest2;//"456"引用次数加1,“123”销毁     ptest->print();     cout<<ptest2.use_count()<<endl;//2     cout<<ptest.use_count()<<endl;//2     ptest.reset();     ptest2.reset();//此时“456”销毁     cout<<"done !\n";     return 0; }

image


weak_ptr(官方文档)

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class B; class A { public:     shared_ptr<B> pb_;     ~A()     {         cout<<"A delete\n";     } }; class B { public:     shared_ptr<A> pa_;     ~B()     {         cout<<"B delete\n";     } };   void fun() {     shared_ptr<B> pb(new B());     shared_ptr<A> pa(new A());     pb->pa_ = pa;     pa->pb_ = pb;     cout<<pb.use_count()<<endl;     cout<<pa.use_count()<<endl; }   int main() {     fun();     return 0; }

image

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

image

注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr<B> p = pa->pb_.lock();    p->print();

 

参考资料

胡健:http://www.cnblogs.com/hujian/archive/2012/12/10/2810776.html

胡健:http://www.cnblogs.com/hujian/archive/2012/12/10/2810754.html

胡健:http://www.cnblogs.com/hujian/archive/2012/12/10/2810785.html

天方:http://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html

gaa_ra:http://blog.csdn.net/gaa_ra/article/details/7841204

cplusplus:http://www.cplusplus.com/

 

【版权声明】转载请注明出处:http://www.cnblogs.com/TenosDoIt/p/3456704.html

https://www.cnblogs.com/TenosDoIt/p/3456704.html

====================

 

 

 

 

参考:

 

标签:std,对象,c++,智能,shared,unique,ptr,指针
From: https://www.cnblogs.com/rebrobot/p/18215501

相关文章

  • 百川智能发布Baichuan 4模型 AI助手“百小应”上线
        大家好,我是AI科技智库(www.aigchouse.com),国内Top10计算机博士毕业,创办了一站式AI工具、资料、课程资源学习平台,每日持续更新。通过分享最新AI工具、资源、变现指南等,帮助更多人了解使用AI,提升工作和学习效率。这里有海量AI工具整合包、AI变现指南、AI学习资料、AI免费......
  • C++ 资源管理要点总结
    C++资源管理要点:使用智能指针:C++11引入了更科学的智能指针,以便自动管理对象的生命周期。三种主要的智能指针类型包括:unique_ptr、shared_ptr和weak_ptr。unique_ptr拥有独占的对象所有权,当指针超出作用域时自动释放资源。shared_ptr可以共享对象所有权,使用引用计数技术,......
  • 【c++提高组】津津的储蓄计划(NOIP2004)
    题目描述津津的零花钱一直都是自己管理。每个月的月初妈妈给津津 300元钱,津津会预算这个月的花销,并且总能做到实际花销和预算的相同。为了让津津学习如何储蓄,妈妈提出,津津可以随时把整百的钱存在她那里,到了年末她会加上 20%还给津津。因此津津制定了一个储蓄计划:每个月的......
  • 【C++/STL】vector(常见接口、模拟实现、迭代器失效)
     ......
  • L4-202 二叉搜索树的2层结点统计(C++)
    二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树。将一系列数字按给定顺序插入一棵初始为空的二叉搜......
  • P3406 海底高铁(C++)
    海底高铁题目描述该铁路经过NNN个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。......
  • n-皇后问题(c++)
    ......
  • 取代或转型?人工智能对软件测试的影响(内附工具推荐)
    在当今快速发展的数字环境中,从移动App到基于Web的平台,软件已成为我们日常生活和工作不可或缺的一部分。然而,随着软件系统变得越来越复杂,如何确保其质量和可靠性已成为开发人员和测试人员所面临的一大重要挑战。这就是软件测试中的人工智能(AI)成为颠覆性技术的地方,它改变了软件测试......
  • C/C++ 指针注意事项
    C/C++中的指针是强大的工具,但需要谨慎使用,错误的使用可能会导致程序崩溃或者内存泄漏。以下指针相关注意事项:初始化指针:在使用指针之前,一定要初始化它,否则它将指向一个随机的内存地址,这可能导致程序崩溃。未初始化的指针通常被称为“野指针”。避免空指针解引用:在解引用......
  • Qt/C++音视频开发75-获取本地有哪些摄像头名称/Qt内置函数方式
    一、前言在需要打开本地摄像头的场景中,有个需求绕不开,那就是如何获取本地有哪些摄像头设备名称,这样可以提供下拉框给用户选择,不然你让用户去填设备名,你觉得用户会知道是啥,他会操作吗?就算你提供了详细的查看步骤,估计也很难,如果用户是程序员还好,如果是电脑小白,鼠标都用不好,你还让他......