c++智能指针

一 为什么使用智能指针

#include <iostream>
#include <string>
#include  <exception>

using namespace std;

void memory_leak_demo1() 
{
    string* str = new string("今天又敲了一天代码,太累了,回家休息了!!!");

    cout << *str << endl;
    return;
}

int memory_leak_demo2() 
{
    string* str = new string("这个世界到处是坑,所以异常处理要谨记在心!!!");


    /***********************************************
     * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
     * 的文件中读取某些数据,而文件此时不存在
     ************************************************/
    {
        throw exception("文件不存在");
    }
    cout << *str << endl;
    delete str;
    return 0;
}

int main()
{
    memory_leak_demo1();
    try 
    {
        memory_leak_demo2();
    }
    catch (exception e) 
    {
        cout << "catch exception: " << e.what() << endl;
    }

    return 0;
}

以上两种情况都会出现内存泄漏!

更好的解决方案: 把string 定义为auto 变量,在函数生命周期结束时释放!

void memory_leak_demo1() 
{
    string str("今天又敲了一天代码,太累了,回家休息了!!!");
    cout << str << endl;
    return;

}


int memory_leak_demo2() 
{
    string str("这个世界到处是坑,所以异常处理要谨记在心!!!");


    /***********************************************
     * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
     * 的文件中读取某些数据,而文件此时不存在
     ************************************************/
    {
        throw exception("文件不存在");
    }
    cout << str << endl;
    return 0;
}

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存, 这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!
C++98 提供了 auto_ptr 模板的解决方案
C++11 增加unique_ptr、shared_ptr 和weak_ptr

二 auto_ptr 使用详解 (C++98)

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

用法:

头文件:    #include <memory>
用  法:    auto_ptr<类型> 变量名(new 类型)

例 如:

auto_ptr<string> str(new string("我要成为大牛~ 变得很牛逼!"));
auto_ptr<vector<int>> av(new vector<int>(10));

使用建议:
1.尽可能不要将auto_ptr 变量定义为全局变量或指针
2.除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
3.C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!

#include <iostream>
#include <string>
#include <exception>
#include <memory>

using namespace std;

class Test
{
public:
    Test()
    {
        m_data = 1;
        cout << "Test is construct" << endl;
    }
    ~Test()
    {
        cout << "Test is destruct" << endl;
    }

    int getData() const
    {
        return m_data;
    }
private:
    int m_data;
};

// 建议1:智能指针不要定义为全局变量
//auto_ptr<Test> t(new Test());

int main()
{
    auto_ptr<Test> t(new Test());

    // 建议2:不要定义指向智能指针对象的指针变量
    //auto_ptr<Test>* tp = new auto_ptr<Test>(new Test());

    // 建议3:除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
    //auto_ptr<Test> t1;
    //t1 = t;
    
    //使用智能指针访问对象时,使用方式和普通方式一样
    cout << "data:" << t->getData() << endl;
    cout << "data:" << (*t).getData() << endl;

    // get 获取申请的动态内存的指针
    Test* temp1 = t.get();  
    cout << "data:" << temp1->getData() << endl;
    cout << "data:" << (t.get())->getData() << endl;

    // release 取消智能指针对动态内存的托管,之前分配的内存必须手动释放
    Test* temp2 = t.release();   
    delete temp2;

    // reset 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
    t.reset();
    return 0;
}

三 unique_ptr 使用详解 (C++11)

auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。

3.1 auto_ptr 的弊端

auto_ptr 主要有两大问题:
1.复制和赋值会改变资源的所有权,不符合人的直觉。
2.在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
3.不支持对象数组的操作

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

using namespace std;


