From Java to C++ 第五篇之智能指针

前叙

From Java to C++ 第一篇

From Java to C++ 第二篇

From Java to C++ 第三篇

From Java to C++ 第四篇之内存管理篇

回顾

前面我们了解到RALL的基本用法,可以在方法执行完以后,主动将堆内存对象释放掉,从而简化了内存管理,解决内存泄漏的可能,这次我们学习下RALL,如果做一个完善的智能指针。

智能指针的本质

它的出现其实是为了解决由于动态内存分配而导致的一些内存问题,比如内存泄漏、生命周期管理、悬挂指针或空指针的问题。智能指针通过RALL管理对象的生命周期,提供少量异常类似普通指针的操作接口,在对象构造时候分配内存,在对象作用域之外释放掉内存,帮助我们管理动态内存。相信你跟我一样,看完这句话其实也不太明白,那大概率是对指针的概念不理解,我们来补充点指针的知识吧

到底指针是什么

要是上来就给指针下个定义,估计也没人看得懂,我们直接来看图。

先来看一个变量的内存模型

变量内存模型.png

上面一张图就表明了,一个变量p,它的内存模型就是上面那样,p就是0x11234564地址所对应的存储单元中的数据,这里一个框代表一个字节,由于int是四个字节,所以你看到的才是四个框组成的一个单元。这里需要说明的是c标准中并没规定哪个数据类型占多少字节,这个跟具体机器和编译器有关。你也可以看到,数值其实是按16进制存储的,0x14转10进制后就是20。
你是不是也发现了,其实每个字节都有一个自己的内存地址。对的,这就是跟我接下来讲的指针有关。请往下看。

简单指针

int p = 20;
int * a;
a = &p; //& 符号可以取得p的内存地址

我们来分析上面这段内存模型,你就明白指针是个什么了

指针变量模型.png

其实指针就是一个内存地址,而指针变量_ a 在内存中保存了指针指向的内存地址,那为什么我们不用a来做下面的赋值操作呢?这就跟号的用法有关,你可以简单这么理解,_ 在=号前面的时候代码指针变量,而在=号后面使用的时候,其实是在去指针对应的值。所以接着往下学习,仔细看指针变量a,它的四个字节合起来的值是 0x11234564,正好是变量p的内存地址,而指针a自己的内存地址是:0x11234568,所以你现在是不是明白了一个简单的指针是什么?它跟普通变量唯一的区别就是,它的值存储的是一个内存地址,如果没赋值的话,可能是0x00000000,也可能是其他随机数字。

空指针&野指针

我们再了解什么是空指针和野指针,所谓的空指针是指不指向任何东西的指针,需要注意的是,当我们定义一个指针变量时,如果没有赋值,那么它指针变量存储是一个随机值,如果这个随机值指向了内存中的代码区域,那么就很危险,所以这个时候一定不能写入操作,很有可能对实际的数据产生污染。建议对一个指针变量赋值为NULL。

int * p = NULL;

野指针是地址已经失效的指针,具体说就是当一个指针指向堆内存中的值时,如果这个堆内存被delete后,你还继续操作这个指针,那么就很危险了,所以不建议你这么操作。
好了言归正传,我们来继续研究智能指针。

为什么使用智能指针

在我们简单了解了内存管理和指针后,就很容易理解下面这三点

  • 智能指针能够帮助我们处理资源泄露问题
  • 它也能够帮我们处理空悬指针(野指针)的问题
  • 它还能够帮我们处理比较隐晦的由异常造成的资源泄露

常见的智能指针

智能指针在C11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。
其实还有auto_ptr,这个在C11中已经被抛弃,我们就不学习过时的技术了。
如果是讲unique_ptr,shared_ptr的用法,其实有大量的文章讲解,完全没必要继续看下去对吧,我们这次接着上篇的代码,来改造一下,实现一个通用的完整的智能指针。先来看下上次的代码


class TestRALL {
public:
    TestRALL() {
        std::cout << "TestRALL done" << std::endl;
    };

    ~TestRALL() {
        std::cout << "~TestRALL done" << std::endl;
    };

    void print() {
        std::cout << 1 << std::endl;
    }
};

TestRALL *createTest() {
    return new TestRALL();
}


class TRDelete {
public:
    explicit TRDelete(TestRALL *tr = nullptr) : tr_(tr) {}

    ~TRDelete() {
        delete tr_;
    }

    TestRALL *get() const { return tr_; }

private:
    TestRALL *tr_;
};

void print() {
    TRDelete trDelete(createTest());
    trDelete.get()->print();
}

int main() {
    print();
    return 0;
}

我们发现,TRDelete只适用于TestRALL这一个类,在Java中,我可以通过泛型来解决通用性的问题,那么C++中有吗?肯定有,哈哈,那我们来改造下,代码如下

