智能指针

参考
http://t.csdn.cn/guyQ2
https://zhuanlan.zhihu.com/p/151744661
https://mp.weixin.qq.com/s/n8O0Xn8hqpa5On-h8Zlcbg

为什么有智能指针

C++使用内存的时候很容易出现野指针、悬空指针、内存泄露的问题。所以C++11引入了智能指针来管理内存。智能指针保证能做到资源的自动释放,利用栈上的对象出作用域自动析构的特征,来做到资源的自动释放。

智能指针是什么

智能指针是一个RAII类模板,用于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作用域时调用析构函数,使用delete删除指针所执行的内存空间。

智能指针有四种:

  • auto_ptr:已经不用了
  • unique_ptr:独占式指针,同一时刻只能有一个指针指向同一个对象
  • shared_ptr:共享式指针,同一时刻可以有多个指针指向同一个对象
  • weak_ptr:用来解决shared_ptr相互引用导致的死锁问题

使用智能指针

auto_ptr

我用vs测试,c++17标准已经用不了了。auto_ptr 的工作原理是让最后一个智能指针持有资源,原来的 auto_ptr 都被赋 nullptr。

访问空指针,程序崩溃。

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

int main()
{
    vector<auto_ptr<int>> vec;
    vec.push_back(auto_ptr<int>(new int(10)));
    vec.push_back(auto_ptr<int>(new int(20)));
    vec.push_back(auto_ptr<int>(new int(30)));
    // 这里可以打印出10
    cout << *vec[0] << endl;
    vector<auto_ptr<int>> vec2 = vec;
    /* 这里由于上面做了vector容器的拷贝,相当于容器中
    的每一个元素都进行了拷贝构造,原来vec中的智能指针
    全部为nullptr了,再次访问就成访问空指针了,程序崩溃
    */
    cout << *vec[0] << endl;
    return 0;
}

unique_ptr

去掉了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生。但是unique_ptr提供了带右值引用参数的拷贝构造和赋值。

unique_ptr<int> ptr(new int(5));
unique_ptr<int> ptr2 = move(ptr);  // 没有move的话会报错

shared_ptr

强智能指针,weak_ptr 被称为弱智能指针。

shared_ptr<int> ptr(new int(5));
shared_ptr<int> ptr2 = ptr;
cout << ptr.use_count() << endl;  // 2

不过强智能指针可能出现交叉引用的问题。

#include <iostream>
#include <memory>
using namespace std;

class B; // 前置声明类B
class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    shared_ptr<B> _ptrb; // 指向B对象的智能指针
};
class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
    shared_ptr<A> _ptra; // 指向A对象的智能指针
};
int main()
{
    shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
    shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
    ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2

    ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2

    cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
    cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2

    /*
    出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
    B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是
    A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,
    导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”
    */
    return 0;
}

weak_ptr

上面的代码智能指针无法正常地析构。

有一个应用规则:定义对象时,使用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。

weak_ptr 和 shared_ptr的区别:

  1. weak_ptr不会改变资源的引用计数,只是一个观察者,通过观察shared_ptr来判定资源是否存在
  2. weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  3. weak_ptr没有提供常用的指针操作,无法直接访问资源,需要通过lock方法提升为shared_ptr强智能指针,才能访问资源。

修改下上面的代码

#include <iostream>
#include <memory>
using namespace std;

class B; // 前置声明类B
class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
    weak_ptr<A> _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
    // 定义对象时,用强智能指针
    shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
    shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
    // A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
    ptra->_ptrb = ptrb;
    // B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
    ptrb->_ptra = ptra;

    cout << ptra.use_count() << endl; // 打印结果:1
    cout << ptrb.use_count() << endl; // 打印结果:1
    /*
    出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
    B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
    被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
    */
    return 0;
}

多线程访问共享对象

先上一个反例,直接用裸指针,会出现指针悬空。

#include <iostream>
#include <thread>
using namespace std;

