C++11 智能指针 shared_ptr

Written on 2023-01-16

个人学习智能指针记录合集:

std::shared_ptr 共享智能指针,也被称为计数智能指针。

共享智能指针会记录有多少个共享智能指针指向同一个对象,当这个数为 0 的时候,程序自动的默认释放(析构)这个对象,记录有多少个的这个方法叫做引用计数

共享智能指针可以有多个共享智能指针同时管理同一个对象。

举个栗子

普通指针管理

#include <iostream>
#include <memory>
using namespace std;
class Person{
public: 
    Person(){ cout << "Constructor: person's age = " << m_age << endl; }
    Person(int age) : m_age(age){ cout << "Constructor: person's age = " << m_age << endl; }
    ~Person(){ cout << "Destructor: person's age = " << m_age << endl; }
    void getAge(){ cout << "Person's age = " << m_age << endl; }
private:
    int m_age = 0; 
}; 
int main()
{
    {
        Person *p = new Person(18);
    }
    cout << endl << "Before return" << endl;
    return 0;
}
/** 输出:
Constructor: person's age = 18
Before return
**/

依输出可见,p指向的对象并没有被析构,因为并没有析构函数中的打印,这造成了内存泄漏。

shared_ptr智能指针管理

// ...
int main()
{
    {
        shared_ptr<Person> sPtr1 {new Person(18)};
    }
    cout << endl << "Before return" << endl;
    return 0;
}
/** 输出:
Constructor: person's age = 18
Destructor: person's age = 18
Before return
**/

依输出可见,离开了程序块的作用域后,析构函数中的打印体现出来了,sPtr1管理的对象自动的被析构了。

获取引用计数

long use_count() const noexcept;

shared_ptrnullptr 时,返回为 0。

初始化

创建空管理的 shared_ptr

constexpr shared_ptr() noexcept;
constexpr shared_ptr(std::nullptr_t) noexcept;

创建对非数组对象和数组对象管理的 shared_ptr

template< class Y >
explicit shared_ptr( Y* ptr );
// ...
int main()
{
    shared_ptr<int> sPtr1; // 创建空管理的shared_ptr
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    shared_ptr<int> sPtr2(nullptr); // 创建空管理的shared_ptr
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    shared_ptr<int> sPtr3(new int(100)); // 创建对非数组对象管理的shared_ptr
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    shared_ptr<int> sPtr4(new int[10]); // 创建对数组对象管理的shared_ptr
    cout << "sPtr4 use count = " << sPtr4.use_count();
    return 0;
}
/** 输出:
sPtr1 use count = 0
sPtr2 use count = 0
sPtr3 use count = 1
sPtr4 use count = 1
**/

解释:
sPtr1sPtr2都为nullptr,故引用计数都为 0;
sPtr3sPtr4所管理的对象,都只有一个 shared_ptr 管理,故引用计数都为 1。

创建指定删除器的 shared_ptr

template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );
template< class Deleter >
shared_ptr( std::nullptr_t ptr, Deleter d );
template< class Y, class Deleter, class Alloc >
shared_ptr( Y* ptr, Deleter d, Alloc alloc );
template< class Deleter, class Alloc >
shared_ptr( std::nullptr_t ptr, Deleter d, Alloc alloc );
  1. 默认删除器:默认对指向的内存地址进行释放,即析构掉这块地址的内容
    1. 非数组类型,以 delete ptr 为默认删除器
    2. 数组类型,以 delete[] ptr 为默认删除器
  2. 指定删除器:能够自行指定引用计数为 0 时,要做什么,比如有些场景不需要对指向的内存进行释放,比如关闭指向文件,关闭指向套接字
    1. 指定删除器的函数结构:
      void deletePtr(T* p){ ... }
      
    2. 也可使用 Lambda表达式

例,对于文件的使用场景,不是直接delete文件,而是关闭文件:

// ...
void closeFile(FILE* fp) 
{
    if (fp == nullptr) return;
    fclose(fp);
    cout << "File closed" << endl;
}
int main() 
{
    FILE* fp = fopen("data.txt" ,"w");
    shared_ptr<FILE> sfp{fp,closeFile};
    if (sfp == nullptr)
        cout << "Failed opened" << endl;
    else 
        cout << "File opened" << endl;
    return 0;
}

创建通过现有共享智能指针的 shared_ptr

主要是通过复制构造函数和移动构造函数std::move()

shared_ptr( const shared_ptr& r ) noexcept;
template< class Y >
shared_ptr( const shared_ptr<Y>& r ) noexcept;
shared_ptr( shared_ptr&& r ) noexcept;
template< class Y >
shared_ptr( shared_ptr<Y>&& r ) noexcept;    

例,

