c++ 智能指针

智能指针

C++中的一个常见的错误就是内存管理。大部分情形中,这些bug来自动态分配内存和指针的使用:当多次释放动态分配的内存时,可能会导致内存损坏或者致命的运行时错误;当忘记释放动态分配的内存时,会导致内存泄露。
智能指针可以帮助管理动态分配的内存。其来源于一个事实:栈比堆要安全的多,因为栈上的变量离开作用域后,会自动销毁并清理。智能指针结合了栈上变量的安全性和堆上变量的灵活性。

Base* ptr = new Base; // Base是一个类或者结构
delete ptr;

申请了一份动态内存,使用之后释放了它。但是很容易会在函数结束前释放它,有一些原因会导致内存无法得到释放,比如函数提前终止。

void Function()
{
    Base* ptr = new Base; 
    
    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;
    
    if (x == 0)
        return;  // 函数终止,无法释放ptr
    if (x < 0)
        throw;   // 出现异常,函数终止,无法释放ptr

    delete ptr;
}

由于过早的return语句以及异常的抛出,ptr将得不到正确释放,从而出现内存泄露,原因还是指针并没有一个内在机制来自动管理与释放。
类内部存储指针,然后在析构函数中销毁该指针。类可以实现资源的自动管理。好处是,只要类局部变量(分配在栈上)超出其作用域(不论其是如何离开的),其析构函数一定会被执行,那么管理的内存也将一定得到销毁。

#include <iostream>
using namespace std;
template<typename T>
class Base_ptr
{
public:
    Base_ptr(T* ptr = nullptr):m_ptr{ptr}{
        cout << "Base_ptr init" << endl;
    }

    virtual ~Base_ptr()
    {
        cout << "m_ptr delete" << endl;
        delete m_ptr;
    }

    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }
private:
    T* m_ptr;
};

class Base_class
{
public:
    Base_class() { cout << "Base_class init" << endl; }
    virtual ~Base_class() { cout << "Base_class destoryed!" << endl; }
};
int main()
{
    {
        Base_ptr<Base_class> res(new Base_class);
    }

    return 0;
}

在这里插入图片描述
将动态申请的资源交给一个类变量来保存,由于类变量在局部作用域,其离开后将会自动调用析构函数,然后释放内存。同时,不论其是如何离开作用域的,即使出现异常,析构函数一定会被执行,内存也一定得到释放,因为该类变量是保存在栈上的。

如果是下面两种情况,程序会崩溃

  • 用res1初始化res2,调用的是默认复制构造函数,执行的是浅复制。所以,res2与res1内部保存是同一块内存,当销毁变量时,同一块内存将会被多次释放,程序会奔溃
int main()
{
    {
        Base_ptr<Base_class> res(new Base_class);
        Base_ptr<Base_class> res2(res);
    }
    cout << "finish" << endl;
    return 0;
}
  • res1被浅复制到函数参数res中,函数执行后其内存会被释放,那么在销毁res1时,又会销毁已经释放的内存,程序崩溃
void passByValue(Base_ptr<Base_class> res)
{}

int main()
{
    {
        Base_ptr<Base_class> res(new Base_class);
        passByValue(res);
    }
    cout << "finish" << endl;
    return 0;
}

修改这个类,自己实现复制构造函数,和赋值运算符重载。在这两个函数中,将指针所有权从一个对象转移到另外一个对象,将解决上述问题

template<typename T>
class Base_ptr
{
public:
    Base_ptr(T* ptr = nullptr) :
            m_ptr{ ptr }
    {}

    virtual ~Base_ptr()
    {
        delete m_ptr;
    }

    Base_ptr(Base_ptr& rhs)
    {
        m_ptr = rhs.m_ptr;
        rhs.m_ptr = nullptr;
    }

    Base_ptr& operator=(Base_ptr& rhs)
    {
        if (&rhs == this)
            return *this;

        delete m_ptr;
        m_ptr = rhs.m_ptr;
        rhs.m_ptr = nullptr;
        return *this;
    }

    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
private:
    T* m_ptr;
};
class Base_class
{
public:
    Base_class() { cout << "Base_class init" << endl; }
    virtual ~Base_class() { cout << "Base_class destoryed!" << endl; }
};


int main()
{

    Base_ptr<Base_class> res1(new Base_class);
    Base_ptr<Base_class> res2;

    cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
    cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");

    res2 = res1; // 转移指针所有权

    cout << "Ownership transferred\n";

    cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
    cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");
    return 0;
}

