文章目录
- 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);
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
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);//移动赋值