// ...
int main()
{
    shared_ptr<int> sPtr1(new int(100));
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl << endl;
    // 创建通过现有现有共享智能指针的 shared_ptr
    // 通过拷贝构造函数创建
    shared_ptr<int> sPtr2(sPtr1);
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl << endl;
    // 通过赋值运算符创建
    shared_ptr<int> sPtr3 = sPtr1;
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl << endl;
    // 通过移动构造函数创建
    shared_ptr<int> sPtr4(move(sPtr1));
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    cout << "sPtr4 use count = " << sPtr4.use_count() << endl << endl;
    std::shared_ptr<int> sPtr5 = move(sPtr2);
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    cout << "sPtr4 use count = " << sPtr4.use_count() << endl;
    cout << "sPtr5 use count = " << sPtr5.use_count();
    return 0;
}
/** 输出:
sPtr1 use count = 1
sPtr1 use count = 2
sPtr2 use count = 2
sPtr1 use count = 3
sPtr2 use count = 3
sPtr3 use count = 3
sPtr1 use count = 0
sPtr2 use count = 3
sPtr3 use count = 3
sPtr4 use count = 3
sPtr1 use count = 0
sPtr2 use count = 0
sPtr3 use count = 3
sPtr4 use count = 3
sPtr5 use count = 3
**/

解释:
sPtr2sPtr3都是通过对sPtr1执行了复制构造函数,因此sPtr1的引用计数一次增加 1;sPtr2sPtr3同理。
通过move(sPtr1),使得sPtr1释放了被管理对象的所有权,此时sPtr1被设置为nullptr,因此sPtr1引用计数为 0;sPtr2同理。

通过 std::make_shared

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );

这种初始化效率更高,在 C++17 之前的编译器更安全

// ...
int main()
{
    shared_ptr<int> sPtr1 = make_shared<int>(100);
    shared_ptr<int> sPtr2(make_shared<int>(200));
    shared_ptr<int> sPtr3{make_shared<int>(300)};
    return 0;
}

通过 .reset( ptr )

使用.reset( ptr ),使得释放对shared_ptr原管理对象的所有权,转为对新对象管理的所有权。

void reset() noexcept;
template< class Y >
void reset( Y* ptr );
template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );
template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );

注意.reset( ptr )若传入的所指向的对象已被占有,程序会异常。

// ...
int main()
{
    shared_ptr<int> sPtr1(new int(100));
    shared_ptr<int> sPtr2 = sPtr1;
    shared_ptr<int> sPtr3 = sPtr2;
    shared_ptr<int> sPtr4 = sPtr1;
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    cout << "sPtr4 use count = " << sPtr4.use_count() << endl << endl;
    sPtr4.reset();
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    cout << "sPtr4 use count = " << sPtr4.use_count() << endl << endl;
    sPtr3.reset(new int(100));
    cout << "sPtr1 use count = " << sPtr1.use_count() << endl;
    cout << "sPtr2 use count = " << sPtr2.use_count() << endl;
    cout << "sPtr3 use count = " << sPtr3.use_count() << endl;
    cout << "sPtr4 use count = " << sPtr4.use_count();
    std::shared_ptr<int> sPtr5;
    sPtr5.reset(sPtr1.get()); // 异常
    return 0;
}
/** 输出:
sPtr1 use count = 4
sPtr2 use count = 4
sPtr3 use count = 4
sPtr4 use count = 4
sPtr1 use count = 3
sPtr2 use count = 3
sPtr3 use count = 3
sPtr4 use count = 0
sPtr1 use count = 2
sPtr2 use count = 2
sPtr3 use count = 1
sPtr4 use count = 0
**/

解释:

  1. 通过复制构造函数后,sPtr1-4的引用计数均为 4。
  2. sPtr4使用了reset()后,释放被管理对象的所有权,被管理对象的shared_ptr个数减 1,sPtr4被设置为nullptr,同时,sPtr1-3的引用计数均变为 3。
  3. sPtr3使用了reset(new int(100))后,释放原被管理对象的所有权,被管理对象的shared_ptr个数减 1,sPtr4被设置为新的管理对象int(100),同时,sPtr1-3的引用计数均变为 2。
  4. sPtr5reset传入的所指向的对象已被占有,程序异常,没有正常结束。

.reset() 释放被管理对象的所有权

用于取消shared_ptr对管理对象的所有权;
当这个对象被shared_ptr管理的数量为 0,会执行删除器。

使用.reset(),会使得shared_ptr设置为nullptr

获取原始储存的指针

T* get() const noexcept;

解引用存储的指针 operator* and operator->

可以像普通指针一样,使用shared_ptr对所管理的对象进行访问。