在这里插入图片描述
其实际上实现的是移动语义,对于移动语义来说,其将转移对象所有权,而不是进行赋值。由于在C++11之前,并没有右值引用,所以没有机制实现移动语义。所以C++11之前的智能指针是std::auto_ptr,其实现就类似于Base_ptr类。但是其存在很多问题。首先如果函数中存在std::auto_ptr类型的参数,你使用一个变量进行传值时,资源所有权将会被转移,那么函数结束后资源将被销毁,然后你可能解引用这个变量,但实际上它已经是空指针了,因此程序可能崩溃。其次,std::auto_ptr内部调用的是非数组delete,那么对于动态分配的数组,std::auto_ptr无法正常工作,可能会出现内存泄露。最后,std::auto_ptr对STL不兼容,因为STL的对象在进行复制时,就是进行复制,而不是移动语义。所以实际上,在std::auto_ptr在C++11中已经被弃用了,并且在C++17中被移除标准库。

基于C++11中的右值引用与移动语义修改,实现了移动构造函数与移动赋值操作符的重载,进而实现了移动语义,但是同时禁用了复制构造函数与复制赋值运算符,因此这个类的变量仅可以通过仅可以传递右值,但是不能传递左值。但可以将右值传递给函数的const左值引用参数。当传递右值时,那么明显要转移指针所有权了,那么当前变量将不再有效,C++11中有类似的实现,那就是std::unique_ptr

template<typename T>
class Base_ptr
{
public:
    Base_ptr(T* ptr = nullptr):
            m_ptr{ptr}
    {}

    Base_ptr(const Base_ptr& rhs) = delete;

    Base_ptr(Base_ptr&& rhs) :
            m_ptr{ rhs.m_ptr }
    {
        rhs.m_ptr = nullptr;
    }

    Base_ptr& operator=(const Base_ptr& rhs) = delete;

    Base_ptr& operator=(Base_ptr&& rhs)
    {
        if (this == &rhs)
        {
            return *this;
        }
        std::swap(m_ptr, rhs.m_ptr);
        return *this;

    }

    virtual ~Base_ptr()
    {
        delete m_ptr;
    }

    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }

    bool isNull() const { return m_ptr == nullptr; }
private:
    T* m_ptr;
};

std::unique_ptr

unique_ptr cpp文档
std::unique_ptr是std::auto_ptr的替代品,其用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权。

#include <iostream>
using namespace std;
class Fraction
{
private:
    int m_numerator = 0;
    int m_denominator = 1;

public:
    Fraction(int numerator = 0, int denominator = 1) :
            m_numerator(numerator), m_denominator(denominator)
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << "/" << f1.m_denominator;
        return out;
    }
};

int main()
{

    std::unique_ptr<Fraction> f1{ new Fraction{ 3, 5 } };
    cout << *f1 << endl; // output: 3/5

    std::unique_ptr<Fraction> f2; // 初始化为nullptr

    // f2 = f1 // 非法,不允许左值赋值
    f2 = std::move(f1);  // 此时f1转移到f2,f1变为nullptr

    // C++14 可以使用 make_unique函数
    auto f3 = std::make_unique<Fraction>(2, 7);
    cout << *f3 << endl;  // output: 2/7

    // 处理数组,但是尽量不用这样做,因为你可以用std::array或者std::vector
    auto f4 = std::make_unique<Fraction[]>(4);
    std::cout << f4[0] << endl; // output: 0/1

    return 0;
}

如果编译器支持,尽量使用make_unique函数创建unique_ptr实例,如果不支持,你可以实现简化的版本:

// 注意:无法处理数组
template<typename T, typename ... Ts>
std::unique_ptr<T> make_unique(Ts ... args)
{
    return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }};
}

std::unique_ptr对象可以作为函数返回值使用,因为函数返回值是个右值,复制给其他变量时,通过移动语义来实现。当然,可以将std::unique_ptr对象传递给函数

#include <iostream>
using namespace std;
class Resource
{
public:
    Resource() { cout << "Resource init!" << endl; }
    virtual ~Resource() { cout << "Resource destoryed!" << endl; }

    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource" << endl;
        return out;
    }
};

void useResource(const std::unique_ptr<Resource>& res)
{
    if (res)
    {
        cout << *res;
    }
}

