Effective Modern C++ 第四章,C++智能指针

智能指针 Smart Pointer

一些说明:

C++98中有智能指针std::auto_ptr,在C++11中,出现std::unique_ptrstd::unique_ptr包含了std::auto_ptr所有的功能;除此之外,后者效率更高,而且不会改变复制对象的意义。因此,只要编译器没有限制,我们应该使用std::unique_ptr而不是std::auto_ptr。正如它的名字中的unique那样,该指针享有对象的独有权,一个对象只能被一个std::unique_ptr指向。

Item 18: Use std::unique_ptr for exclusive-ownership resource management.

std::unique_ptr是最常用的智能指针,它占用的内存空间与原生指针一样;而且对于大多数操作,和原生指针一样。std::unique_ptr进行移动操作的时候,比如原来指向A,移动后指向B,那么A会被销毁。因此,std::unique_ptr只能进行移动操作,而不能进行复制操作,因为复制的过程销毁原来的对象,又指向被销毁的对象,是没有意义的。

std::unique_ptr的一个通用的应用是在继承过程中,作为工厂函数返回对象的值。

class Investment {}; // 基类

// 三个派生类
class Stock: public Investment {};
class Bond: public Investment {};
class RealEstate: public Investment {};

// do some operations, allocates an object on the heap, return a pointer of Investment
template<typename Ts>
std::unique_ptr<Investment>makeInvestment(Ts&& params);


template<typename T>
void caller(T& arguments) {
    auto pInvestment = makeInvestment(arguments);
    // some operations here
}   // destroy *pInvestment, 自动执行的

上述过程的操作可以理解为:std::unique_ptr被工厂函数返回,并移动到一个容器内,这个容器的成员顺序地移动到一个对象的数据成员中,并且这个一会再被销毁,当这个销毁过程发生时,std::unique_ptr也会被销毁。即使上述caller函数执行过程被异常等打断,std::unique_ptr自动销毁 过程还是会进行,除非打断的程序是std::abort, std::Exit等的函数退出命令。

std::unique_ptr构造时的详细说明

下面的代码说明了可以自定义删除函数。std::unique_ptr默认使用delete来删除对象。如果我们想在删除对象的时候做一些其他的操作,那么需要自己定义一个删除函数。

auto delInvmt = [](Investment* pInvestment) {  // lambda表达式,在这里是自定义的删除函数
    makeLogEntry(pInvestment);	// 假设这是统计类型信息,并写入日志的函数
    delete pInvestment;
};

