C++11新特性 1 智能指针 shared_ptr 学习笔记

506 阅读3分钟

智能指针 shared_ptr

正常使用指针的时候是这样的

#include <iostream>

using namespace std;

class A 
{
public:
    ~A() {
        cout << "delete A" << endl;
    }
};

int main()
{
    {
        A *pa = new A;
    }    

    cout << "main finish" << endl;
    return 0;
}

输出是这样的

$ g++ -o 1-1-shared_from_this 1-1-shared_from_this.cpp -std=c++11 && ./1-1-shared_from_this

main finish

是不会主动调用析构函数的

使用普通指针, 容易造成堆内存泄漏(忘记释放啊), 二次释放, 程序发生异常时内存泄漏问题, 使用智能指针能更好管理堆内存

  • auto_ptr, 这个被c++11弃用了
  • unique_ptr
  • shared_ptr
  • wark_ptr

一个例子能大致的显示出智能指针的优势

#include <iostream>
#include <memory>

using namespace std;

class A 
{
public:
    ~A() {
        cout << "delete A" << endl;
    }
};

int main()
{
    {
        A *pa = new A;
        std::shared_ptr<A> pb(new A);
    }    

    cout << "main finish" << endl;
    return 0;
}

输出

$ g++ -o 1-1-shared_from_this2 1-1-shared_from_this2.cpp -std=c++11 && ./1-1-shared_from_this2

delete A
main finish

思考一下, 这个智能指针咋实现自动回收的

也是使用引用计数, Java和Python好像也是这么做的

1. shared_ptr 共享的智能指针

std::shard_ptr使用引用计数, 每一个shared_ptr的拷贝都指向相同的内存, 在最后一个shared_ptr析构的时候, 内存会被释放

shared_ptr共享被管理对象, 同一时刻可以有多个shared_ptr拥有对象的所有权, 当最后一个shared_ptr对象销毁的时候, 被管理对象自动销毁

shared_ptr包含了两部分:

  • 一个指向上创建的对象的裸指针 raw_ptr
  • 一个指向内部隐藏的, 共享的管理对象 shared_count_object, 就是use_count表明当前堆上的这个对象被多少对象引用了, 其实就是计数

1.1 shared_ptr用法

1.1.1 初始化

构造函数, std::shared_ptr辅助函数, reset

// 智能指针初始化 
std::shared_ptr p1(new int(1)); 
std::shared_ptr p2 = p1; 
std::shared_ptr p3; 
p3.reset(new int(1)); 
if(p3) { 
    cout << "p3 is not null"; 
}

优先考虑make_shared构造智能指针, 更高效

auto sp1 = make_shared<int>(100);
// 相当于
shared_ptr<int> sp1(new int(100));

不能将一个原始指针直接赋值给一个智能指针

std::shared_ptr<int> p = new int(1);

对于一个未初始的智能指针, 可以通过reset初始化

当智能指针有值时调用reset会引起引用计数-1

#include <iostream>
#include <memory>

int main() {
    auto sp1 = std::make_shared<int>(100); // 优先使用make_shared来构造智能指针
    // 相当于
    std::shared_ptr<int> sp2(new int(100));

    // std::shared_ptr<int> p = new int(1); // 不能将一个原始指针直接赋值给一个智能指针
    
    std::shared_ptr<int> p1;
    p1.reset(new int(1)); // 分配资源
    std::shared_ptr<int> p2 = p1;

    // 引用计数器此时应该是2
    std::cout << "p2.use_count() = " << p2.use_count() << std::endl;
    p1.reset(); // 释放资源, p1变成空
    std::cout << "p1.reset()\n";
    // 引用计数器此时应该是1
    std::cout << "p2.use_count() = " << p2.use_count() << std::endl;

    if (!p1) {
        std::cout << "p1 is empty\n";
    }
    if (!p2) {
        std::cout << "p2 is empty\n";
    }

    p2.reset();
    std::cout << "p2.reset()\n";
    std::cout << "p2.use_count() = " << p2.use_count() << std::endl;

    if (!p2) {
        std::cout << "p2 is empty" << std::endl;
    }
    
    return EXIT_SUCCESS;
    
}

1.1.2 获取原始指针

std::shared_ptr<int> ptr(new int(1));
int* p = ptr.get(); // 这是非常危险的操作
// 如果你不小心还delete p
// 就会重复释放两次资源
// 可能会出现段错误

