文章目录

  • ​​0 结论​​
  • ​​1 裸指针的坏处​​
  • ​​2 智能指针概述​​
  • ​​2.1 std::auto_ptr​​
  • ​​2.2 std::unique_ptr​​
  • ​​3 高效使用std::unique_ptr(管理具备专属对象的资源)​​
  • ​​3.1 形式​​
  • ​​3.2 使用情景(包括析构器)​​
  • ​​4 高效使用std::shared_ptr(管理具备共享所有权的资源)​​
  • ​​4.1 引用计数​​
  • ​​4.2 内存细节​​
  • ​​4.3 使用场景(包括析构器)​​
  • ​​4.3.1错误使用(使用裸指针)​​
  • ​​4.3.2 错误用法(成员函数嵌套std::shared_ptr)​​
  • ​​5 高效使用std::weak_ptr(管理有可能空悬的指针)​​
  • ​​5.1 检测空悬指针​​
  • ​​5.2 带缓存的工厂函数​​
  • ​​5.3 避免std::shared_ptr指针环路​​
  • ​​6 高效率使用make系列函数​​
  • ​​6.1 优点​​
  • ​​6.2 缺点​​
  • ​​7 智能指针运用———Pimp(point to implementation)​​

0 结论

1 裸指针的坏处

  • 裸指针的声明没有指出指向单个对象和一个数组;
  • 从裸指针的声明中不知道指针是否拥有其指向的对象;
  • 不知道如何析构才合适;
  • 使用deleted运算符时,一旦把delete(对象)和delete[](数组形式)用错,就会导致未定义;
  • 要保证析构在所有代码路径上仅执行一次也是十分困难的,如果析构次数多于一次,就会导致未定义行为;
  • 没有什么正规的方式能够检测出指针是否空悬(是否指向对象);
  • 。。。等等

智能指针却可以解决这些问题,它可以动态管理分配对象的生命周期。

2 智能指针概述

共有四种智能指针: ​​std::auto_ptr​​​、​​std::unique_ptr​​​、​​std::shared_ptr​​​、​​std::weak_ptr​​。

2.1 std::auto_ptr

C++98中对智能指针进行标准化的尝试(问题:std::auto_ptr对象进行复制操作时会将其置空,不能在容器中存储​​std::auto_ptr​​​),后面成为C++11中的​​std::unique_ptr​​(需要用到移动语意)。除非使用C++98编译器时,才去使用它。

2.2 std::unique_ptr

可以完成​​std::auto_ptr​​中任何事,效率一样高,不用扭曲语意去复制对象。

3 高效使用std::unique_ptr(管理具备专属对象的资源)

​std::unique_ptr​​​拥有和裸指针相同的尺寸,对于大多数情况和裸指针一样高效。​​std::unique_ptr​​不允许复制,否则两者会指向同一资源,导致两者都认为应该析构此资源,从而资源被重复析构。

优点:

  • 小巧、高速、具备只移动类型的智能指针,对托管资源实施专属所有权语义;
  • 默认析构资源采用delete运算符,但可以指定自定义删除器(有状态的删除器和采用函数指针实现的删除器会增加​​std::unique_ptr​​类型的对象尺寸);
  • 讲​​std::unique_ptr​​​转换为​​std::shared_ptr​​是容易实现的。

3.1 形式

有两种形式:

  • 单个对象(​​std::unique_ptr<T>​​),不提供索引运算符(operator[])
  • 数组(​​std::unique_ptr<T[]>​​),不提供提领运算符(operator*和operator->)

数组的形式使用的比较少,例如C风格的API,返回堆上的裸指针,且指定指涉对象的所有权。

3.2 使用情景(包括析构器)

在对象继承谱系中作为工厂函数的返回类型。

例如下面情况:

A、B、C都继承于Base。

class Base{
public:
Base(double num):m_value(num){}
virtual ~Base(){std::cout<<"Base";}//必备
virtual void print() = 0;
protected:
double m_value;
};

class A:public Base{
using Base::Base;//使用父类构造函数
public:
void print(){std::cout<<"A:"<<Base::m_value<<std::endl;}
};

class B:public Base{
using Base::Base;
public:
void print(){std::cout<<"B:"<<Base::m_value<<std::endl;}
};

