C++的堆区使用以及智能指针

    Hello亲们!逗比老师又回来啦!今天我们的话题是C++11中的智能指针。

    我们知道,指针,一直以来在C语言当中就占据着统治地位,逗比老师也曾经说过,如果你不会指针,那么你就不要说你会C语言。而C++作为C的超集,自然,也少不了使用指针。但是,由于类的加入,它的构造和析构相比标准类型都要复杂很多。利用普通的指针来管理对象有一些时候就会显得不是很方便,而且,我们可能会在代码中大量出现内存管理相关的部分。因此,C++11为我们提供了智能指针,可以一定程度上减少我们程序员对内存管理的关注,而可以将更多的精力放在业务逻辑上。

    首先,我们来回忆一下,在C++当中,数据所占有的区域分为4块,分别是:1.全局区 2.静态区 3.栈区 4.堆区。全局区会在主函数运行前初始化,在程序结束后释放。静态区会在变量第一次使用时初始化,程序结束后释放。栈区会在变量定义处初始化,变量所在当前代码块结束时释放。堆区需要手动分配,手动释放。那么观察这4个区的特点,前三个其实都不太需要我们过多去关心的,只有堆区,需要我们合理安排分配和释放的时机。

    使用堆空间的办法,首先,是最基础的,也是C语言提供的方法,调用malloc()函数进行分配,调用free()函数进行释放。下面是这两个函数的函数原型:

void *malloc(size_t size);
void free(void *ptr);

    相信大家对这两个函数应该比较熟悉了,我们简单举个例子就好:

#include <iostream>
#include <cstdint>
#include <cstdlib>

int main(int argc, const char * argv[]) {
    // 分配3个32位uint类型的空间
    uint32_t *area = (uint32_t *)malloc(3 * sizeof(uint32_t));
    // 对其进行操作
    area[0] = 2;
    area[1] = 44;
    area[2] = 67;
    // 遍历打印
    for (size_t i = 0; i < 3; i++) {
        std::cout << area[i] << std::endl;
    }
    // 释放堆空间,并且清空指针
    free(area);
    area = nullptr;
    
    return 0;
}

    第二种方法是使用new和delete运算符(也包括new []和delete []运算符)。这两个运算符是原生C++提供的,针对于标准数据类型(或是POD类型)来说,他们的效果和malloc()和free()是一样的,唯一的区别就在于,new运算符返回的直接是对应的指针类型,因此不需要进行指针类型转换,示例如下:

#include <iostream>

int main(int argc, const char * argv[]) {
    // 分配1个32位uint类型的空间
    uint32_t *p = new uint32_t;
    // 对其进行操作
    *p = 4;
    // 打印
    std::cout << *p << std::endl;
    // 释放堆空间,并且清空指针
    delete p;
    p = nullptr;
    
    // 分配3个32位uint类型的空间
    uint32_t *area = new uint32_t[3];
    // 对其进行操作
    area[0] = 33;
    area[1] = 9;
    area[2] = 53;
    // 遍历打印
    for (size_t i = 0; i < 3; i++) {
        std::cout << area[i] << std::endl;
    }
    // 释放堆空间,并且清空指针
    delete [] area;
    area = nullptr;
    
    return 0;
}

    针对POD和标准数据类型很简单,但是对于对象来说,就稍微复杂一些。因为一个对象可能含有多个构造函数,而且也有可能构造函数是有参数的,请看我们的例程:

#include <iostream>

class Test {
public:
    // 无参构造函数
    Test();
    // 有参数的构造函数
    explicit Test(int a);
    // 析构函数
    ~Test();
};

Test::Test() {
    std::cout << "无参构造函数" << std::endl;
}

Test::Test(int a) {
    std::cout << "有参构造函数,a=" << a << std::endl;
}

Test::~Test() {
    std::cout << "析构函数" << std::endl;
}