int main()
{
    // 弊端1. auto_ptr被c++11抛弃的原因: 复制或赋值都会改变资源的所有权
    cout << "---------test 弊端1---------" << endl;
    auto_ptr<string> pStr1(new string("I am pStr1"));
    auto_ptr<string> pStr2(new string("I am pStr2"));
    cout << "pStr1: 0x" << hex << pStr1.get() << endl;
    cout << "pStr2: 0x" << hex << pStr2.get() << endl;

    // 执行pStr1 = pStr2; 会将pStr2置为nullptr
    pStr1 = pStr2;
    /*
    // 转到定义看源码
    auto_ptr& operator=(auto_ptr& _Right) noexcept {
        reset(_Right.release());
        return *this;
    }
    _Ty* release() noexcept {
        _Ty* _Tmp = _Myptr;
        _Myptr    = nullptr;
        return _Tmp;
    }

    void reset(_Ty* _Ptr = nullptr) noexcept { // destroy designated object and store new pointer
        if (_Ptr != _Myptr) {
            delete _Myptr;
        }

        _Myptr = _Ptr;
    }
    */

    cout << endl << "after pStr1 = pStr2;" << endl;
    cout << "pStr1: 0x" << hex << pStr1.get() << endl;
    cout << "pStr2: 0x" << hex << pStr2.get() << endl;

    // 弊端2:在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
    cout << endl << "---------test 弊端2---------" << endl;
    vector<auto_ptr<string>> v1;
    auto_ptr<string> pStr3(new string("I am pStr3"));
    auto_ptr<string> pStr4(new string("I am pStr4"));

    //v1.push_back(pStr3);  //error C2558: class“std::auto_ptr<std::string>”: 没有可用的复制构造函数或复制构造函数声明为“explicit”
    /* auto_ptr源码
    explicit auto_ptr(_Ty* _Ptr = nullptr) noexcept : _Myptr(_Ptr) {}
    */
    v1.push_back(std::move(pStr3));  // move 把pStr3右值化,这样就可以
    v1.emplace(v1.end(), pStr4);

    cout << "*v1[0]:" << *v1[0] << endl;
    cout << "*v1[1]:" << *v1[1] << endl;

    // 风险来了
    cout << endl << "test v1[0] = v1[1];" << endl;
    v1[0] = v1[1];
    cout << "*v1[0]:" << *v1[0] << endl;
    //cout << "*v1[1]:" << *v1[1] << endl;  // 报错 auto_ptr not dereferenceable

    // 弊端3:不支持对象数组的内存管理
    //auto_ptr<int[]> aparr(new int[5]);   //报错 没有与参数列表匹配的构造函数
    unique_ptr<int[]> uparr(new int[5]);   //unique_ptr支持

    // 4. auto_ptr陷阱,不能把同一段内存交给多个auto_ptr的变量去管理
    {
        string* str = new string("智能指针的内存管理陷阱");
        {
            auto_ptr<string> p1;
            p1.reset(str);  
        } //p1的生命周期结束,会释放str所指向的内存
        
        auto_ptr<string> p2;
        //p2.reset(str);          // 报错 引发了异常: 读取访问权限冲突。
        //cout << *p2 << endl;  // 报错 c0005 访问越界
    }

    return 0;
}

image

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

3.2 基于auto_ptr, unique_ptr的特性

unique_ptr特性:
基于排他所有权模式:两个指针不能指向同一个资源
无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
在容器中保存指针是安全的

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

using namespace std;


int main()
{
    // 弊端1. auto_ptr被c++11抛弃的原因: 复制或赋值都会改变资源的所有权
    // unique_ptr如何解决这个问题? 不允许显示的右值赋值和构造 
    unique_ptr<string> pStr1(new string("I am pStr1"));
    unique_ptr<string> pStr2(new string("I am pStr2"));
    cout << "pStr1: 0x" << hex << pStr1.get() << endl;
    cout << "pStr2: 0x" << hex << pStr2.get() << endl;

    //pStr1 = pStr2;              // 报错,左值赋值禁止
    pStr1 = std::move(pStr2);     // 如果一定要转移,使用move把左值转成右值

    unique_ptr<string> pStr3(new string("I am pStr3"));
    //unique_ptr<string> pStr4(pStr3);             // 报错:左值拷贝构造也不行
    unique_ptr<string> pStr4(std::move(pStr3));    // 必须转成右值

    cout << endl << "after pStr1 = std::move(pStr2);" << endl;
    cout << "pStr1: 0x" << hex << pStr1.get() << endl;
    cout << "pStr2: 0x" << hex << pStr2.get() << endl;
    cout << endl;

    // 弊端2:在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
    vector<unique_ptr<string>> v1;
    unique_ptr<string> pStr5(new string("I am pStr5"));
    unique_ptr<string> pStr6(new string("I am pStr6"));

    v1.push_back(std::move(pStr5));  // move 把pStr3右值化,这样就可以
    v1.emplace_back(std::move(pStr6));

    cout << "*v1[0]:" << *v1[0] << endl;
    cout << "*v1[1]:" << *v1[1] << endl;

    // 风险来了
    //v1[0] = v1[1];            // 报错  unique_ptr不支持直接赋值
    v1[0] = std::move(v1[1]);   // 要显示调用
    cout << "*v1[0]:" << *v1[0] << endl;
    //cout << "*v1[1]:" << *v1[1] << endl;  // 报错 引发了异常: 读取访问权限冲突。this 是 nullptr。

    // 弊端3:不支持对象数组的内存管理, 但是unique_ptr支持
    //auto_ptr<int[]> aparr(new int[5]);   //报错 没有与参数列表匹配的构造函数
    unique_ptr<int[]> uparr(new int[5]);   //unique_ptr支持,自动会调用delete[]去释放

    // 4. auto_ptr陷阱,不能把同一段内存交给多个auto_ptr的变量去管理, unique_ptr同样是排他型的,两个指针不能指向同一个资源
    {
        string* str = new string("智能指针的内存管理陷阱");
        {
            unique_ptr<string> p1;
            p1.reset(str);  
        } //p1的生命周期结束,会释放str所指向的内存
        
        unique_ptr<string> p2;
        //p2.reset(str);          // 报错 引发了异常: 读取访问权限冲突。
        //cout << *p2 << endl;  // 报错 c0005 访问越界
    }

    return 0;
}