class C:public Base{
using Base::Base;
public:
void print(){std::cout<<"C:"<<Base::m_value<<std::endl;}
};
void makeLogEntry(Base* pInvestment){
std::cout<<"打印日志"<<std::endl;
}
//自定义析构器
auto delBase = [](Base* pInvestment){
makeLogEntry(pInvestment);
delete pInvestment;
};
//工厂函数 C++11版本
template<typename... T>
std::unique_ptr<Base, decltype(delBase)>
makeBase(int type, T&&... params){
std::unique_ptr<Base, decltype(delInvmt)> pInv(nullptr, delBase);//待返回的指针
if ( type == 1 )
{
pInv.reset(new A(std::forward<T>(params)...));//完美转发
}
else if ( type == 2 )
{
pInv.reset(new B(std::forward<T>(params)...));
}
else if (type == 3 )
{
pInv.reset(new C(std::forward<T>(params)...));
}
return pInv;

};

调用:

int main(){
auto p = makeBase(1, 2.8);
p->print();
return 0;
}

C++14版本的工厂函数(区别:自定义析构器位于内部):

template<typename... T>
std::unique_ptr<Base, decltype(delBase)>
makeBase(int type, T&&... params){
std::unique_ptr<Base, decltype(delBase)> pInv(nullptr, delBase);

auto delInvmt = [](Base* pBase){//自定义析构器位于内部
makeLogEntry(pBase);
delete pBase;
};

if ( type == 1 )
{
pInv.reset(new A(std::forward<T>(params)...));
}
else if ( type == 2 )
{
pInv.reset(new B(std::forward<T>(params)...));
}
else if (type == 3 )
{
pInv.reset(new C(std::forward<T>(params)...));
}
return pInv;
};

在​​std::unique_ptr​​使用默认析构器时,大小和裸指针尺寸相同;在自定义析构器后,

  • 若析构器是函数指针,那么​​std::unique_ptr​​会增加一道两个字长;
  • 若析构器是函数对象,则带来的尺寸变化取决于该函数对象存储的状态
  • 析构器采用无状态函数对象不会浪费任何存储尺寸【如,lambda表达式】)
  • 析构器采用带有大量状态的函数对象实现,可能使得​​std::unique_ptr​​对象增加大量尺寸。

4 高效使用std::shared_ptr(管理具备共享所有权的资源)

​std::shared_ptr​​拥有和垃圾回收类似的功能,并且具备预测资源回收的时序(类似于析构函数)。这里附一下手写版的的​​std::shared_ptr源代码​​。

多个​​std::shared_ptr​​​共同拥有一个对象,所有指涉某个对象的​​std::shared_ptr​​​共同协作,当最后一个​​std::shared_ptr​​不再指向该对象时,该对象才被析构。

4.1 引用计数

由于​​std::shared_ptr​​的实现是由引用计数(指针句柄类)完成的,下面讲一引用计数。

引用计数的操作:

  • 构造函数:引用计数增加(移动构造函数【移动构造函数构造对象时,不再分配新内存,而是接管源对象的内存,移动后源对象进入可被销毁的状态(源对象中如果有指针数据成员,那么它们应该在移动构造函数中应该赋值为NUL)】不会增加);
  • 析构函数:引用计数递减;
  • 复制赋值运算符函数:形如sp1 = sp2, 两个指针sp1指向了sp2指向的对象,sp1的引用计数递减,sp2的引用计数递增;
  • 如果引用计数递减喂0,即不再有​​std::shared_ptr​​​指向该资源,​​std::shared_ptr​​就会析构此资源。

引用计数带来的性能影响:

  • 尺寸是裸指针的两倍。(内容包含了两个指针,一个指向资源的裸指针,一个指向引用计数的裸指针) ;
  • 引用计数的内存必须是动态分配的(无论是否使用​​std::make_ptr​​);
  • 引用计数的递增和递减必须是原子操作。

4.2 内存细节

​std::shared_ptr<T>​​的内存:

  • 指向T类型的对象指针:
  • T类型的对象
  • 指向控制块的指针:
  • 引用计数
  • 弱计数
  • 其他数据(自定义删除器、分配器等)