谨慎使用p.get()的返回值, 如果你不知道其危险性则永远不要调用get()函数

  • 不要保存p.get()的返回值, 无论是保存为裸指针还是shared_ptr都是不对的
  • 保存为裸指针不知道啥时候会变成空悬指针, 保存为shared_ptr则产生了独立指针
  • 不要delete p.get(), 会导致一块内存被释放两次

1.1.3 指定删除器

是个比较重要的东西哈

如果用shared_ptr<int>管理非new对象或是没有析构的类时, 应当为其传递合适的删除器

先定义删除器

void DeleteIntPtr(int* p) 
{
    std::cout << "call DeleteIntPtr" << std::endl;
    delete p;
}
// 调用
std::shared_ptr<int> p(new int(1), DeleteIntPtr);

p的引用计数器为0时, 自动调用删除器DeletePtr来释放对象的内存

也可以使用lambda表达式

std::shared_ptr<int> p2(new int(1), [](int* p) {
        std::cout << "call DeleteIntPtr" << std::endl;
        delete p;
    });

也是等效的

如果需要管理动态数组时, 需要指定删除器, 因为shared_ptr的默认删除器是不支持数组对象

std::shared_ptr<int> p3(new int[10], [](int* p) {
        delete []p;
    }

1.2 使用shared_ptr要注意的问题

1.2.1 不要用一个原始指针初始化多个shared_ptr, 比如

int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr); // 逻辑错误

1.2.2 不要再函数实例中创建shared_ptr

function(std::shared_ptr<int>(new int), g()); // 有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的, 一般是从又到左

如果g()发生异常, 而shared_ptr还没有创建, 则int内存泄漏

应该先创造智能指针

std::shared_ptr<int> p(new int);
function(p, g());

1.2.3 通过shared_from_this()返回this指针

不要将this指针作为std::shared_ptr返回出来, 因为this本质上是一个裸指针, 因此, 如果这么操作可能会导致重复析构

定义A类型

class A 
{
public:
    std::shared_ptr<A> GetSelf() {
        return std::shared_ptr<A>(this); // 不要这么操作
    }
    ~A() {
        std::cout << "Deconstruction A" << std::endl;
    }
};

调用

int main() 
{
    std::shared_ptr<A> sp1(new A);
    std::shared_ptr<A> sp2 = sp1->GetSelf();

    return 0;
}

运行

g++ 1-1-shared_from_this2.cpp -std=c++11 && ./a.out

Deconstruction A

Deconstruction A

free(): double free detected in tcache 2

[1] 3495877 abort (core dumped) ./a.out

由于用同一个指针(this)构造了两个智能指针sp1sp2, 而他们两个之间是没有任何关系的, 在离开作用域之后 this将会被构造的两个智能指针各自析构, 导致重复析构的错误

正确的做法:

让目标类通过std::enable_shared_from_this类, 然后使用基类的成员函数std::shared_from_this来返回this中的shared_ptr

class A : public std::enable_shared_from_this<A>
{
public:
    std::shared_ptr<A> GetSelf() {
        return shared_from_this();
    }
    ~A() {
        std::cout << "Deconstruction A" << std::endl;
    }
};

调用

int main() 
{
    std::shared_ptr<A> sp1(new A);
    std::shared_ptr<A> sp2 = sp1->GetSelf(); // ok

    std::cout << "sp1.use_count() = " << sp1.use_count()<< std::endl;
    std::cout << "sp2.use_count() = " << sp2.use_count()<< std::endl;
    std::cout << "leave {}" << std::endl;

    return 0;
}

1.2.4 避免循环引用

循环引用会导致内存泄漏

Class A

class A 
{
public:
    std::shared_ptr<B> bptr;
    ~A() {
        std::cout << "A is deleted" << std::endl;
    }
};

Class B

class B 
{
public:
    std::shared_ptr<A> aptr;
    ~B() {
        std::cout << "B is deleted" << std::endl;
    }
};

调用

int main() 
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);

        ap->bptr = bp;
        bp->aptr = ap;
    }

    std::cout << "main level" << std::endl; //循环引用导致ap bp退出了作用域也没有析构
    
    return 0;
}

结果

g++ 1-1-cycle_shared_ptr.cpp -std=c++11 && ./a.out

main level

会发现退出了作用域之后, AB都没有调用析构, 原因是循环引用导致apbp的引用计数为2, 在离开作用域之后, apbp的引用计数减为1, 并不会减为0, 导致两个指针都不会被析构, 产生内存泄漏

解决方法
  1. 把A和B任何一个成员变量改为weak_ptr

  2. 也可以主动释放

bp->aptr.reset();