#include <iostream>


class TestRALL {
public:
    TestRALL() {
        std::cout << "TestRALL done" << std::endl;
    };

    ~TestRALL() {
        std::cout << "~TestRALL done" << std::endl;
    };

    void print() {
        std::cout << 1 << std::endl;
    }
};

TestRALL *createTest() {
    return new TestRALL();
}

template <typename T>
class HeapDel {
public:
    explicit HeapDel(T *tr = nullptr) : tr_(tr) {}

    ~HeapDel() {
        delete tr_;
    }

    T *get() const { return tr_; }

private:
    T *tr_;
};

void print() {
    HeapDel<TestRALL> heapDel(createTest());
    heapDel.get()->print();
}

int main() {
    print();
    return 0;
}

为了通用性和编码规范,我们重新取名HeapDel,意思是释放堆内存,用template 来定义个模板,它的标准格式如下

template <class identifier> function_declaration;
template <typename identifier> function_declaration;

示例

template <typename T> void swap(T& t1, T& t2);

感觉跟Java的泛型很像,有机会我们再深入讨论。我们改造后的代码运行结果如下:

TestRALL done
1
~TestRALL done

同样达到了预期。但是这样就是完整的智能指针了吗,肯定不是哈。再来优化一下

void print() {
    HeapDel<TestRALL> heapDel(createTest());
//    heapDel.get()->print();
    heapDel->print();
}

如果我想直接用heapDel来调用TestRALL的print方法可以吗?肯定是可以的,要不然每次都get岂不是很累。

template<typename T>
class HeapDel {
public:
    explicit HeapDel(T *tr = nullptr) : tr_(tr) {}
    T *operator->() const { return tr_; } //加入该行代码即可
    ~HeapDel() {
        delete tr_;
    }

    T *get() const { return tr_; }

private:
    T *tr_;
};

这里用 operator 重载标识,重载 -> 返回 tr_ 指针变量。具体详细用法请自己查阅哈,目前我对他也不是很理解,后续再学习中了解它。上面我们是想了通过->来方位tr_的成员,那*tr_获取指针的值,该怎么做呢?

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

注意:
// & 取内存地址,* 取指针的值,因为指针的值就是指向的地址,所以赋值给&修饰的变量,其实就是地址与地址的赋值,没什么问题。
再加上面一行,你就可以实现 *heapDel 其实就是 *tr_ 一样的作用。
接下来我们看一个问题,如果我这样操作

void print() {
    HeapDel<TestRALL> heapDel(createTest());
    heapDel->print();
    HeapDel<TestRALL> heapDel2(heapDel);
    heapDel2->print();
}

运行后是这样的

untitled1(3107,0x102f4ae00) malloc: *** error for object 0x7fb06ac05a00: pointer being freed was not allocated
untitled1(3107,0x102f4ae00) malloc: *** set a breakpoint in malloc_error_break to debug
TestRALL done
1
1
~TestRALL done
~TestRALL done

~TestRALL析构函数被执行两次,意味着你释放了两次,着肯定是不允许的,程序已经崩溃报错了,那我们如何避免这个问题呢?

template<typename T>
class HeapDel {
public:
    HeapDel(const HeapDel &) = delete;
    HeapDel &operator=(const HeapDel &) = delete;
    explicit HeapDel(T *tr = nullptr) : tr_(tr) {}
    T *operator->() const { return tr_; }
    T &operator*() const { return *tr_; }
    ~HeapDel() {
        delete tr_;
    }
    T *get() const { return tr_; }
private:
    T *tr_;
};

我们把HeapDel的构造函数给禁掉了,这个时候你再次调用 heapDel2(heapDel) 的时候已经不允许了,会提示如下:

Call to deleted constructor of 'HeapDel<TestRALL>'

但这样做真的合理吗?如果我真需要用一个新的智能指针来获取这个所有权,怎么办呢?

template<typename T>
class HeapDel {
public:
    HeapDel(HeapDel &other) {
        //给当前对象的指针赋值
        tr_ = other.release();
    }

    HeapDel &operator=(HeapDel &hd) {
        HeapDel(hd).swap(*this);
        return *this;
    }

    explicit HeapDel(T *tr = nullptr) : tr_(tr) {}

    T *operator->() const { return tr_; }

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

    ~HeapDel() {
        delete tr_;
    }

    /**
     * 创建新的指针变量返回
     * 将老的指针变量赋值空指针
     * @return
     */
    T *release() {
        T *tr = tr_;
        tr_ = nullptr;
        return tr;
    }

    void swap(HeapDel &hd) {
        using std::swap;
        //用std 的swap,来交换tr_
        swap(tr_, hd.tr_);
    }