int main()
{

    {
        auto ptr = std::make_unique<Resource>();
        useResource(ptr);
        cout << "Ending" << endl;
    }
    // output
    // Resource init
    // I am a resource
    // Ending
    // Resource destroyed
    return 0;
}

std::unique_ptr对象可以传值给左值常量引用参数,因为这并不会改变内存所有权。也可以右值传值,实现移动语义


#include <iostream>
using namespace std;

class Base_class
{
public:
    Base_class() { cout << "Base_class init!" << endl; }
    virtual ~Base_class() { cout << "Base_class destoryed!" << endl; }

    friend std::ostream& operator<<(std::ostream& out, const Base_class &res)
    {
        out << "I am a Base_class" << endl;
        return out;
    }
};

void takeOwnerShip(std::unique_ptr<Base_class>&& res) // 也可以用 std::unique_ptr<Base_class> res
{
    if (res)
    {
        cout << *res;
    }
}

int main()
{

    {
        auto ptr = std::make_unique<Base_class>();
        // takeOwnerShip(ptr); // 非法
        takeOwnerShip(std::move(ptr)); // 必须传递右值
        cout << "Ending" << endl;
    }
    // output
    // Base_class init
    // I am a Base_class
    // Base_class destroyed
    // Ending
    return 0;
}

可以看到,std::unique_ptr对象可以方便地管理动态内存。但是前提是该对象是建立在栈上的,千万不要使用动态分配的类对象,那么将在堆上,其行为与普通指针变得一样。

使用std::unique_ptr可能犯的两个错误是:

// 千万不要用同一个资源来初始化多个std::unique_ptr对象
Base_class *res = new Base_class;
std::unique_ptr<Base_class> res1(res);
std::unique_ptr<Base_class> res2(res);

// 不要混用普通指针与智能指针
Base_class *res = new Base_class;
std::unique_ptr<Base_class> res1(res);
delete res;

std::unique_ptr默认使用new和delete运算符来分配和释放内存,可以修改这个行为,下面的代码使用malloc()和free()函数管理资源:

// 大部分时候没有理由这样做
auto deleter = [](int* p) { free(p); };
int* p = (int*)malloc(sizeof(int));
*p = 2;
std::unique_ptr<int, decltype(deleter)> mySmartPtr{ p, deleter };
cout << *mySmartPtr << endl; // output: 2

std::unique_ptr常用的方法:

  • release():返回该对象所管理的指针,同时释放其所有权;
  • reset():析构其管理的内存,同时也可以传递进来一个新的指针对象;
  • swap():交换所管理的对象;
  • get():返回对象所管理的指针;
  • get_deleter():返回析构其管理指针的调用函数。

std::shared_ptr

shared_ptr cpp文档
std::shared_ptr与std::unique_ptr类似。要创建std::shared_ptr对象,可以使用make_shared()函数(C++11是支持的,貌似制定这个标准的人忘了make_unique(),所以在C++14追加了)。std::shared_ptr与std::unique_ptr的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。还有一个区别是std::shared_ptr不能用于管理C语言风格的动态数组,这点要注意

#include <iostream>
using namespace std;

class Base_class
{
public:
    Base_class() { cout << "Base_class init!" << endl; }
    virtual ~Base_class() { cout << "Base_class destoryed!" << endl; }

    friend std::ostream& operator<<(std::ostream& out, const Base_class &res)
    {
        out << "I am a Base_class" << endl;
        return out;
    }
};


int main()
{
    auto ptr1 = std::make_shared<Base_class>();
    cout << ptr1.use_count() << endl;  // output: 1
    {
        auto ptr2 = ptr1;  // 通过复制构造函数使两个对象管理同一块内存
        std::shared_ptr<Base_class> ptr3;   // 初始化为空
        ptr3 = ptr1;   // 通过赋值,共享内存
        cout << ptr1.use_count() << endl;  // output: 3
        cout << ptr2.use_count() << endl;  // output: 3
        cout << ptr3.use_count() << endl;  // output: 3
    }
    // 此时ptr2与ptr3对象析构了
    cout << ptr1.use_count() << endl;  // output: 1

    return 0;
}

通过复制构造函数或者赋值来共享内存,这一点很重要

#include <iostream>
using namespace std;

class Base_class
{
public:
    Base_class() { cout << "Base_class init!" << endl; }
    virtual ~Base_class() { cout << "Base_class destoryed!" << endl; }