image

3.3 unique_ptr的使用

构造函数

unique_ptr<T> up ; //空的unique_ptr,可以指向类型为T的对象
unique_ptr<T> up1(new T()) ;//定义unique_ptr,同时指向类型为T的对象
unique_ptr<T[]> up ; //空的unique_ptr,可以指向类型为T[的数组对象
unique_ptr<T[]> up1(new T[]) ;//定义unique_ptr,同时指向类型为T的数组对象
unique_ptr<T,D> up(); //空的unique_ptr,接受一个D类型的删除器d,使用d释放内存
unique_ptr<T,D> up(new T()); //定义unique_ptr,同时指向类型为T的对象,接受一个D类型的删除器d,使用删除器d来释放内存

示例

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

using namespace std;

class Test
{
public:
    Test() { cout << "This is construct" << endl; }
    ~Test() { cout << "This is destruct" << endl; }
    void print() { cout << " This is print" << endl; }
};

class DestructTest
{
public:
    void operator()(Test* pt)
    {
        pt->print();
        delete pt;
    }
};

int main()
{
    // 普通的unique_ptr变量定义
    unique_ptr<Test> up1;
    unique_ptr<Test> up2(new Test());
    Test* pTest1 = new Test();
    up1.reset(pTest1);
    up1 = std::move(up2);

    // 数组对象的unique_ptr 定义
    unique_ptr<Test[]> up3;
    unique_ptr<Test[]> up4(new Test[5]);
     
    // 使用自定义的删除器
    unique_ptr<Test, DestructTest> up5(new Test());
    return 0;
}

赋值,必须使用移动语义

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(new int(11));
up1 = std::move(up2);//必须使用移动语义,结果,up1 内存释放, up2 交由up1 管理

主动释放对象

up = nullptr ;//释放up指向的对象,将up置为空
或  up = NULL; //作用相同 

放弃对象控制权

up.release();    //放弃对象的控制权,返回指针,将up置为空,不会释放内存

重置

up.reset(…) ; //参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值

交换

up.swap(up1);  //将智能指针up 和up1管控的对象进行交换

四 shared_ptr 使用详解 (C++11)

熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?

如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!

image

构造函数