    T *get() const { return tr_; }

private:
    T *tr_;
};

新增了俩函数都是为了交换 tr_ 的所有权,再运行刚的代码,你会发现正常了

void print() {
    HeapDel<TestRALL> heapDel(createTest());
    heapDel->print();
    HeapDel<TestRALL> heapDel2(heapDel);
    heapDel2->print();
}
//运行结果如下:

TestRALL done
1
1
~TestRALL done

到目前为止你就实现了被 C++ 11 抛弃的版本 auto_ptr 它的核心逻辑。为什么会抛弃呢?你也看到了在swap中,其实是用HeapDel(hd).swap(*this); 这相当于构造了一个临时对象,再调用swap,如果再赋值过程中发生了异常,this对象可能会部分破坏,就不是一个完整的状态了。而且它最大的问题在于,如果用了新的HeapDel,那之前的HeapDel就不再拥有tr_。下面来看下unique_ptr 智能指针是如何解决上面问题的

template<typename T>
class HeapDel {
public:
    HeapDel(HeapDel &&other)  noexcept {
        //给当前对象的指针赋值
        tr_ = other.release();
    }

    HeapDel &operator=(HeapDel hd) {
        hd.swap(*this);
        return *this;
    }

    explicit HeapDel(T *tr = nullptr) : tr_(tr) {}

    T *operator->() const { return tr_; }

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

    ~HeapDel() {
        delete tr_;
    }

    /**
     * 创建新的指针变量返回
     * 将老的指针变量赋值空指针
     * @return
     */
    T *release() {
        T *tr = tr_;
        tr_ = nullptr;
        return tr;
    }

    void swap(HeapDel &hd) {
        using std::swap;
        //用std 的swap,来交换tr_
        swap(tr_, hd.tr_);
    }

    T *get() const { return tr_; }

private:
    T *tr_;
};

我们将HeapDel重载的构造函数,参数other,改为了&&修饰,原本的实现其实是叫拷贝构造函数,在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数,也就是HeapDel(HeapDel &other) 的写法。现在用两个&&,则变成了移动构造函数,你是不是又多了一个问号,它是干嘛用的呢? 它的来源其实就是由于拷贝构造函数在做对象初始化过程中,底层是进行了两次深拷贝,如果申请的堆空间较小也无伤大雅,可谁能保证呢?随着业务的增多,肯定会需要申请大的空间,从而影响拷贝的执行效率。
移动构造函数,指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。想要深入了解的,可以自查哦,嘿嘿。
再一个就是将重载的 = 改为了hd.swap(*this)的实现,并且参数hd去掉了&,我们知道这种方式叫值传递,hd的任何修改对实参无影响。下面来看下改造后如何用

void print() {
    HeapDel<TestRALL> heapDel(createTest());
    heapDel->print();
    HeapDel<TestRALL> heapDel2;
    heapDel2 = std::move(heapDel);
    heapDel2->print();
}

std::move() 函数就是强制调用HeapDel的移动构造函数,如果还有拷贝构造或者其他构造函数。
现在unique_ptr的实现,你已经知道了。还有更复杂的shared_ptr,如果想把它搞明白,估计需要不少的知识,我们后续有机会再讨论,后面继续学习基础知识。

总结

本期,对智能指针两种实现unique_ptr、auto_ptr,有了深刻的理解,也明白了指针到底是什么,收获很多,确实感觉到C++的复杂性,一个构造函数就有这么多的变数。想要学明白就要理解它背后的动机以及设计的规范,很多设计的背后其实在原理上还是有共通的点,这里分享一个简单且高效的学习过程:先知道是什么,且一定要弄明白为什么,然后才是怎么用。这次分享就到这里,感谢跟我一起学习。加油。

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

推荐阅读更多精彩内容

  • C++智能指针 原文链接:http://blog.csdn.net/xiaohu2022/article/deta...
    小白将阅读 6,793评论 2 21
  • C++线程与智能指针 [TOC] 线程 线程,有时被称为轻量进程,是程序执行的最小单元。 C++11线程 POSI...
    咸鱼Jay阅读 911评论 0 1
  • 线程 线程,有时被称为轻量进程,是程序执行的最小单元。 C++11线程 POSIX线程 POSIX 可移植操作系统...
    Innocencellh阅读 384评论 0 0
  • 导读## 最近在补看《C++ Primer Plus》第六版,这的确是本好书,其中关于智能指针的章节解析的非常清晰...
    小敏纸阅读 1,951评论 1 12
  • 本节我们将介绍 C++ STL 中智能指针的使用。 智能指针(英语:Smart pointer)是一种抽象的数据类...
    思想永不平凡阅读 2,545评论 0 2