无论自定义析构器是什么类型的,​​std::shared_ptr​​对象的尺寸都相当于裸指针的两倍,因为指定的析构器会被复制一份到控制块中,而这部分内存被分配在堆上或者是自定义内存分配器的内存位置上(如意支持自定义内存分配器)。

控制块规则:

  • ​std::make_shared​​总是创建一个控制块;
  • 从具备专属所有权的指针(即​​std::unique_ptr​​​或​​std::auto_ptr​​​指针)出发创造一个​​std::shared_ptr​​时,会创建一个控制块。

4.3 使用场景(包括析构器)

​std::shared_ptr​​​仅被用于处理指向单个对象的指针,不能处理数组,没有​​std::shared_ptr<T[]>​​的形式。

自定义析构器:

由于析构器不属于​​std::shared_ptr​​​中的一部分,所以每个​​std::shared_ptr​​都可以拥有自己的析构器。

class Widget{

};

auto customDelete1 = [](Widget* pw){};
auto customDelete2 = [](Widget* pw){};

int main(){
std::shared_ptr<Widget> spw(new Widget,customDelete1);
std::shared_ptr<Widget> spw2(new Widget, customDelete2);//定义不同的析构器

}

还可以执行下面的代码,因为spw和spw2具有相同的类型,可以被相互赋值,亦可以传递给​​std::shared_ptr<Widget>​​类型参数的函数。

std::vector<std::shared_ptr<Widget>> vpw{spw, spw2};

4.3.1错误使用(使用裸指针)

class Widget{

};

void makeLogEntry(Widget* pw){
std::cout<<"打印日志"<<std::endl;
}
//自定析构器
auto loggingDel = [](Widget* pw){
makeLogEntry(pw);
delete pw;
};

int main(){
auto pw = new Widget;
std::shared_ptr<Widget> spw1(pw, loggingDel);//malloc: *** error for object 0x7fe16cc05960: pointer being freed was not allocated
std::shared_ptr<Widget> spw2(pw, loggingDel);
}

错误原因:违背了使用裸指针的建议,被指向的对象会有多重控制块,意味着有多重指针,也就是析构的时候,该对象会被析构多次。

解决方法:尽量避免传递裸指针给​​std::share_ptr​​​,而是使用​​std::make_shared​​​。如果采用自定义析构函数(无法使用​​std::make_shared​​​),则使用​​std::share_ptr<对象类型> 变量名(new 对象类型, 析构器函数)​

4.3.2 错误用法(成员函数嵌套std::shared_ptr)

class Widget;
std::vector<std::shared_ptr<Widget>> processWigets;

class Widget{
public:
void process();
};

void Widget::process() {
processWigets.emplace_back(this);
}


int main(){
Widget w;
w.process();//error for object 0x7ffeeb2479a8: pointer being freed was not allocated
}

在上面的例子中,Widget对象的成员函数process外部嵌套了一个​​std::shared_ptr​​,会导致被重复析构。

解决方法:

类继承​​std::enable_shared_from_this​​​,然后使用​​this指针​​​时换成​​shared_from_this()​​​。​​shared_from_this()​​​会去查询当前对象的控制块,并创建一个指向当前控制块的心新的​​std::shared_ptr​​​,因此就必须有一个指向当前对象的​​std::shared_ptr​​。

为了解决问题,我们将继承自​​std::enable_shared_from_this​​​的累的构造函数声明为private访问层级,并且只允许使用​​std::share_ptr​​的工厂函数来创建对象。

修改后的版本为:

class Widget;
std::vector<std::shared_ptr<Widget>> processWigets;

class Widget: public std::enable_shared_from_this<Widget>{
public:
template<typename... Ts>
static std::shared_ptr<Widget> create(Ts&&... params){
std::shared_ptr<Widget> spw(new Widget);
return spw;
}
void process();
private:
Widget(){}
};

void Widget::process() {
processWigets.emplace_back(shared_from_this());//libc++abi: terminating with uncaught exception of type std::__1::bad_weak_ptr: bad_weak_ptr
}


int main(){
auto wp = Widget::create();
wp->process();
}

5 高效使用std::weak_ptr(管理有可能空悬的指针)

​std::weak_ptr​​​是​​std::shared_ptr​​​的一种扩展,一般通过​​std::shared_ptr​​​来创建,二者指向相同位置,​​std::weak_ptr​​​不会影响所指向对象的引用计数(操作的是弱计数)。可以使用​​std::weak_ptr​​检测空悬指针。