int main(int argc, const char * argv[]) {
    // 利用无参构造函数在堆上创建对象
    auto t1 = new Test;
    // 利用有参的构造函数在堆上创建对象
    auto t2 = new Test(4);
    
    // 释放
    delete t1;
    t1 = nullptr;
    delete t2;
    t2 = nullptr;
    
    return 0;
}

    大家在这里一定要注意的一点就是,对于普通的运算符(比如说+),运算符的调用就是对应调用了对应的函数(operator +),但是唯独new和delete是特殊的,他们除了调用对应的函数(也就是operator new和operator delete)之外还做了别的事情。针对于new运算符,其实是经历的这两个步骤:1.调用operator new函数来返回堆空间 2.调用构造函数。而针对于delete运算符,也是经历两个步骤:1.调用析构函数 2.调用operator delete函数来释放堆空间。

    因此,对于类类型来说,使用堆空间的过程被拆分成了“分配”和“构造”,或者“析构”和“释放”。我们可以针对某个类型去重载它的operator new和operator delete函数,更改它们默认的分配堆的方法。POD和标准类型也不例外。只不过,POD和标准类型没有构造和析构而已,但是类类型是有的,因此还需要调用构造和析构。

    做个简单的实验,我们在上面例程的基础上,为Test类重载operator new和operator delete,例程如下:

#include <iostream>

class Test {
public:
    // 无参构造函数
    Test();
    // 有参数的构造函数
    explicit Test(int a);
    // 析构函数
    ~Test();
    
    static void *operator new(size_t size);
    static void operator delete(void *ptr, size_t size);
    static void *operator new [](size_t size);
    static void operator delete [](void *ptr, size_t size);
};

Test::Test() {
    std::cout << "无参构造函数" << std::endl;
}

Test::Test(int a) {
    std::cout << "有参构造函数,a=" << a << std::endl;
}

Test::~Test() {
    std::cout << "析构函数" << std::endl;
}

void *Test::operator new(size_t size) {
    std::cout << "operator new" << std::endl;
    return malloc(size * sizeof(Test));
}

void Test::operator delete(void *ptr, size_t size) {
    std::cout << "operator delete" << std::endl;
    free(ptr);
}

void *Test::operator new [](size_t size) {
    std::cout << "operator new []" << std::endl;
    return malloc(size * sizeof(Test));
}

void Test::operator delete [](void *ptr, size_t size) {
    std::cout << "operator delete []" << std::endl;
    free(ptr);
}

int main(int argc, const char * argv[]) {
    // 利用无参构造函数在堆上创建对象
    auto t1 = new Test;
    // 利用有参的构造函数在堆上创建对象
    auto t2 = new Test(4);
    
    // 释放
    delete t1;
    t1 = nullptr;
    delete t2;
    t2 = nullptr;
    
    return 0;
}

    大家要注意的是,为什么我们在重载delete和delete []的时候,还需要加size参数,明明在函数中没有用到啊?是这样的,因为我们这里堆空间是直接用malloc来分配的,因此,malloc是在全局中对分配的空间有记录的,所以我们free的时候就不需要填写size了,但是,假如我们用的并不是标准的malloc,而是一个,我们自己写的内存池的话,那么我们还是需要提供一种机制,来保证这个释放的size的,因此在标准C++中对operator delete和operator delete []函数保留了size参数。大家根据自己的需要来使用即可。

    利用传统的new来使用堆空间虽然简单,但是缺点就在于,我们必须要在不用的时候进行delete,否则这一片堆空间一直得不到释放,就会有内存泄漏。然而释放后还需要将指针清空,防止野指针出现。释放只能有一次,不能多次释放,并且,整个过程中都必须保证有至少一个有效指针在指向堆空间,否则,空间没有指针去指则会垂悬,然后再也无法获取到,也无法释放,造成内存泄漏。所以,坑还是很多的,然而C++11为我们提供了救世主,那就是智能指针。逗比老师今天会给大家介绍三种类型的智能指针,分别是shared_ptr,unique_ptr和weak_ptr。

    要使用智能指针,需要引入<memory>头文件。首先我们要介绍的是shared_ptr,顾名思义,就是共享指针。我们在需要管理的对象上添加一个数字,称为“引用计数”,有多少个shared_ptr指向它,这个数字就是几,当没有任何shared_ptr指向它时,引用计数为0,这时,自动释放对象。

    请看下面的例程:

#include <iostream>
#include <memory>

class Test {
public:
    // 无参构造函数
    Test();
    // 有参数的构造函数
    explicit Test(int a);
    // 析构函数
    ~Test();
};

Test::Test() {
    std::cout << "无参构造函数" << std::endl;
}

Test::Test(int a) {
    std::cout << "有参构造函数,a=" << a << std::endl;
}

Test::~Test() {
    std::cout << "析构函数" << std::endl;
}

int main(int argc, const char * argv[]) {
    auto p1 = new Test; // 划分堆空间
    std::shared_ptr<Test> sp(p1); // 创建智能指针
    std::cout << sp.use_count() << std::endl; // 打印引用计数
    {
        std::shared_ptr<Test> sp2(sp); // 创建另一个智能指针
        std::cout << sp.use_count() << std::endl; // 打印引用计数
    } // 这里sp2会释放
    std::cout << sp.use_count() << std::endl; // 打印引用计数
    
    return 0;
}

    这是执行结果:

无参构造函数
1
2
1
析构函数

    当sp创建时,引用计数是1,然后又有sp2指向,所以引用计数变为2,之后sp2释放了,于是引用计数变为1,最后sp也释放的时候,自动释放对象,于是我们看到了析构函数。

    所以,shared_ptr的作用就是多个指针共享一个对象的生命周期,当所有指针都释放(或是不再指向对象)的时候,自动释放对象。在这里(敲黑板了!!)大家一定一定一定要明白一件事情,智能指针的唯一作用,就是自动delete对象!注意,这一句话可以说明很多问题,其一,既然是要delete,那么这玩意首先必须是堆空间,所以如果你把栈空间、全局空间或者静态空间里变量的指针传给智能指针了,那么一定会出现奇怪的问题,因为对象在不该释放的时候被释放了,要不就是对象会重复释放,所以大家一定要注意,创建智能指针的时候,只能使用堆空间。其二,智能指针既然会自动delete对象,我们就不能再去手动delete对象了,否则,也会发生多次释放的问题。其三,也是比较深的一个坑,请先看下面例程:

int main(int argc, const char * argv[]) {
    auto p1 = new Test; // 划分堆空间
    std::shared_ptr<Test> sp(p1); // 创建智能指针
    std::shared_ptr<Test> sp2(p1); // 创建另一个智能指针
    
    return 0;
}

    这段程序运行后,将会直接抛出异常。原因很简单,因为我们的sp和sp2都是用p1这个指针来创建的,他们彼此不知道对方的存在,所以,我们等于是用了两套引用计数的体系来管理了同一个对象,这其实和我们用智能指针来管理栈空间的效果是一样的,对象会被重复释放。所以大家在使用的时候一定要注意,同一个对象只能用同一套内存管理体系,如果它已经有智能指针了,那么再创建智能指针时,需要通过原来已有的指针创建,而不能重复用原始空间来创建。

    为了避免这个问题,STL提供了一个函数,叫做make_shared,函数原型如下:

template <typename T, typename ...Args>
std::shared_ptr<T> std::make_shared(Args && ...args);

    所以,我们希望大家在使用智能指针时,用make_shared函数来创建对象,而不要手动去new,这样就可以防止我们去使用原始指针创建多个引用计数体系。例程如下:

int main(int argc, const char * argv[]) {
    auto sp = std::make_shared<Test>(); // 创建智能指针
    auto sp2 = sp; // 创建另一个智能指针
    
    return 0;
}

    下面是几个shared_ptr常用的函数的函数原型和作用:

template <typename T>
class shared_ptr {
public:
    // 通过指针构造智能指针
    shared_ptr(T *);
    // 拷贝构造(引用计数会增加)
    shared_ptr(const shared_ptr &);
    // 赋值普通指针(老对象引用计数会减少)
    shared_ptr &operator =(T *);
    // 拷贝赋值(新对象引用计数会增加,老对象引用计数会减少)
    shared_ptr &operator =(const shared_ptr &);
    // 获取普通指针
    T *get() const;
    // 获取引用计数
    long use_count() const;
    // 重置为空(老对象引用计数会减少)
    void reset();
    // 重置为新的对象(老对象引用计数会减少)
    void reset(T *);
    // 和nullptr比较
    bool operator ==(const std::nullptr_t &);