// ...
int main()
{
    shared_ptr<int> sPtr1 {new int(100)};
    cout << *sPtr1 << endl << endl;
    int *i = sPtr1.get();
    cout << *i << endl;
    cout << *sPtr1 << endl << endl;
    *i = 200;
    cout << *i << endl;
    cout << *sPtr1 << endl << endl;
    *sPtr1 = 300;
    cout << *i << endl;
    cout << *sPtr1;
    return 0;
}
/** 输出:
100
100
100
200
200
300
300
**/

直观展示自动管理内存

// ...
int main()
{
    shared_ptr<Person> sPtr1 {make_shared<Person>()};
    shared_ptr<Person> sPtr2 {make_shared<Person>(18)};
    shared_ptr<Person> sPtr3 {make_shared<Person>(22)};
    shared_ptr<Person> sPtr4 = sPtr1;
    sPtr1.reset();
    sPtr2.reset();
    sPtr3.reset(new Person(19));
    cout << endl << "Before return" << endl;
    return 0;
}
/** 输出:
Constructor: person's age = 0
Constructor: person's age = 18
Constructor: person's age = 22
Destructor: person's age = 18
Constructor: person's age = 19
Destructor: person's age = 22
Before return
Destructor: person's age = 0
Destructor: person's age = 19
**/

解释:

  1. 定义了三个shared_ptrsPtr1sPtr2sPtr3,它们管理的对象分别为age = 0age = 18age = 22,打印了三行Person对象的构造函数中的输出
  2. 通过赋值运算符,使得sPtr4同时管理sPtr1管理的对象
  3. 释放sPtr1被管理对象的所有权,此时因为sPtr4还在管理原sPtr1管理的对象age = 0,因此age = 0对象并没有被析构
  4. 释放sPtr2被管理对象的所有权,此时因为age = 18对象没有任何shared_ptr进行管理,age = 18对象被析构,打印了age = 18对象的析构函数中的输出
  5. 释放sPtr3被管理对象的所有权,sPtr3转为管理age = 19的对象;是先构造age = 19对象,后再释放sPtr3被管理对象的所有权,打印了age = 19对象的构造函数中的输出;此时因为sPtr3原管理的age = 22对象没有任何shared_ptr进行管理,age = 22对象被析构,打印了age = 22对象的析构函数中的输出
  6. 在程序返回前Before return,管理age = 0age = 19对象的智能指针sPtr4sPtr3被析构,age = 0age = 19对象没有智能指针管理,age = 0age = 19对象被析构,打印了它们对象的析构函数中的输出。

别名

template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

别名用于访问类的成员变量。

我们需要访问的是某个实例的成员,因此并不希望在使用这个成员的时候,对应的实例被销毁了。

使用别名的shared_ptr,增加了对这个实例的控制权,但仍然访问的是成员。

// ...
struct Person{ int age = 18; };
struct Student{ Person person; };
int main(){
    shared_ptr<Student> studentPtr{make_shared<Student>()};
    cout << "studentPtr use count = " << studentPtr.use_count() << endl;
    shared_ptr<Person> personPtr{studentPtr, &(studentPtr->person)};
    cout << "studentPtr use count = " << studentPtr.use_count() << endl;
    cout << personPtr->age << endl;
    return 0;
}
/** 输出:
studentPtr use count = 1
studentPtr use count = 2
18
**/

shared_ptr 与函数

按值传递

int main(){
	auto func = [](shared_ptr<int> sPtr){
        cout << "value = " << *sPtr << endl;
        cout << "enter func: use count = " << sPtr.use_count() << endl;
    };
    auto sPtr = make_shared<int>(100);
    cout << "init: use count = " << sPtr.use_count() << endl;
    func(sPtr);
    cout << "exit func: use count = " << sPtr.use_count() << endl;
    return 0;
}
/** 输出:
init: use count = 1
value = 100
enter func: use count = 2
exit func: use count = 1
**/

按值传递,函数会复制一份参数,因此传入的sPtr会被复制一份,造成其对象的引用计数增加 1;
执行完这个函数,复制的那一份sPtr被销毁,使得其对象的引用计数减少 1;

按引用传递

int main(){
	auto func = [](shared_ptr<int> &sPtr){
        cout << "value = " << *sPtr << endl;
        cout << "enter func: use count = " << sPtr.use_count() << endl;
    };
    auto sPtr = make_shared<int>(100);
    cout << "init: use count = " << sPtr.use_count() << endl;
    func(sPtr);
    cout << "exit func: use count = " << sPtr.use_count() << endl;
    return 0;
}
/** 输出:
init: use count = 1
value = 100
enter func: use count = 1
exit func: use count = 1
**/

按引用传递,函数不会复制一份参数;
因此若函数内部无其它导致增加引用计数的操作,函数执行过程中引用计数都不会改变。