使用场景:

  • 缓存;
  • 观察者列表;
  • 避免​​std::shared_ptr​​指针环路。

5.1 检测空悬指针

测试1:

class Widget{

};

int main(){
auto spw = std::make_shared<Widget>();
auto spw2 = std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw);

spw = nullptr;
spw2 = nullptr;//让引用计数为0,指向对象被析构

if(wpw.expired()){//检测std::shared_ptr是否失效
std::cout<<"Object is nullptr";
}
}

测试2:

class Widget{

};

int main(){

std::weak_ptr<Widget> wpw;

{
auto spw = std::make_shared<Widget>();
auto spw2 = std::make_shared<Widget>();
wpw = spw;
}

if(wpw.expired()){
std::cout<<"Object is nullptr";
}
}

在多线程中,如果expired的调用和提领操作之间,另一个线程可能重新赋值或者析构最后一个指向该对象的​​std::std::shared_ptr​​,从而导致该对象被析构,此情况下提领会引发未定义行为。

解决方法是使用原子操作完成​​std::weak_ptr​​是否失效的检测,有两种方法:

方法一(不报错,返回一个​​std::shared_ptr​​​,如果​​std::weak_ptr​​​失效,则​​std::shared_ptr​​为空):

class Widget{

};

int main(){
auto spw = std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw);
spw = nullptr;
std::shared_ptr<Widget> spw1 = wpw.lock();//若wpw失效,则spw1为空
if(spw1 == nullptr){
std::cout<<"Object is nullptr";
}
}

方法二(若​​std::weak_ptr​​失效,会抛出异常):

class Widget{

};

int main(){
auto spw = std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw);
spw = nullptr;
std::shared_ptr<Widget> spw3(wpw);//libc++abi: terminating with uncaught exception of type std::__1::bad_weak_ptr: bad_weak_ptr
}

5.2 带缓存的工厂函数

缓存管理器的指针需要能够校验指向缓存对象的指针何时为空。

std::unique_ptr<const Widget> loadWidget(int widgetID);

#include <unordered_map>
std::shared_ptr<const Widget>
fastLoadWidget(int widgetID){
static std::unordered_map<int, std::weak_ptr<const Widget>> cache;

auto objPtr = cache[widgetID].lock();

if(!objPtr){//如果对象不在缓存中
objPtr = loadWidget(widgetID);
cache[widgetID] = objPtr;
}
return objPtr;
}

此代码会导致的问题为当时Widget不再被使用(被析构)时,缓存中的失效​​std:;weak_ptr​​会不断的积累。

另外一个用例,观察者设计模式:主要组件为主题(可以改变状态的对象)和观察者(对象状态发生改变后通知的对象)。

在多数实现下,每个主题包含一个数据成员,该成员指向观察者的指针,使得主题可以容易地在状态发生改变时发出通知。主题不会去关心观察者的生存周期,但是需要当一个观察者被析构后,主题不在去访问它。

合理的设计方式就是让每个主题持有一个容器来放置指向观察者的​​std::weak_ptr​​,以便指针在使用某个指针时,检查它是否空悬。

5.3 避免std::shared_ptr指针环路

例子:

A ⇆B ← C

描述:指针A、B互指,指针B指向C。

存在的问题:

  • 裸指针:如果A被析构,C仍然指向B,B将保存指向A的控制者,但是B却检测不出来,当B提领这个空指针,将会产生未定义;
  • ​std::shared_ptr​​​:A、B相互保存指向对方的​​std::shared_ptr​​,当C不在指向B时,程序结束后,A、B的引用计数还一直为1,无法进行析构,相当于发生了内存泄露。

实例代码:

class Widget2;
class Widget{
public:
Widget(){std::cout<<"Widget"<<std::endl;}
~Widget(){std::cout<<"~Widget"<<std::endl;}
void setWidget2(std::shared_ptr<Widget2> w){sp = w;}
private:
std::shared_ptr<Widget2> sp;
};

class Widget2{
public:
Widget2(){std::cout<<"Widget2"<<std::endl;}
~Widget2(){std::cout<<"~Widget2"<<std::endl;}
void setWidget(std::shared_ptr<Widget> w){sp = w;}
private:
std::shared_ptr<Widget> sp;
};