    // 重载解指针运算(得到对象的引用)
    T &operator *();
    // 重载箭头运算(得到对象的成员)
    T *operator ->();
};

    由于智能指针重载了*和->运算符,所以我们基本可以当普通指针那样去用它。还有一些不常用的,逗比在这里就不赘述了,需要的同学可以去查询相关资料。

    看起来,shared_ptr的确很方便,可以帮我们自动delete对象。但是,这里面是有坑的,今天逗比老师要介绍两种坑,一种浅坑一种深坑。我们先来看个浅坑,请看下面例程:

#include <iostream>
#include <memory>

class Test2;
class Test1 {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
    std::shared_ptr<Test2> t2; // 有个成员,是另一个类的智能指针
};

class Test2 {
public:
    // 无参构造函数
    Test2() = default;
    // 析构函数
    ~Test2() {std::cout << "~Test2" << std::endl;}
    std::shared_ptr<Test1> t1; // 有个成员,是另一个类的智能指针
};

int main(int argc, const char * argv[]) {
    auto t1 = std::make_shared<Test1>();
    auto t2 = std::make_shared<Test2>();
    t1->t2 = t2;
    t2->t1 = t1;
    
    return 0;
}

    乍一看好像没什么问题,但我们执行后就会发现,t1和t2的析构函数都没有被调用过。说明两个对象都没有得到释放,发生了内存泄漏。这是为什么呢?是这样的,我们在创建两个对象时(我们不妨称Test1的这个对象叫对象1,Test2类型的这个对象叫对象2),t1指向对象1,t2指向对象2,之后,两个赋值语句结束以后,指向对象1的指针有t1和t2->t1,指向对象2的指针有t2和t1->t2,此时两个对象的引用计数都为2。当主函数结束后,t1和t2被释放,但对象1还有t2->t1在指向,对象2还有t1->t2在指向,因此两个对象的引用计数都是1,所以无法被释放。

    这就是引用计数原理当中非常经典的循环引用问题,由于C++语言并不存在垃圾回收机制中可达性分析的机制,因此,这种循环引用问题是无法避免的。那么解决途径就是,让其中一个指针不去影响引用计数,这样就不会循环引用。所以,比较容易想到的做法就是把其中一个智能指针改成普通的指针。

    但这样做虽然可以解决循环问题,但是,这个裸指针放在这里总是很眨眼,毕竟不属于智能指针体系,而且,裸指针是不安全的(比如我们可能会直接delete它这种风险操作),所以,STL还提供了另一种智能指针,这就是weak_ptr,用weak_ptr引用是不影响引用计数的,并且,weak_ptr还有一个功能就是,如果指向的对象被释放了,它会自动置空(也就是说不会出现野指针问题),所以这显然是优于裸指针的。因此,把上面例程可以这样更改:

#include <iostream>
#include <memory>

class Test2;
class Test1 {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
    std::shared_ptr<Test2> t2; // 有个成员,是另一个类的智能指针
};

class Test2 {
public:
    // 无参构造函数
    Test2() = default;
    // 析构函数
    ~Test2() {std::cout << "~Test2" << std::endl;}
    std::weak_ptr<Test1> t1; // 有个成员,是另一个类的智能指针
};

int main(int argc, const char * argv[]) {
    auto t1 = std::make_shared<Test1>();
    auto t2 = std::make_shared<Test2>();
    t1->t2 = t2;
    t2->t1 = t1;
    
    return 0;
}

    只需要把其中的一个shared_ptr改成weak_ptr就可以完美解决所有问题。

    但是,weak_ptr和shared_ptr在使用上还是有区别的,比如说weak_ptr不能直接使用*和->运算,也不能和nullptr进行比较。那么,我们应该怎么操作呢?下面是逗比老师给大家总结的常用的weak_ptr的函数:

template <typename T>
class weak_ptr {
public:
    // 通过shared_ptr构造
    weak_ptr(const shared_ptr<T> &);
    // 拷贝构造
    weak_ptr(const weak_ptr &);
    // 赋值
    weak_ptr &operator =(const shared_ptr<T> &);
    // 拷贝赋值
    weak_ptr &operator =(const weak_ptr<T> &);
    // 获取shared_ptr
    shared_ptr<T> lock() const;
    // 判断是否为空
    bool expired() const;
};

    因此我们要操作对象,需要先lock()取出对应的shared_ptr再进行操作,而判空是需要调expired(),如果返回true则表示为空。不过这里lock这个名字略显诡异,为什么不直接叫get_shared这样呢,而要叫个lock?这是因为考虑了多线程的场景,假如我们通过weak_ptr操作对象的时候,正好这个对象所有的shared_ptr都释放了,那这时候还是会出现野指针问题。因此,这时我们通过lock()函数返回的这个临时的shared_ptr会让对象的引用计数+1,这样可以保证我们的操作完成以后,对象再被释放。就像是暂时给这个对象加了把锁,所以起名叫lock(说实话,我还是觉得叫get_shared比较好………………)。

    这个小坑介绍完了,接下来我们介绍个大坑,请看下面例程:

#include <iostream>
#include <memory>

class Test1 {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
};

void test1(const std::shared_ptr<Test1> p) {
    std::cout << p.use_count() << std::endl;
}

void test2(const std::shared_ptr<Test1> &p) {
    std::cout << p.use_count() << std::endl;
}

void test3(const std::shared_ptr<const Test1> &p) {
    std::cout << p.use_count() << std::endl;
}

int main(int argc, const char * argv[]) {
    auto t1 = std::make_shared<Test1>();
    test1(t1);
    test2(t1);
    test3(t1);
    
    return 0;
}

    请问,test1,test2和test3函数分别会打印几?运行一下可能大出所料:

2
1
2
~Test1

    我们来一个一个解释。首先test1,参数是个智能指针,只不过加上了const,这个不要紧,p仍然是个新的指针,因此在函数体中,p和主函数中的t1这两个指针会同时指向同一个对象,所以引用计数为2,这个应该好理解。

    再来看test2,参数是个智能指针的引用,而把t1传进来的时候,这个引用绑定了t1,因此p只是t1的引用,而并不是新的指针,因此,自始至终只有一个指针指向对象,所以,引用计数为1,这个应该也不难理解。

    令人费解的就是这个test3了,明明这里p被声明成了引用,可为什么引用计数还是增加了呢?原因就在于,我们给Test1前面加个这个const。当这种情况的时候,大家一定不能再傻乎乎把智能指针无脑地当指针来对待了,虽然说它叫智能指针,但它本质上来说,还是个模板类,只不过起到了指针的作用罢了。既然是模板类,那么在传入参数时会被实体化成一个具体的类。因此,shared_ptr<Test>和shared_ptr<const Test>是两个不同的类,照理说,假如AB两个类没有任何关系的话,A的引用是不能用来接收B的对象的。然而此时却能够接收,那么此时必有蹊跷。

    其实,在shared_ptr类中有类似这样一个定义:

template <typename T>
shared_ptr<const T>(const shared_ptr<T> &);

    换句话说,我们可以用Test的指针构造出const Test的指针,然而,这个构造函数并没有用explicit修饰,因此允许隐式转换。而又因为很碰巧地,const引用除了可以作为引用来使用外,还可以作为普通变量来使用,比如说用常引用绑定常量时,相当于一个普通变量:

const int &a = 4; // 用常引用绑定常量
const int a = 4; // 和上面写法等价,都会占用一个int的空间
const int &b = a; // 用常引用绑定同类型的变量,是指针的语法糖(可以理解为别名)

    正是因为【shared_ptr<Test>可以隐式构造shared_ptr<const Test>】以及【常引用有时等价于普通变量】这两件事情凑到一起,就造成了现在test3中的情况。首先,由于shared_ptr<Test>和shared_ptr<const Test>是不同的类型,因此,shared_ptr<const Test>不能直接用来绑定t1。但是,shared_ptr<Test>可以用来隐式构造shared_ptr<const Test>,因此,常引用const shared_ptr<const Test> &p作为了普通变量来使用,其构造参数就是t1,因此,这里p是一个新的对象,也就是另一个不同于t1的指针(要想验证这个说法,可以打印一下&p和&t1,它们地址是不同的),于是,引用计数变成了2。总结一下test3,其实等价成了下面的操作:

test3(t1); // 非同类型绑定,常引用作为新的普通变量来使用
// 上面的等价于下面的
test3(std::shared_ptr<const Test>(t1));
// 或者可以等价于下面的
const std::shared_ptr<const Test> tmp = t1;
test3(tmp); // 同类型绑定,常引用作为普通的引用来使用

    所以这里的深坑就在于,shared_ptr<const T>和shared_ptr<T>要作为两个不同的类型来对待,而并不能当做同一类型的const和非const来对待。所以在使用常引用的时候,要小心类似的问题。

    接下来我们再介绍一种智能智能,假如说,我这个对象并不需要在多个指针间共享生命周期,而是某个指针独有的,那么,这时候再使用shared_ptr就会显得不安全,同时引用计数系统还会占用一定的资源。因此在C++14标准中引入了unique_ptr,还有make_unique函数。基本的使用和shared_ptr如出一辙,只是unique_ptr不可以拷贝,也不能转化为shared_ptr,自然也没有引用计数器。当指针不再指向对象时,对象自动释放。unique_ptr同样可以进行*和->操作,也可以和nullptr进行比较,还可以通过get()获取裸指针,不再赘述。如下面例程,简单做个示范:

#include <iostream>
#include <memory>

class Test1 {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
    void test() {}
};


int main(int argc, const char * argv[]) {
    auto p = std::make_unique<Test1>();
    p->test(); // 可以通过->操作
    auto &t = *p; // 可以进行解指针操作
    auto ptr = p.get(); // 可以获取裸指针
    p.reset(); // 可以重置
    std::cout << (p == nullptr) << std::endl; // 可以和nullptr比较
    
    return 0;
}

    unique_ptr既然一次只能有一个指针指向,自然管理起来方便许多。但是,shared_ptr还有一个问题没有解决,假如说,在一个类当中,我们需要用到自己的智能指针怎么办?因为我们创建智能指针肯定是在类外,那如果类里要使用怎么办?请看下面例程:

#include <iostream>
#include <memory>

class Test1;
void show(std::shared_ptr<Test1> p) {}

class Test1 : public std::enable_shared_from_this<Test1> {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
    void test() {
        show(shared_from_this());
    }
};


int main(int argc, const char * argv[]) {
    auto p = std::make_shared<Test1>();
    p->test();
    
    return 0;
}

    由于在成员函数test中需要调用show函数传入自己,但是个智能指针类型,于是,我们需要让这个类继承自std::enable_shared_from_this,这个类中提供了一个函数叫shared_from_this,顾名思义就是this指针的shared_ptr版。对应的还有一个是返回weak指针的叫weak_from_this,同样定义在std::enable_shared_from_this中,不再赘述。

    但是这里看起来简单,还是有两个坑在里面的。第一个坑就是,构造函数中不能调用shared_from_this。为什么呢?答案很简单,因为我们调make_shared函数中,会调用new,而new中会调用构造函数,这个时候,make_shared还没完成,自然也就没有智能指针的生成,所以,在构造函数中调用shared_from_this就会导致程序抛出异常,因此,我们应该避免在构造函数中使用shared_from_this。再次强调一遍,更不能用shared_ptr(this),这种方式,因为这样会引入另一个引用计数系统,造成重复delete。

    另一个坑就是,如果继承了std::enable_shared_from_this类以后,这个类如果有派生类的话,派生类不能够直接使用shared_from_this,因为类型会不匹配,也不能重复继承std::enable_shared_from_this,因为会造成非虚函数的重复定义。请看以下例程:

#include <iostream>
#include <memory>


class Test1 : public std::enable_shared_from_this<Test1> {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
};

class Test2;
void show(std::shared_ptr<Test2> p) {}