template<typename Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&& params) {
    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);

    if(/* a Stock object should be created */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if(/* a Bond object should be created */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if(/* a RealEstate object should be created */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    } else {
        return pInv;
    }
}// 如果外界有其他作用域调用了这个函数,那么在销毁的智能指针的时候,会使用自己编写lambda函数进行销毁操作

上述代码是C++11的标准,在C++14标准准中,可以有下列更简单的方式:

template<typename Ts>
auto makeInvestment(Ts&& params) {
    auto delInvmt = [](Investment * pInvestment) {
        makeLogEntry(pInvestment);
        delete pInvestment;
    };

    std::unique_ptr<Investment, decltype<delInvmt>>pInv(nullptr, delInvmt);
    if(/* a Stock object should be created */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if(/* a Bond object should be created */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if(/* a RealEstate object should be created */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    } else {
        return pInv;
    }
}

删除器其实是在真正需要使用的时候才进行调用。

使用默认delete删除器的std::unique_ptr才与原生指针占用相同的内存空间。如果自定义了删除器,那么占用的空间由删除器中函数对象中存储的状态个数决定。比如在上述的代码中,由makeLogEntry决定智能指针的占用空间。如果指针占用的空间太大,那么需要修改删除器的设置,以满足合理空间的需求。

std::unique_ptr<T>是指向单个元素的,而std::unique_ptr<T[]>是用来指向数组的。一般来说,后者不怎么常用,因为后者更多的被std::vectorstd::string等容器所代替。

Item 19: Use std::shared_ptr for shared-ownership resource management.

一个对象可以被多个std::shared_ptr指向,如果没有任何一个std::shared_ptr指向的时候,该对象会被销毁。这就像引入了垃圾回收机制,对于被std::shared_ptr绑定的空间,如果没有任何一个std::shared_ptr指向,那么该空间会被自动销毁。一个std::shared_ptr可以显示自己是否是最后一个指向对象的智能指针。(具体参见手册)

有三点需要格外注意:

  • std::shared_ptr占用的内存空间是原生指针的两倍
  • 被指向对象的内存空间必须是动态分配
  • 增加或者减少指针引用对象的操作必须是原子操作类型的,多线程操作时要谨慎使用std::shared_ptr

类似于std::unique_ptrstd::shared_ptr默认删除器是delete,而且也支持自定义的删除器。不过,std::shared_ptr支持的删除器更加灵活。

auto customDeleter1 = [](Widget* pw) {/*...*/};
auto customDeleter2 = [](Widget* pw) {/*...*/};

std::shared_ptr<Widget>pw1(new Widget, customDeleter1);
std::shared_ptr<Widget>pw2(new Widget, customDeleter2);

因为两个删除器有相同的结构,那么上述代码可以写成:

std::vector<std::shared_ptr<Widget>>vpw{pw1, pw2};

std::shared_ptrstd::unique_ptr的另一个不同之处在于:前者的占用的空间始终是原生指针的两倍,不会随着自定义删除器的改变而变化。像删除器的函数对象、计数器等占用的内存由一个单独的控制块来管理。一个std::shared_ptr分成两部分,一个是指向对象,另一个是指向控制块(如下图)。

创建一个std::shared_ptr指向一个对象之前,不可能知道当前是否有std::shared_ptr指向该对象,因此,下面给出控制块创建的规则:

  • std::make_shared总是创建一个内存块。
  • 当一个std::shared_ptr通过std::unique_ptr构造时,内存块会被创建。
  • std::shared_ptr使用原生指针作为构造参数时,会产生控制块;使用std::shared_ptr作为参数则不会。

一定不能出现含有不同控制块的std::shared_ptr指向一个对象,这意味着会有多次销毁操作,会发生不可预知的情况!

Item 20: Use std::weak_ptr for std::shared_ptr like pointers that can dangle.

std::weak_ptr不会改变引用对象的被引用次数,同时还可以悬空。比如:

// after spw is constructed,the pointed-to Widget's ref count (RC) is 1.
auto spw = std::make_shared<Widget>();

// wpw points to same Widget as spw. RC remains 1
std::weak_ptr<Widget>wpw(spw);

// RC goes to 0, and the Widget is destroyed. wpw now dangles
spw = nullptr;

可以理解成,std::weak_ptr可以脱离对象悬空存在,而std::shared_ptr不行。

利用std::weak_ptr::expired()可以检测是否是悬空的状态。

std::shared_ptr<Widget> spw1 = wpw.lock();  // if wpw's wxpired, spw1 is nullptr
auto spw2 = wpw.lock(); // same as above, but uses auto
std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_ptr

std::weak_ptr广泛应用在观察者模式中。观察者模式中分为观察者(observer)和被观察对象(subject)。在大多数实现方式中,每一个被观察对象包含一个数据容器,用于存放指向观察者的指针,这样对于被观察者来说,就很容易表达自己的状态。被观察者对控制观察者的生命周期的长短不感兴趣,但是被观察者需要知道观察者何时被销毁,被观察者不会频繁地尝试访问它。一个合理的设计方式为:每个被观察者都包含一个容器,用于存放指向观察者的std::weak_ptr类型的指针。在被观察者使用使用之前,先检查一下指针是否悬空。
这里写图片描述
A和C是观察者,B是被观察者。???处最佳的选择方式为std::weak_ptr类型的指针。选择原生的指针,会使B在使用指针的时候不知道A是否已经被销毁;使用std::shared_ptr会进入一种指针循环,这可能会导致A和B同时被销毁(参见std::shared_ptr的销毁时间)。

Item 21: Prefer std::make_unique and std::make_shared to direct use of new .

在一般的使用过程中,我们应该使用std::make_uniquestd::make_shared ,而不是使用new方式进行创建新对象。
C++11新标准更加推荐使用make方式与智能指针配合使用,而不是使用new

// 这是推荐使用的方式
auto upw1(std::make_unique<Widget>());
auto spw1(std::make_shared<Widget>());
// 这种方式在新标准中不推荐使用
std::unique_ptr<Widget>upw2(new Widget);
std::shared_ptr<Widget>spw2(new Widget);

使用make的方式可以先出潜在的内存泄露的问题,给出下面一个例子,我们需要一个函数来根据Widget的优先级来执行:

void processWidget(std::make_shared<Widget> spw, int priority);

现在,假设有一个函数来进行计算优先级:

int compputePriority();

之后,我们直接调用new的方式,进行执行:

// 存在潜在的内存泄露!!!
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

正确的方式应该是:

processWidget(std::make_shared<Widget>(), computePriority());

可能内存泄露的原因是,在执行processWidget函数时,下列的步骤必须按照顺序执行:

  • new Widget执行,用于创建对象,widget对象创建在堆上
  • std::shared_ptr<Widget>执行构造,用于管理对象,但是前提是必须上一步已经构造了对象了
  • computePriority()必须执行,返回优先级

然而编译器执行computePriority()时的顺序可能介于newstd::make_shared之间,而此时的创建优先级函数如果抛出异常,那么智能指针不会发生构造,而new却执行完毕了,此时是一个没有指向的地址,产生泄露了。

对于后者安全的方式是,是因为make的时候,是先构造管理对象,然后分配内容。即使出现优先级计算异常,开辟的地址也被管理了,所以不用担心泄露。

对于std::make_shared也是同样的原因,使用std::make_unique而不是直接进行new操作。而且使用make的效率要比直接使用new的效率高很多。

但是,某些具体的情境下,不太适合使用make方式进行内存分配。这里给出两种情景:

  • 需要自定义析构操作
    auto widgetDeleters = [](Widget* pw) {/*...*/};
    std::unique_ptr<Widget, decltype(widgetDeleters)>upw(new, widgetDeleters);
    std::shared_ptr<Widget>spw(new Widget, widgetDeleters);
    
  • 需要使用花括号初始化,初始化列表的情况只能使用new进行分配。

Item22 暂时用不到,留作后期修正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值