int main(){
std::shared_ptr<Widget> spw(new Widget);
std::shared_ptr<Widget2> spw2(new Widget2);
spw->setWidget2(spw2);
spw2->setWidget(spw);
}

解决上面问题的方法,使用​​std::weak_ptr​​。

  • 裸指针:若A被析构,B的回指指针为空,但是B能检测到。
  • ​std::shared_ptr​​:尽管A、B指向彼此,但是B持有的指针不会影响A的引用计数,因此不会阻止析构。

6 高效率使用make系列函数

​make系列函数​​包括:

*​​std::unique_ptr​

  • ​std::shared_ptr​

​make系列函数​​的优点:

  • 消除代码重复性
  • 改进异常安全性
  • 生成的代码尺寸更小、速度更快

缺点:

  • 不适合定制删除器以及期望直接传递大括号初始化物

不适合场景:

  • 自定义内存管理的类;
  • 内存紧张的系统、非常大的对象、以及存在比指向到相同对象​​std::shared_ptr​​​生存期更久的​​std::weak_ptr​

6.1 优点

  • 消除重复代码:
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

auto upw(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);
  • 异常安全
class Widget{

};

int computePriority() {//计算相对优先级

}

void processWidget(std::shared_ptr<Widget>(spw), int priority){

}

int main(){
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
}

上面的代码会因为使用了new运算符导致内存泄露,因为在​​processWidget​​的调用过程中前,会完成评估求值:

  • 表达式new Widget必须先完成求值,即一个Widget对象必须先在堆上创建;
  • 由new产生的裸指针的托管对象​​std::shared_ptr<Widget>​​的构造函数必须执行;
  • ​computePriority​​必须运行。

​new Widget​​​必须在​​std::shared_ptr​​​的构造函数调用之前执行完,但是​​computePriority​​​却可以在上述两个前、中、后调用,如果在new和​​std::shared_ptr<Widget>​​​之间调用就会出现内存泄漏。因为如果​​computePriority​​产生了异常,那么动态分配的Widget会被泄漏,解决的方法就是使用下面的语句。

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

​std::make_shared<Widget>()​​​和​​computePriority()​​​无论谁先被调用,都不存在内存泄漏,如果​​std::make_shared<Widget>()​​​先被调用,那么​​Widget​​​会在调用​​computePriority()​​​之前安全存储在​​std::shared_ptr​​​对象中,即使后面的​​computePriority()​​​发生异常,Widget也可以被正常析构。如果​​computePriority()​​​先被调用并产生了异常,那么​​std::make_shared​​将不会被调用。

如果使用了自定义析构器,则使用下面的方法(使用​​std::move​​,是为了使用右值移动语义):

class Widget{

};

int computePriority() {//计算相对优先级

}

void cusDel(Widget *ptr){//自定义的析构器

}

void processWidget(std::shared_ptr<Widget>(spw), int priority){

}

int main(){
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());
}

6.2 缺点

  • 完美转发是否使用​​std::initializer_list​​类型构造

在​​make​​​系列函数中,对行参进行完美转发【当创建对象使用的是大括号,则会匹配行参类型为​​std::initializer_list​​​类型的构造函数;如果使用的是圆括号,则会匹配非​​std::initializer_list​​类型的构造函数】的代码使用的是圆括号【()】而非大括号【{}】。

auto spv = std::make_shared<std::vector<int>>(10, 20);

如果需要大括号初始化物来创建指向对象的指针,就必须直接使用new表达式。一种的变通的方式,就是使用auto。

auto initList = {10, 20};//10个20元素
auto spw = std::make_shared<std::vector<int>>(initList);
  • 自定义​​operate new​​​和​​operate delete​

Widget的​​operate new​​​和​​operate delete​​​被设计为处理的尺寸都刚好是​​sizeof(Widget)​​​的内存块,而​​std::make_shared​​所需要的尺寸是动态分配的对象尺寸+控制块的尺寸。

  • 内存紧张的系统、非常大的对象、以及存在比指向到相同对象​​std::shared_ptr​​​生存期更久的​​std::weak_ptr​