    friend std::ostream& operator<<(std::ostream& out, const Base_class &res)
    {
        out << "I am a Base_class" << endl;
        return out;
    }
};

int main()
{
    Base_class* res = new Base_class;
    std::shared_ptr<Base_class> ptr1{ res };
    cout << ptr1.use_count() << endl;  // output: 1
    {

        std::shared_ptr<Base_class> ptr2{ res };   // 用同一块内存初始化

        cout << ptr1.use_count() << endl;  // output: 1
        cout << ptr2.use_count() << endl;  // output: 1

    }
    // 此时ptr2对象析构了, output:Base_class destroyed
    cout << ptr1.use_count() << endl;  // output: 1

    return 0;
}

ptr1与ptr2虽然是用同一块内存初始化,但是这个共享却并不被两个对象所知道。这是由于两个对象是独立初始化的,它们互相之间没有通信。当然上面的程序会最终崩溃,因为同一块内存会被析构两次。所以,还是使用复制构造函数还有赋值运算来使不同对象管理同一块内存。如果深挖的话,std::shared_ptr与std::unique_ptr内部实现机理有区别,前者内部使用两个指针,一个指针用于管理实际的指针,另外一个指针指向一个”控制块“,其中记录了哪些对象共同管理同一个指针。这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。所以,上面的问题就出现了。但是如果是使用复制构造函数还有赋值运算时,“控制块”会同步更新的,这样就达到了引用计数的目的。使用std::make_shared就不会出现上面的问题,所以推荐使用。

std::weak_ptr

weak_ptr cpp文档
std::shared_ptr可以实现多个对象共享同一块内存,当最后一个对象离开其作用域时,这块内存被释放。但是仍然有可能出现内存无法被释放的情况,联想一下“死锁”现象,对于std::shared_ptr会出现类似的“循环引用”现象

创建两个Person动态对象,交由智能指针管理,并且通过partnerUp()函数互相引用为自己的伙伴。


#include <iostream>
using namespace std;

class Person
{
public:
    Person(const string& name):
            m_name{name}
    {
        cout << m_name << " created" << endl;
    }

    virtual ~Person()
    {
        cout << m_name << " destoryed" << endl;
    }

    friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
    {
        if (!p1 || !p2)
        {
            return false;
        }

        p1->m_partner = p2;
        p2->m_partner = p1;

        cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
        return true;
    }

private:
    string m_name;
    std::shared_ptr<Person> m_partner;
};

int main()
{
    {
        auto p1 = std::make_shared<Person>("Lucy");
        auto p2 = std::make_shared<Person>("Ricky");
        partnerUp(p1, p2);  // 互相设为伙伴
    }

    return 0;
}

执行结果有问题,对象没有被析构!出现内存泄露!std::shared_ptr对象是什么时候才能被析构,就是引用计数变为0时,但是当析构p1时,p2内部却引用了p1,无法析构;反过来也无法析构。互相引用造成了“死锁”,最终内存泄露!这样的情形也会出现在“自锁”中:

int main()
{
    {
        auto p1 = std::make_shared<Person>("Lucy");
        partnerUp(p1, p1);  // 自己作为自己的伙伴
    }
    
    return 0;
}

std::weak_ptr可以包含由std::shared_ptr所管理的内存的引用。但是它仅仅是旁观者,并不是所有者。那就是std::weak_ptr不拥有这块内存,当然不会计数,也不会阻止std::shared_ptr释放其内存。但是它可以通过lock()方法返回一个std::shared_ptr对象,从而访问这块内存。这样可以用std::weak_ptr来解决上面的“循环引用”问题

#include <iostream>
using namespace std;

class Person
{
public:
    Person(const string& name):
            m_name{name}
    {
        cout << m_name << " created" << endl;
    }

    virtual ~Person()
    {
        cout << m_name << " destoryed" << endl;
    }

    friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
    {
        if (!p1 || !p2)
        {
            return false;
        }

        p1->m_partner = p2;  // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
        p2->m_partner = p1;

        cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
        return true;
    }

private:
    string m_name;
    std::weak_ptr<Person> m_partner;
};

int main()
{
    {
        auto p1 = std::make_shared<Person>("Lucy");
        auto p2 = std::make_shared<Person>("Ricky");
        partnerUp(p1, p2);  // 互相设为伙伴
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值