class Test
{
public:
    // 构造Test对象,_ptr指向一块int堆内存,初始值是20
    Test() :_ptr(new int(20)) 
    {
        cout << "Test()" << endl;
    }
    // 析构Test对象,释放_ptr指向的堆内存
    ~Test()
    {
        delete _ptr;
        _ptr = nullptr;
        cout << "~Test()" << endl;
    }
    // 该show会在另外一个线程中被执行
    void show()
    {
        cout << *_ptr << endl;
    }
private:
    int *volatile _ptr;
};
void threadProc(Test *p)
{
    // 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
    std::this_thread::sleep_for(std::chrono::seconds(2));
    /* 
    此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
    此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
    存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
    */
    p->show();
}
int main()
{
    // 在堆上定义共享对象
    Test *p = new Test();
    // 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
    std::thread t1(threadProc, p);
    // 在main线程中析构Test共享对象
    delete p;
    // 等待子线程运行结束
    t1.join();
    return 0;
}

使用shared_ptr

#include <iostream>
#include <thread>
#include <memory>
using namespace std;

class Test
{
public:
    // 构造Test对象,_ptr指向一块int堆内存,初始值是20
    Test() :_ptr(new int(20))
    {
        cout << "Test()" << endl;
    }
    // 析构Test对象,释放_ptr指向的堆内存
    ~Test()
    {
        delete _ptr;
        _ptr = nullptr;
        cout << "~Test()" << endl;
    }
    // 该show会在另外一个线程中被执行
    void show()
    {
        cout << *_ptr << endl;
    }
private:
    int* volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
    // 睡眠两秒
    std::this_thread::sleep_for(std::chrono::seconds(2));
    /*
    如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
    为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
    的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
    已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
    */
    shared_ptr<Test> ps = pw.lock();
    if (ps != nullptr)
    {
        ps->show();
    }
}
int main()
{
    // 在堆上定义共享对象
    shared_ptr<Test> p(new Test);
    // 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
    std::thread t1(threadProc, weak_ptr<Test>(p));
    // 在main线程中析构Test共享对象
    // 等待子线程运行结束
    t1.join();
    return 0;
}

使用类型转换

dynamic_cast

只能够用在指向类的指针或者引用上。目的是确保目标指针类型所指向的是一个有效且完整的对象。

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived : public Base { int a; };

int main() {
    try {
        Base* pba = new Derived;
        Base* pbb = new Base;
        Derived* pd;

        pd = dynamic_cast<Derived*>(pba);
        if (pd == 0) cout << "Null pointer on first type-cast.\n";

        pd = dynamic_cast<Derived*>(pbb);
        if (pd == 0) cout << "Null pointer on second type-cast.\n";

        Base* pd2 = dynamic_cast<Base*>(pba);
        if (pd2 == 0) cout << "Null pointer on third type-cast.\n";
    }
    catch (exception& e) { cout << "Exception: " << e.what(); }
    return 0;
}

输出

Null pointer on second type-cast.

static_cast

static_cast能够完成指向相关类的指针上的转换,并不会有运行时的检查来确保转换到目标类型上的指针指向的对象有效且完整。这就需要程序员来保证转换的安全性,不会带来额外检查的开销。

上面的错误信息就可以改成这样,允许这样写

pd = static_cast<Derived*>(pbb);
if (pd == 0) cout << "Null pointer on second type-cast.\n";

reinterpret_cast

不会有内容检查,是一份完全相同的二进制复制品。

const_cast

const_cast可以用来设置或者移出指针所指向对象的const。

#include <iostream>
using namespace std;

void MyPrintf(char* str)
{
    cout << str << endl;
}

int main() {
    const char* s = "hello world";
    MyPrintf(const_cast<char*>(s));
    return 0;
}

手写shared_ptr

shared_ptr底层是一个指针、一个计数器。

先实现计数类

// 计数类
class SharedCount {
public:
    SharedCount() : count_(1) {}
    void add() { ++count_; }
    void minus() { --count_; }
    int get() const { return count_; }
private:
    std::atomic<int> count_;
};