shared_ptr<T> sp ; //空的shared_ptr,可以指向类型为T的对象
shared_ptr<T> sp1(new T()) ;//定义shared_ptr,同时指向类型为T的对象
shared_ptr<T[]> sp2 ; //空的shared_ptr,可以指向类型为T[的数组对象 C++17后支持
shared_ptr<T[]> sp3(new T[]{...}) ;//指向类型为T的数组对象 C++17后支持
shared_ptr<T> sp4(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D

示例

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

using namespace std;

class Test
{
public:
    Test(int val) 
    { 
        this->value = val;
        cout << "This is construct, value:" << value << endl; 
    }
    ~Test() 
    { 
        cout << "This is destruct, value:" << value << endl;
    }

    int value;
};


class DestructTest
{
public:
    void operator()(Test* pt)
    {
        cout << "DestructTest::operator()" << endl;
        delete pt;
    }
};

int main()
{
    shared_ptr<Test> sp1;
    shared_ptr<Test> sp2(new Test(10));

    sp1 = sp2;
    shared_ptr<Test> sp3(sp1);
    cout << "use_count:" << sp2.use_count() << endl;

    // 数组对象的管理
    shared_ptr<Test[]> sp4(new Test[5]{0,1,2,3,4});

    // 使用删除器释放内存
    shared_ptr<Test> sp5(new Test(5), DestructTest());
    return 0;
}

image

释放内存

shared_ptr<T> sp5(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存

初始化
方式一 构造函数

shared_ptrr<int> up1(new int(10));  //int(10) 的引用计数为1
shared_ptrr<int> up2(up1);  //使用智能指针up1构造up2, 此时int(10) 引用计数为2

方式二 使用make_shared 初始化对象,分配内存效率更高
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:
make_shared<类型>(构造类型对象需要的参数列表);

shared_ptr<int> p4 = make_shared<int>(2); //多个参数以逗号','隔开,最多接受十个
shared_ptr<string> p4 = make_shared<string>("字符串");

示例:

class Test
{
public:
    Test(int val) 
    { 
        this->value = val;
        cout << "This is construct, value:" << value << endl; 
    }
    ~Test() 
    { 
        cout << "This is destruct, value:" << value << endl;
    }

    int value;
};

int main()
{
    shared_ptr<Test> sp6;
    shared_ptr<Test> sp7;
    sp6 = make_shared<Test>(6);
    sp7 = sp6;
    cout << "sp7 use_count:" << sp7.use_count() << endl;
    return 0;
}

赋值

shared_ptrr<int> up1(new int(10));  //int(10) 的引用计数为1
shared_ptr<int> up2(new int(11));   //int(11) 的引用计数为1
up1 = up2;//int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)
的引用计数为2

主动释放对象

shared_ptrr<int> up1(new int(10));
up1 = nullptr ;//int(10) 的引用计数减1,计数归零内存释放 
或up1 = NULL; //作用同上 

重置

up.reset() ;    //将p重置为空指针,所管理对象引用计数 减1
up.reset(p1);   //将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
up.reset(p1,d);  //将p重置为p(的值),p 管控的对象计数减1并使用d作为删除器

交换

std::swap(p1,p2); //交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2);    //同上

使用陷阱
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

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

using namespace std;
class Girl;

class Boy
{
public:
    Boy()
    {
        cout << "Boy construct" << endl;
    }
    ~Boy()
    {
        cout << "Boy destruct" << endl;
    }

    void setGirlFriend(shared_ptr<Girl> &g)
    {
        girlFriend = g;
    }
private:
    shared_ptr<Girl> girlFriend;
};

class Girl
{
public:
    Girl()
    {
        cout << "Girl construct" << endl;
    }
    ~Girl()
    {
        cout << "Girl destruct" << endl;
    }

    void setBoyFriend(shared_ptr<Boy>& b)
    {
        boyFriend = b;
    }

private:
    shared_ptr<Boy> boyFriend;
};

// 使用陷阱
void use_trap()
{
    shared_ptr<Girl> spGirl(new Girl());
    shared_ptr<Boy> spBoy(new Boy());
    spGirl->setBoyFriend(spBoy);
    spBoy->setGirlFriend(spGirl);
}

int main()
{
    use_trap();

    return 0;
}

image

五 weak_ptr 使用详解 (C++11)

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.  同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

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

using namespace std;
class Girl;

class Boy
{
public:
    Boy()
    {
        cout << "Boy construct" << endl;
    }
    ~Boy()
    {
        cout << "Boy destruct" << endl;
    }

    void setGirlFriend(shared_ptr<Girl> &g)
    {
        girlFriend = g;
    }
private:
    shared_ptr<Girl> girlFriend;
};

class Girl
{
public:
    Girl()
    {
        cout << "Girl construct" << endl;
    }
    ~Girl()
    {
        cout << "Girl destruct" << endl;
    }

    void setBoyFriend(shared_ptr<Boy>& b)
    {
        boyFriend = b;
    }

private:
    shared_ptr<Boy> boyFriend;
};

// 使用陷阱
void use_trap()
{
    shared_ptr<Girl> spGirl(new Girl());
    shared_ptr<Boy> spBoy(new Boy());

    shared_ptr<Girl> spGirl2(spGirl);
    shared_ptr<Girl> spGirl3(spGirl);
    shared_ptr<Girl> spGirl4(spGirl);

    // 弱指针的使用
    weak_ptr<Girl> wpGirl1;           // 定义空的弱指针
    weak_ptr<Girl> wpGirl2(spGirl);   // 使用共享指针构造
    wpGirl1 = spGirl;                 // 共享指针赋值给弱指针

    cout << "spGirl use_count:" << spGirl.use_count() << endl;
    cout << "wpGirl use_count:" << wpGirl1.use_count() << endl;

    //弱指针不支持 * 和 -> 对指针的访问,必要的时候可以转成共享指针
    shared_ptr<Girl> spGirl5;
    spGirl5 = wpGirl1.lock();
    cout << "after spGirl5 = wpGirl1.lock();" << endl;
    cout << "spGirl use_count:" << spGirl.use_count() << endl;
    cout << "wpGirl use_count:" << wpGirl1.use_count() << endl;
    spGirl5 = NULL;  // 使用完之后要释放

    spBoy->setGirlFriend(spGirl);
}

int main()
{
    use_trap();

    return 0;
}

image

六 智能指针的使用陷阱

1.不要把一个原生指针给多个智能指针管理

int *x = new int(10);
unique_ptr<int> up1(x);
unique_ptr<int> up2(x);
//警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);

2.记得使用u.release()的返回值
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了

3.禁止delete 智能指针get 函数返回的指针
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

4.禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!

shared_ptr<int> sp1(new int(10));
//一个典型的错误用法 shared_ptr<int> sp4(sp1.get());
posted @ 2022-05-21 14:41  荒年、  阅读(85)  评论(0编辑  收藏  举报