按引用传递,但为const

int main(){
    auto func = [](const shared_ptr<int> &sPtr){
        cout << "value = " << *sPtr << endl;
        cout << "enter func: use count = " << sPtr.use_count() << endl;
        sPtr.reset(); // error
        sPtr.reset(new Person()); // error
        sPtr.release(); // error
    };
    auto sPtr = make_shared<int>(100);
    cout << "init: use count = " << sPtr.use_count() << endl;
    func(sPtr);
    cout << "exit func: use count = " << sPtr.use_count() << endl;
    return 0;
}

使用const的引用传递,不能改变shared_ptr所管理的对象是哪一个,使用.reset()等都会造成编译错误。

返回值为shared_ptr

int main(){
	auto createSPtr = [](int i) -> shared_ptr<Person>{
        shared_ptr<Person> sPtr = make_shared<Person>(i);
        cout << "age = " << i << " use count = " << sPtr.use_count() << endl;
        return sPtr;
    };
    shared_ptr<Person> sPtr = createSPtr(100);
    sPtr->getAge(); cout << "use count = " << sPtr.use_count() << endl;
    // 用作链式函数
    createSPtr(200)->getAge();
    cout << endl << "Before main return" << endl;
    return 0;
}
/** 输出:
Constructor: person's age = 100
age = 100 use count = 1
Person's age = 100
use count = 1
Constructor: person's age = 200
age = 200 use count = 1
Person's age = 200
Destructor: person's age = 200
Before main return
Destructor: person's age = 100
**/

可见当用作链式函数时,使用完毕后,unique_ptr会被销毁,同时被管理的对象也被析构。

管理动态数组需要指定删除器

shared_ptr 的默认删除器不支持释放数组对象,需要指定删除器。

例,一维数组指定删除器

    // ...
    shared_ptr<int> ptr(new int[10], [](int* p) {delete[] p; });

同时,也可以使用 std::default_delete<T>() 函数作为删除器,这个函数内部的删除功能是通过delete释放,T 为释放什么类型的内存的类型。

例,一维数组指定删除器

    // ...
    shared_ptr<int> ptr(new int[10], default_delete<int[]>());

可以自己封装模板函数来使 shared_ptr 支持释放数组对象。

// ...
template <typename T>
shared_ptr<T> arrayShared_ptr(size_t size)
{
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}
int main()
{
    shared_ptr<int> sPtr1 = arrayShared_ptr<int>(100);
    shared_ptr<int> sPtr2 = arrayShared_ptr<char>(200);
    return 0;
}

危险行为

仍然可使用delete释放智能指针管理的对象的地址

可以使用delete释放shared_ptr管理的对象的地址,但是其它共享指针仍然可能会访问这块地址,若访问了则会出现程序异常,因此应避免使用手动的delete

    // ...
    shared_ptr<int> sPtr1 {new int(100)};
    shared_ptr<int> sPtr2 = sPtr1;
    delete sPtr1.get();
    cout << *sPtr2 << endl; // error,运行时异常

同时存在原始指针和智能指针

如果一个地址内存,同时有原始指针和shared_ptr指向它,即使当所有shared_ptr都被销毁,原始指针还依然存在的情况下,这个地址内存仍然会被释放,若再用原始指针去访问这个内存地址就是访问了一块未知地址的内容。

    // ...
	int *i1 = new int{100};
    shared_ptr<int> sPtr1{i1};
    int *i2 = sPtr1.get();
    cout << *i1 << endl;
    cout << *i2 << endl << endl;
    sPtr1.reset();
    cout << *i1 << endl; // 危险行为 
    cout << *i2 << endl; // 危险行为 
	return 0;
/** 输出:
100
100
1664688 // 这是随机的,访问了一块未知地址的内容
1664688 // 这是随机的,访问了一块未知地址的内容
**/

同时,无论是使用shared_ptr,还是其它的智能指针,都应该避免与原始指针混用。

避免同时存在原始指针和智能指针的解决方案:

    // ...
    int *i = new int{100};
    shared_ptr<int> sPtr1{i};
    i = nullptr;
    delete i;
    cout << *sPtr1 << endl; // ok

使用一个原始指针初始化多个shared_ptr

不能使用一个原始指针初始化多个shared_ptr

    // ...
    int *i = new int{100};
    shared_ptr<int> sPtr1{i};
    shared_ptr<int> sPtr2{i}; // error 编译通过 运行错误

最后

shared_ptr 由于使用引用计数,因此会造成额外的内存和性能开销,因此在性能要求极为苛刻的情况下不适用。

unique_ptr是 0 开销的智能指针,也能够自动管理内存,但不会造成性能损失。

发表回复