class Test2 : public Test1 {
public:
    void test() {
        // 错误!!!因为类型不匹配导致编译不通过
        show(shared_from_this());
    }
};


int main(int argc, const char * argv[]) {
    auto p = std::make_shared<Test2>();
    p->test();
    
    return 0;
}

    这种写法是错误的,因为此时shared_from_this会返回shared_ptr<Test1>类型,不符合show函数参数,会导致编译不通过。

class Test2 : public Test1, public std::enable_shared_from_this<Test2> {
public:
    void test() {
        // 错误!!!因为对同名同参函数,不能进行仅有返回值不一致的重载
        show(shared_from_this());
    }
};

    这种写法也是错误的,因为Test1和std::enable_shared_from_this<Test2>都会提供一个shared_from_this函数的声明,函数名和参数都是一致的,只是返回值不同,无法构成函数重载,因此会报错。

class Test2 : public Test1 {
public:
    std::shared_ptr<Test2> shared_from_this() {
        return std::dynamic_pointer_cast<Test2>(Test1::shared_from_this());
    } 
    void test() {
        // 错误!!!因为shared_from_this不是虚函数
        show(shared_from_this());
    }
};

    这种写法也是不正确的,因为shared_from_this并不是虚函数,如果对非虚函数进行重写的话,当多态场景(父类指针或引用指向子类对象)时,调用到父类的函数,导致返回结果错误。

class Test2 : public Test1 {
public:
    void test() {
        show(std::dynamic_pointer_cast<Test2>(shared_from_this()));
    }
};

    因此只有上面这一种写法是正确的,我们只能死皮赖脸地显式去进行指针动态转化然后调用父类的shared_from_this了。

    哦对了!还有一个坑没有说,那就是,一旦某个类继承的std::enable_shared_from_this,那么,这个类的对象必须通过shared_ptr和weak_ptr来管理,而不能用于栈空间、全局变量、静态变量、成员对象的方式,否则,shared_from_this将会抛出异常(因为找不到对应的shared_ptr)。请看下面例程:

#include <iostream>
#include <memory>


class Test1 : public std::enable_shared_from_this<Test1> {
public:
    // 无参构造函数
    Test1() = default;
    // 析构函数
    ~Test1() {std::cout << "~Test1" << std::endl;}
    void test() {}
};

Test1 g_t1; // 错误,不能用于全局空间

class Test2 {
    Test1 t1; // 错误,不能用于成员对象
    std::shared_ptr<Test1> pt1; // 正确,可以通过shared_ptr管理
    std::weak_ptr<Test1> wpt1; // 正确,可以通过weak_ptr管理
};


int main(int argc, const char * argv[]) {
    static Test1 s_t1; // 错误,不能用于静态空间
    Test1 t1; // 错误,不能用于栈空间
    auto p = new Test1;
    p->test(); // 错误,不能用于非shared_ptr管理的纯粹堆空间
    auto up = std::make_unique<Test1>(); // 错误,不能用于unique_ptr
    auto sp = std::make_shared<Test1>(); // 正确,必须用shared_ptr管理【1】
    std::shared_ptr<Test1> sp2(p); // 【2】
    p->test(); // 正确,因为之前已经用智能指针管理过了,所以可以使用了【2】
    
    return 0;
}

    例程已经解释得很清楚了。但是有一点要注意的就是标记【1】和【2】的部分不能同时出现,因为不能同时用两套体系。使用new创建的堆空间,如果用于创建智能指针了(当然,只能创建1次的情况下),就可以使用了,否则,仍然不能使用。因此,这里再次再次强烈建议大家用make_shared来创建对象,不要再手动来new了!并且,如果非必要,还是不要使用enable_shared_from_this,因为这样可以兼顾所有的内存管理方式,然而一旦使用了,就只能用shared_ptr来管理了。这一点大家一定一定要注意!

    好啦,今天的内容就到这里啦!如果大家还有什么问题,欢迎留言,我们一起讨论。我是逗比老师,一个编程达人,同时也是专业的逗比,感谢大家的支持!

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

borehole打洞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值