shared_ptr实现也还好,核心思想是构造就计数器+1;析构就计数器-1,当计数器为0时释放指针。还需要注意的是发生拷贝构造和拷贝赋值时引用计数要+1,移动语义不需要改变计数器。

// SharedPtr 类
template<typename T>
class SharedPtr {
public:
    // 含参构造函数
    SharedPtr(T *ptr) : ptr_(ptr), ref_count_(new SharedCount)
    {
        std::cout << "SharedPtr(T *ptr)" << std::endl;
    }
    // 默认构造函数
    SharedPtr() : ptr_(nullptr), ref_count_(new SharedCount)
    {
        std::cout << "SharedPtr()" << std::endl;
    }
    // 析构函数
    ~SharedPtr()
    {
        std::cout << "~SharedPtr()" << std::endl;
        clean();
    }
    // 拷贝构造函数
    SharedPtr(const SharedPtr& p)
    {
        std::cout << "SharedPtr(const SharedPtr& p)" << std::endl;
        this->ptr_ = p.ptr_;
        this->ref_count_ = p.ref_count_;
        ref_count_->add();   // 多个类对象指向同一块地址, 所以只用加一次
    }
    // 拷贝赋值函数
    SharedPtr& operator=(const SharedPtr& p)
    {
        std::cout << "SharedPtr& operator=(const SharedPtr& p)" << std::endl;
        clean(); // 默认计数1, 先把自己清空
        this->ptr_ = p.ptr_;
        this->ref_count_ = p.ref_count_;
        ref_count_->add();
        return *this;
    }
    // 移动语义
    SharedPtr(SharedPtr&& p)
    {
        std::cout << "SharedPtr(SharedPtr&& p)" << std::endl;
        this->ptr_ = p.ptr;
        this->ref_count_ = p.ref_count_;
        p.ptr_ = nullptr;
        p.ref_count_ = nullptr;
    }   
    // 移动赋值函数
    SharedPtr& operator=(SharedPtr&& p)
    {
        std::cout << "SharedPtr& operator=(SharedPtr&& p)" << std::endl;
        clean();
        this->ptr_ = p.ptr_;
        this->ref_count_ = p.ref_count_;
        p.ptr_ = nullptr;
        p.ref_count_ = nullptr;
        return *this;
    }
    // 获取指针的个数
    int use_count() {return ref_count_->get();}
    // 返回指针
    T* get() const {return ptr_;}
    // 重载 ->
    T* operator->() const {return ptr_;}

    T& operator*() const {return *ptr_;}

    operator bool() const {return ptr_;}

private:
    // 指针
    T* ptr_;
    // 计数器, 是指针
    SharedCount* ref_count_;    
    // 析构的清理函数  如果引用数变为了0,delete指针
    void clean()
    {
        if (ref_count_)
        {
            ref_count_->minus();
            if (ref_count_->get() == 0)
            {
                if(ptr_) delete ptr_;
                delete ref_count_;
            }
        }
    }
};

在基类无虚构函数时,shared_ptr可以析构基类,但是unique_ptr不行

#include <iostream>
#include <memory>

using namespace std;

class Human
{
public:
    Human(){}
    virtual ~Human(){
        cout << "Human destroyed" << endl;
    }
    virtual void print()
    {
        cout << "Human print" << endl;
    }
};

class Man : public Human
{
public:
    Man() {}
    ~Man() {
        cout << "Man destoryed" << endl;
    }
    void print()
    {
        cout << "Man print" << endl;
    }
};

int main()
{
    {
//      Human *p = new Man;
//      p->print();
//      delete p;
    }

    {
//      cout << "-------shared_ptr--------" << endl;
//      shared_ptr<Human>p = make_shared<Man>();
//      p->print();
    }

    {
        cout << "----unique_ptr----" << endl;
        unique_ptr<Human> p = make_unique<Man>();
        p->print();
    }

    return 0;
}

file

file

评论

  1. 备战秋招
    1 年前
    2023-8-11 15:31:25

    刷面经的时候忘了,过来学习学习

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