在使用​​std::make_shared​​​时,所分配的内存在最后一个​​std::shared_ptr​​​和最后一个指向它的​​std:;weak_ptr​​​都被析构前,都无法得到释放。而使用​​std::shared_ptr<>(new)​​,则不会出现这样的情况。

class BigType{

};

auto pBigObj = std::make_shared<BigType>();

//创建多个指向大对象的std::shared_ptr和std:;weak_ptr
//使用智能指针操作对象

//最后一个指向对象的std:;shared_ptr在此被析构
//但指向该对象的std::weak_ptr仍存在
//大对象所占内存未被回收

//最后一个指向该对象的std::weak_ptr被析构
//控制块和对象所占的同一块内存在此被释放
class BigType{

};

std::shared_ptr<BigType> pBigObj(new BigType);

//创建多个指向大对象的std::shared_ptr和std:;weak_ptr
//使用智能指针操作对象

//最后一个指向对象的std::shared_ptr在此被析构
//但指向该对象的std::weak_ptr仍存在
//大对象所占内存被回收

//在此阶段,仅控制块所占的内存仍在处于分配未回收状态

//最后一个指向该对象的std::weak_ptr被析构
//控制块所占的同一块内存在此被释放

7 智能指针运用———Pimp(point to implementation)

Pimpl:指向到实现的指针。即,把某类的数据成员用一个指向某个实现类(或结构体)的指针替代,然后把原来在主类中的数据成员放在实现类中,并通过指针间接访问这些数据成员。

作用:

  • 降低类的客户和类实现者之间的依赖;【把头文件的成员变量的库声明放在.cpp中,减少客户使用Widget时的编译时间,同时当成员变量数量发生变化时,也不会影响到Widget的头文件。】
  • 下面的建议都只适用于​​std:;unique_ptr​​​,但不适用​​std::shared_ptr​​【无需显示声明特种成员函数】;
  • 对于采用​​std::unique_ptr​​实现的Piml指针,需在类的头文件中声明特种成员函数,但在实现类中实现它们,即使默认函数实现有着正确的行为。

⚠️:一般使用​​std:;unique_ptr​​​,是因为对象内部(例如:Widget)的Piml指针拥有相应的实现对象(例如:​​Widget::Impl​​​对象)的专属所有权。如果需要共享所有权时,则使用​​std:;shared_ptr​​。

原始版本:

//gadget.h
struct Gadget{

};

//widget.h
class Widget{
public:
Widget();

private:
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

C++98版本:

.h

struct Gadget{

};

class Widget{
public:
Widget();
~Widget();

private:
struct Impl;
Impl *pImpl;
};

.cpp

#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(new Impl) {

}

Widget::~Widget() {
delete pImpl;
}

C++11错误版本(原因:在析构时,C++11会使用static_assert来确保裸指针未指向到非完整类型,于是就会报错【虽然号称​​std::unique_ptr​​可以支持非完整类型】):

//.h

struct Gadget{

};

class Widget{
public:
Widget();

private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};

//.cpp

struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>()) {

}

正确版本(添加默认析构函数的声明和定义):

//.h
struct Gadget{

};

class Widget{
public:
Widget();
~Widget();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};

//.cpp

struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>()) {

}

Widget::~Widget() =default;

改进版本(添加移动构造和移动赋值):

//.h
struct Gadget{

};

class Widget{
public:
Widget();
~Widget();

Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};


//.cpp
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>()) {

}

Widget::~Widget() =default;

Widget &Widget::operator=(Widget &&rhs) =default;

Widget::Widget(Widget &&rhs) =default;

最终版本(添加深复制【复制构造和复制操作符函数】)

//.h
struct Gadget{

};

class Widget{
public:
Widget();
~Widget();
//移动
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
//复制
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs);

private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};

//.cpp
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>()) {

}

Widget::~Widget() =default;

Widget &Widget::operator=(Widget &&rhs) =default;

Widget::Widget(const Widget &rhs) :pImpl(std::make_unique<Impl>(*rhs.pImpl)){}

Widget &Widget::operator=(const Widget &rhs) {
*pImpl = *rhs.pImpl;
return *this;
}

Widget::Widget(Widget &&rhs) =default;

//测试:
// Widget w1;
// auto w2(std::move(w1));//移动构造
// w1 = std::move(w2);//移动赋值