C++ 的智能指针

专属所有权:unique_ptr

我们大多数场景下用到的应该都是 unique_ptr。

// C++11
unique_ptr<string> p1;
p1.reste(new string("hello world"));
// C++11
unique_ptr<string> p1(new string("hello world"));
// C++14 
auto w = make_unique<Wight>();  

unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有,所以,unique_ptr 不支持复制和赋值,如下:

auto w = std::make_unique<Wight>();
auto w2 = w;  // 编译错误

unique_ptr 在默认情况下和裸指针的大小是一样的。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可自定义删除器可指定其他操作)。

在unique_ptr生命周期内,可以改变unique_ptr所指对象:
1、通过reset方法重新指定、
2、通过release方法释放所有权、
3、通过移动语义转移所有权。

u = nullptr;      // 释放u指向的对象,并将u置为空
u.release()       // u放弃对指针的控制权,返回指针,并将u置为空
unique_ptr<string> p2(p1.release())    // 将p1置为空,并将资源所有权转移给p2

u.reset()         // 释放u指向的对象
u.reset(q)        // 令u释放原管理的对象,重新管理q资源
p2.reset(p1.release()) 

unique_ptr<string> p2 = std::move(p1);

共享所有权:shared_ptr

代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存,shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。

shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化

shared_ptr p(new int(11));

也可以使用make_shared函数初始化。

auto w = make_shared<Wight>();  

不能将指针直接赋值给一个shared_ptr指针,一个是类,一个是指针。

std::shared_ptr<int> p4 = new int(1)  // 错误写法
  • get函数获取原始指针,注意不要使用get初始化另一个智能指针或为智能指针赋值

    shared_ptr<int> p(new int(42));
    
    int *q = p.get();
    shared_ptr<int> p(p.get())    // 错误 ,原因同下
    shared_ptr<int> p2(q)         // 错误, 原因同下
    
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。

    int *p = new int(10);
      
    shared_ptr<int> shp(p);
    shared_ptr<int> shp2(p);      // 两次释放同一内存
    
  • 注意不要混合使用普通指针和智能指针

    int *x (new int(42));
    process(shared_ptr<int>(x));    // 临时变量,内存会被释放掉
    int j = *x;
    
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用稍后介绍。


weak_ptr的使用

  • weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

  • weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。

    shared_ptr<int> sp = make_shared<int>(10);
    
    wak_ptr<int> w(sp);
    w = sp;
    
  • weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

    if (shared_ptr<int> np = wp.lock()) {
        ......
    }
    
循环引用

TODO


智能指针与多线程

有两个指针p1 和 p2,都指向同一个堆上的对象object,假设线程A通过p1指针将对象销毁了,那p2就成了空悬指针,这时一种典型的C/C++内存错误。

image.png

下面的例子:展现了这种内存错误。

struct client
{
public:
    client() : m_name(new string("Bob")) { }
    ~client() {  delete m_name;  cout << "~client()" << endl; }
    string *m_name;
};

class client_manager
{
public:
    void and_client(int client_id)
    {
        lock_guard<mutex> lk(m_lock);
        m_client_map.insert({client_id, new client()});
    }

    void remove_client(int client_id)
    {
        lock_guard<mutex> lk(m_lock);
        auto it = m_client_map.find(client_id);
        if (it != m_client_map.end())
        {
            m_client_map.erase(it);
            delete it->second;  // 销毁
        }
    }

    bool find_client(int client_id, client *&ptr)
    {
        bool result = false;
        lock_guard<mutex> lk(m_lock);
        auto it = m_client_map.find(client_id);
        if (it != m_client_map.end())
        {
            result = true;
            ptr = it->second;
        }

        return result;
    }

private:
    mutex m_lock;
    map<int, client *> m_client_map;
};

int main()
{
    client_manager manager;
    int id = 1;

    thread t1([&manager, &id]() {
        manager.and_client(id++);
    });
    t1.join();

    thread t2([&manager]() {
        this_thread::sleep_for(chrono::seconds(2));  // 模拟OS调度
        manager.remove_client(1);
    });

    thread t3([&manager]() {
        client *p1 = NULL;
        if (manager.find_client(1, p1))
        {
            this_thread::sleep_for(chrono::seconds(4));  // 模拟OS调度
            (*p1->m_name) = "world";  // 这里会段错误,因为p1 所指的对象在t2线程已经销毁了。
        }
    });

    t2.join();
    t3.join();
}

所以要想安全的销毁对象,最好在别人(线程)都看不到的情况下,偷偷的做,这也正是GC的原理,也符合shared_ptr 的 用法。

利用 shared_ptr 改造上面的错误。

class client_manager
{
public:
    void and_client(int client_id)
    {
        lock_guard<mutex> lk(m_lock);
        shared_ptr<client> p_client(new client());
        m_client_map.insert({client_id, p_client});
    }

    void remove_client(int client_id)
    {
        lock_guard<mutex> lk(m_lock);
        auto it = m_client_map.find(client_id);
        if (it != m_client_map.end())
        {
            m_client_map.erase(it);
        }
    }

    bool find_client(int client_id, shared_ptr<client> &ptr)
    {
        bool result = false;
        lock_guard<mutex> lk(m_lock);
        auto it = m_client_map.find(client_id);
        if (it != m_client_map.end())
        {
            result = true;
            ptr = it->second;
        }

        return result;
    }

private:
    mutex m_lock;
    map<int, shared_ptr<client>> m_client_map;
};

int main()
{
    client_manager manager;
    int id = 1;

    thread t1([&manager, &id]() {
        manager.and_client(id++);
    });
    t1.join();

    thread t2([&manager]() {
        this_thread::sleep_for(chrono::seconds(2)); // 模拟OS调度
        manager.remove_client(1);
    });

    thread t3([&manager]() {
        shared_ptr<client> p1;
        if (manager.find_client(1, p1))
        {
            cout << p1.use_count() << endl;
            this_thread::sleep_for(chrono::seconds(4)); // 模拟OS调度
            cout << p1.use_count() << endl;
            (*p1->m_name) = "world";
        }
        else
        {
            cout << "not found" << endl;
        }
    });

    t2.join();
    t3.join();
}

实现

TODO


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容