前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能指针在面试中得重要地位!

智能指针在面试中得重要地位!

作者头像
用户9831583
发布2022-12-04 16:05:33
9860
发布2022-12-04 16:05:33
举报
文章被收录于专栏:码出名企路码出名企路

第4章 智能指针

//智能指针式对裸指针进行包装,避免很对再使用裸指针时会遇到陷阱,为管理动态分配对象的生命周期设计

//通过保证这样的对象在适当的时机以适当的方式析构来防止内存泄漏。

条款18:使用 std::unique_ptr管理具备专属所有权的资源

//http://c.biancheng.net/view/7909.html

//宗旨:std::unique_ptr是首先,和裸指针有相同的尺寸,甚至可以在内存和时钟周期紧张场合下使用

//情况1:std::unique_ptr是个只移动型别,不可以复制

//移动一个std::unique_ptr会将所有权从源指针移动到目标指针,源指针被置为空

//如果复制了一个 std::unique_ptr,会得到两个指向同一个资源的 std::unique_ptr,这两者都认为自己拥有该资源,需要析构两次

代码语言:javascript
复制
std::unique_ptr<double> data(new double);
//std::unique_ptr<double> data_(data);//错误,堆内存不共享
std::unique_ptr<double> data__(std::move(data));//正确,调用移动构造函数

//情况2:常见用法:在对象继承中作为工厂函数的返回型别

//以下函数会在堆上分配一个对象并且返回一个指到它的指针,并当不再需要该对象时,由调用者复制删除

//std::unique_ptr被析构时,又会自动对其所指向的对象实施delete

代码语言:javascript
复制
//std::unique_ptr被析构时,又会自动对其所指向的对象实施delete
class Investment{
    public:
        Investment(){
            cout<<"base Inv"<<endl;
        }
        virtual ~Investment(){
            cout<<"base del "<<endl;
        }
};
class Stock:public Investment{
    public:
        Stock(){
            cout<<"con stock "<<endl;
        }
        ~Stock(){
            cout<<"del stock"<<endl;
        }
};
class Bond:public Investment{

};
class RealEstate:public Investment{

};
//声明如下:返回std::unique_ptr 指到根据指定实参创建的对象
template<typename... Ts>
std::unique_ptr<Investment> makeInvestmen(Ts&&... params);
//定义如下:自定义的析构器
Investment* m_Inv = nullptr;
void makeLogEntry(Investment* pInvestment)
{
    m_Inv = pInvestment;
}
auto delInvmt = [](Investment* pInvestment){
    makeLogEntry(pInvestment);
    cout<<"delInvmt stock"<<endl;
    delete pInvestment;//通过一个基类指针删除派生类对象,基类中必须具备一个虚析构函数
};
//改进的返回值型别
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestmen2(Ts&&... params){
    //调用自定义的析构,其型别必须被指定为 std::unique_ptr的第二个实参的型别,本例为 delInvmt的型别
    //先创建一个空的 std::unique_ptr,使它指向适当型别对象,然后返回,delInvmt作为构造函数的第二个实参,和pInv关联起来
    std::unique_ptr<Investment,decltype(delInvmt)> pInv(nullptr,delInvmt);//调用自定义的析构
    //创建一个 Stock型别对象
    if(1){
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if(2){
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if(3){
        //直接使用 new运算符的结果赋给 std::unique_ptr不会通过编译,因为这会形成从裸指针到智能指针的隐式转换型别,C++11禁止
        //因此需要使用 reset来指定让 pInv获取从使用 new 运算符产生的对象的所有权
        //并且对每一次 new 运算符的调用结果,使用 std::forward将实参完美转发给 makeInvestment,可以使得所创建对象的构造函数能够获得调用者提供的所有信息
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }

    return pInv;

}

//情况3:C++4 有了函数返回型别推导,可以改进以上设计,变得更加简单和封装性更好

代码语言:javascript
复制
template<typename... Ts>
auto makeInvestmen3(Ts&&... params){
    //使用无状态的 lambda表达式作为自定义析构器
    auto delInvmt = [](Investment* pInvestment){
        //自定义析构位于makeInvesment内部了
        makeLogEntry(pInvestment);
        delete pInvestment;
    };

    std::unique_ptr<Investment,decltype(delInvmt)> pInv(nullptr,delInvmt);//调用自定义的析构
    //后面部分同上了
};

//情况4:返回值尺寸

//比较情况2:设计也可以改写成函数

代码语言:javascript
复制
void delInvmt2(Investment* pInvestment){
    makeLogEntry(pInvestment);
    delete pInvestment;
}
//返回值尺寸等于 Investment*的尺寸 加上至少函数指针的尺寸
template<typename... Ts>
std::unique_ptr<Investment,void (*)(Investment*)> makeInvestmen4(Ts&&... params);

客户端测试

代码语言:javascript
复制
int main()
{   
    //测试2:
    //auto pInvestment = makeInvestmen();//只声明没有定义,以下是无法通过编译的
    auto pInvestment = makeInvestmen2();//OK

    //测试 n: 将 std::unique_ptr型别的对象转换为std::shared_ptr型别
    std::shared_ptr<Investment> sp = makeInvestmen2();//共享所有权和专属所有权的转换
    
    //管理一般型别
    std::unique_ptr<int> aa(new int(4));
    //管理动态数组
    std::unique_ptr<int[]> bb(new int[5] {1,2,3,4,5});
    bb[0] = 0;//重载了 operator[]

    return 0;
}

// 要点速记

// • std: : unique _ptr 是小巧、高速的、具各只移型别的智能指针,对托管资源

// 实施专属所有权语义

// • 默认地,资源析构采用 delete 运算符来实现,但可以指定自定义删除器

// 有状态的删除器和采用函数指针实现的删除器会增加 std::unique_ptr

// 别的对象尺寸

// • std::unique_ptr 转换成 std: : shared _ptr 是容易实现的

//来自:C++11 unique_ptr智能指针详解 (biancheng.net)

成员函数名

功 能

operator*()

获取当前 unique_ptr 指针指向的数据。

operator->()

重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。

operator =()

重载了 = 赋值号,从而可以将 nullptr 或者一个右值 unique_ptr 指针直接赋值给当前同类型的 unique_ptr 指针。

operator

重载了 [] 运算符,当 unique_ptr 指针指向一个数组时,可以直接通过 [] 获取指定下标位置处的数据。

get()

获取当前 unique_ptr 指针内部包含的普通指针。

get_deleter()

获取当前 unique_ptr 指针释放堆内存空间所用的规则。

operator bool()

unique_ptr 指针可直接作为 if 语句的判断条件,以判断该指针是否为空,如果为空,则为 false;反之为 true。

release()

释放当前 unique_ptr 指针对所指堆内存的所有权,但该存储空间并不会被销毁。

reset(p)

其中 p 表示一个普通指针,如果 p 为 nullptr,则当前 unique_ptr 也变成空指针;反之,则该函数会释放当前 unique_ptr 指针指向的堆内存(如果有),然后获取 p 所指堆内存的所有权(p 为 nullptr)。

swap(x)

交换当前 unique_ptr 指针和同类型的 x 指针。

条款19:使用std::shared_ptr管理具备共享所有权的资源

//宗旨:shared_ptr可以拥有一个自动运作得系统,类似垃圾回收,也能应用到所有资源并且具备可预测时序 类似析构函数

//std::shared+ptr 可以通过访问某资源的引用计数来确定是否自己是最后一个指涉及到该资源的。引用计数是与资源关联的值

//用来记录跟踪指涉到该资源的 std:shared_ptr的数量,其构造函数会使计数递增,析构函数使计数递减,如果std::shared_ptr

//在实施一次递减后引用计数变成了零,即不再有 shared_ptr指涉到该资源,则std::shared_ptr会析构

/**

引用计数带来性能影响:

1,std::shared_ptr的尺寸使裸指针的两倍,其内部包含一个指涉到该资源的裸指针,也包含一个指涉到该资源的引用计数的裸指针

2,引用计数的内存必须动态分配

3,引用计数的递增和递减必须使原子操作,因为在不同的线程中可能存在并发的读写器,一个线程在析构,一个在复制,原子操作比非原子操作慢

*/

/**

移动构造函数 与 复制构造函数的区别:

从一个 已有 std::shsred_ptr移动构造一个新的 std::shared_ptr会将 源 std::shared_ptr置空,这意味着一但新的 std::shared_ptr 产生后,原有的 std::shared将不再指涉到其资源,结果是不需要进行任何引用计数操作。因此,移动比复制要快!

*/

//情况1:std::shared_ptr也使用 delete运算符作为默认资源析构机制,同样支持自定义析构器,与 std::unique_ptr不同的是,后者析构器的型别是智能指针型别的一部分,前者却并非如此

代码语言:javascript
复制
class Widget{
    public:
        //针对问题2的涉及
        void process();

};
//自定义析构器
auto loggingDel = [](Widget *pw){
    delete pw;
};
//析构器型别是智能指针型别的一部分
std::unique_ptr<Widget,decltype(loggingDel)> upw(new Widget,loggingDel);
//析构器型别不是智能指针型别的一部分
std::shared_ptr<Widget> spw(new Widget,loggingDel);

//不同点1:

//虽然自定义析构器的型别不同,但是 std::shared_ptr型别却是一样的

//自定义析构器各有不同的型别

代码语言:javascript
复制
auto customDel1 = [](Widget *pw){

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

};
std::shared_ptr<Widget> pw1(new Widget,customDel1);
std::shared_ptr<Widget> pw2(new Widget,customDel2);
//pw1和pw2具有同一型别 ,因此可以这样操作
std::vector<std::shared_ptr<Widget>> vpw{pw1, pw2
//但是请注意:对于具有不同自定义析构器型别的 std::unique_ptr来说,以上这些均无法实现
//因为自定义析构器的型别会影响 std::unqie_ptr的型别

//不同点2:

//自定义析构器不会改变 std::shared_ptr的尺寸,其对象尺寸都相当于裸指针的两倍。

//注意自定义析构器可能是函数对象,函数对象可以包含任意数量的数据,这意味着它们的尺寸可能是任意大小

//std::shared_ptr如何能够在不使用更多内存的前提下,指涉到任意尺寸的析构器?

/**

std::shared_ptr不得不使用更多的内存,但是该部分内存却不属于 std::shared_ptr对象的一部分,它位于堆上:控制块

std::shared_ptr

指涉到 T 型别的对象的指针 ----------------> T型别对象

指涉到控制块的指针 ------------------------> 控制块

引用计数

弱计数

其他数据(例如,自定义删除器,分配器等)

控制块的创建遵循以下规则:

1,std::make_shared 总是创建一个控制块,它会生产出一个用来指涉到的新对象,因此在调用它的时刻,不会有针对该对象的而控制块存在

2,从具备专属所有权的指针(std::unique_ptr或 std::auto_ptr指针)出发构造一个 std::shared_ptr时,会创建一个控制块

3,std::shared_ptr构造函数使用裸指针作为实参来调用时,它会创建一个控制块。

*/

//问题1:

//从同一个裸指针出发来构造不止一个 std::shared_ptr的话,会出现未定义的行为

//因为,这样依赖被指涉到的对象将会有多重的控制块,多重的控制块意味着多重的引用计数,而多重的引用计数意味着该对象被析构多次

//行不通:

代码语言:javascript
复制
    auto pw = new Widget;//pw是裸指针
    std::shared_ptr<Widget> spw1(pw,loggingDel);//为*pw创建一个控制块 同时创建一个引用计数
   // std::shared_ptr<Widget> spw2(pw,loggingDel);//为*pw创建了第二个控制块,又创建一个引用计数
    //这样,*pw就有了两个引用计数,而每个引用计数最终都会变为零,从而导致 *pw 被析构两次,第二次析构就会引发未定义行为  

//因此可以得到两个结论:

/**

1,尽可能避免将裸指针传递给一个 std::shared_ptr的构造函数,替代手法是使用 std::make_shared,但是使用了自定义析构器,无法用std::make_shared

2,如果必须将一个裸指针传递给std::shared_ptr的构造函数,直接传递 new 运算符的结果,而非传递一个裸指针变量

*/

代码语言:javascript
复制
    
    //改进问题1:
    std::shared_ptr<Widget> spw11(new Widget,loggingDel);//直接传递 new 表达式
    std::shared_ptr<Widget> spw22(spw11);//spw22使用的是和spw11同一个控制块

//问题2:

//使用 裸指针变量作为 std::shared_ptr的构造函数实参时,会有一种令人吃惊的方式导致涉及 this指针的多重控制块

//假设程序使用 std::shared_ptr来托管 Widget对象,并且有哥数据结构来追踪被处理的widget

代码语言:javascript
复制
std::vector<std::shared_ptr<Widget>> processedWidgets;
void Widget::process()
{
    //处理对象本身
    //将处理完的Widget加入链表,裸指针 this传入一个 std::shared_ptr容器
    //由此构造的 std::shared_ptr将为其所指涉到的 Widget型别的对象 *this 创建一个新的控制块
    //如果在已经指涉到该 Widget型别的对象的成员函数外部再套一层 std::shared_ptr的话,未定义行为就出现了
    processedWidgets.emplace_back(this);
}

//改进问题2:

//当你希望一个托管到 std::shared_ptr的类能够安全地由 this指针创建一个 std::shared_ptr时候,将如下设计

代码语言:javascript
复制
class WidgetOK:public std::enable_shared_from_this<WidgetOK>
{
    void process();

};
//std::enable_shared_from_this是一个基类模板,型别形参总是其派生类地类名,它定义了一个成员函数,会创建一个
//std::shared_ptr指涉到当前对象,但同时不会重复创建控制块,安全实现如下
void WidgetOK::process()
{
    //将指涉到当前对象地 std::shared_ptr加入
    processedWidgets.emplace_back(shared_from_this());
}

// 要点速记

// • std: : shared _ptr 提供方便的手段,实现了任意资源在共享所有权语义下进

// 行生命周期管理的垃圾回收

// • std: : unique _ptr 相比, std::shared_ptr 的尺寸通常是裸指针尺寸的两

// 倍,它还会带来控制块的开销,并要求原子化的引用计数操作

// • 默认的资源析构通过 delete 运算符进行,但同时也支持定制删除器。删除

// 器的型别对 std::shared_ptr 的型别没有影响

// • 避免使用裸指针型别的变量来创建 std: : shared _ptr 指针

条款20:对于类似 std::shared_ptr但有可能空悬地指针使用 std::weak_ptr

//主旨:std::weak_ptr可以处理指涉对象可能已被析构的指针,可以跟踪指针何时空悬,判断其所指涉到的对象已不复存在来处理问题

//情况1:创建 std::weak_ptr,其实就是 std::shared_ptr的扩充

/**

它是通过 std::shared_ptr 来创建,当使用 std::shared_ptr完成初始化std::weak_ptr的时刻,两者就指涉到相同的位置了

但是 std::weak_ptr并不影响所指涉的对象的引用计数

*/

代码语言:javascript
复制
//spw构造完成后,指涉到 Widget的引用计数置为 1
class Widget{

};
auto spw = std::make_shared<Widget>();
//wpw和spw指涉到同一个 Widget,引用计数保持为1
std::weak_ptr<Widget> wpw(spw);
//引用计数变为0时,Widget对象被析构,wpw空悬
//spw = nullptr;

测试

代码语言:javascript
复制
    //测试1 
    //std::weak_ptr空悬 不再指涉到任何对象
    spw = nullptr;
    if(wpw.expired())
    {
        std::cout<<"NULL pointer"<<std::endl;
    }

//但是,我们想得到的是:如何校验 std::weak_ptr是否失效

//在未失效的条件下提供对指涉到的对象的访问:这个操作通过由 std::weak_ptr创建std::shared_ptr来实现

//方式一:std::weak_ptr::lock 返回一个 std::shared_ptr

代码语言:javascript
复制
//方式一:std::weak_ptr::lock 返回一个 std::shared_ptr
std::shared_ptr<Widget> spw1 = wpw.lock();//如 wpw失效,则 spw1为空
auto spw2 = wpw.lock();// 同上

//方式二:用 std::weak_ptr作为实参来构造 std::shared_ptr,这样 std::weak_ptr失效的话,抛出异常

代码语言:javascript
复制
//方式二:用 std::weak_ptr作为实参来构造 std::shared_ptr,这样 std::weak_ptr失效的话,抛出异常
std::shared_ptr<Widget> spw3(wpw);//如 wpw失效,抛出 std::bad_weak_ptr型别的异常

//说了那么多,std::weak_ptr用来干什么呢?

//用处一:缓存的对象不再有用时将其删除

代码语言:javascript
复制
//用处一:缓存的对象不再有用时将其删除
class WidgetID{

};
std::unique_ptr<const Widget> loadWidget(WidgetID id)
{
     std::unique_ptr<const Widget> str;

     return str;
}
//重新设计一个工厂函数:满足
/**
1,调用者可以获取指涉到缓存对象的智能指针,调用者也当然应该决定这些对象的生产期
2,缓存管理器需要能够校验指涉到这些对象的指针何时空悬,用完对象,就会被析构,相应的缓存条目会空悬
3,因此,应该缓存 std::shared_ptr ,可以检测空悬的指针,意味着,该工厂的返回值为 std::shared_ptr
   因为只有当对象的生产期托管给 std::shared_ptr时,std::weak_ptr才能检测空悬
*/
// std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
// {  
//     //unordered_map 不能使用自定义的类型作为 key值,因此底层时候的是 std::hash
//     //map使用 std::less可以使用自定义类型,里面没有WidgetID的比较函数
//     static std::map<WidgetID, std::weak_ptr<const Widget> > cache;
//     //objPtr的型别是 std::shared_ptr,指涉到缓存对象,如果对象不再缓存中,则返回空指针
//     auto objPtr = cache[id].lock();

//     //如果对象不在缓存中,则加载,并缓存
//     if(!objPtr){
//         objPtr = loadWidget(id);
//         cache[id] = objPtr;
//     }

//     return objPtr;
// }

//用处二:观察者模式

/**

需要保证观察者被析构以后,主题不会去访问它,让每个主题持有一个容器来放置指涉到其观察者的 std::weak_ptr

以便主题在使用某个指针之前,能够先确定它是否空悬。

*/

代码语言:javascript
复制
//观察者接口 通知方法
class IObsercer
{
    public:
        virtual void notify() const = 0;
        virtual ~IObsercer(){}
};
//具体观察者
class Observer:public IObsercer
{
    public:
        Observer(const std::string& message):m_message(message){}
        void notify() const{
            printf("%s",m_message.c_str());
        }
    private:
        std::string m_message;
};
//主题类 通知观察者
class Subject
{
    public:
        //shared_ptr保证 observer是有效的
        void registerObserver(const std::shared_ptr<IObsercer> &o)
        {
            std::lock_guard<std::mutex> guard(m_observersMutex);
            m_observers.push_back(o);
        }
        void unregisterobserver(const std::shared_ptr<IObsercer> &o)
        {
            std::lock_guard<std::mutex> guard(m_observersMutex);
            //
        }
        //通知观察者一些事情
        void doNotify()
        {
            std::lock_guard<std::mutex> guard(m_observersMutex);
            std::for_each(m_observers.cbegin(),m_observers.cend(),
                [](const std::weak_ptr<IObsercer> &o)
                {
                    auto observer = o.lock();
                    if(observer)
                    {
                        observer->notify();
                    }
                });

                //删除已经是空的观者者对象
                m_observers.erase(std::remove_if(m_observers.begin(),m_observers.end(),
                    [](const std::weak_ptr<IObsercer>& o)
                    {
                        return o.expired();
                    }), m_observers.end());
        }
    
    private:
        std::vector<std::weak_ptr<IObsercer>> m_observers;
        std::mutex m_observersMutex;
};

测试:

代码语言:javascript
复制
   //测试2:
    Subject subject;
    auto observerHello = std::make_shared<Observer>( "Hello world" );
    subject.registerObserver( observerHello );
    {
        // Create a scope to show unregistration.
        auto observerBye = std::make_shared<Observer>( "Good bye" );
        subject.registerObserver( observerBye );

        subject.doNotify();
    }
    printf( "%s\r\n", "Observer good bye is now be destructed" );
    subject.doNotify();

//用法3:

//A和C共享B的所有权

/***

A --std::shared_ptr ---> B <-- std::shared_ptr -- C

<-- ??????? ----

????处应该填写?

1,裸指针:如A 被析构,C仍然指涉到 B,B将保存着 指涉到 A的空悬指针,B却检测不出来,所以B 可能无意中提领这个空悬指针

2,std::shared_ptr :AB环路保持,阻止了 A和B被析构,两者始终保持彼此的引用计数为 1 ,资源得不到回收,内存泄漏

3,std::weak_ptr:避免上述两个问题,如 A被析构,B的回指指针将会空悬,B能检测到这一点。并且,B持有的指针不会影响A的引用计数

因此当 std::shared_ptr不再指涉到A时,不会阻止A被析构

*/

// 要点速记

// • 使用 std: :weak_ptr 来代替可能空悬的 std:: shared_ptr

// • std: :weak_ptr 可能的用武之地包括缓存,观察者列表, 以及进免

// std: : shared _ptr 指针环路

条款21:优先选用std::make_unique和std::make_shared, 而非直接使用 new

//结论:相对于直接使用 new 表达式,优先选用 make 系列函数

//C++11 std::make_shared

//C++14 std::make_unique , 利用C++11实现一个基础版本的 std::make_unique

//将形参向待创建对象的构造函数作了一次完美转发,并返回一个指涉到该对象的智能指针

//这个形式的函数不支持数组和自定义析构器

代码语言:javascript
复制
template<typename T,typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

//好处一:避免了代码冗余 写两遍

代码语言:javascript
复制
//好处一: 避免了代码冗余 写两遍
class Widget{

};
//使用make系列函数
auto upw1(std::make_unique<Widget>());
//不使用 make系列: 重复撰写型别两遍
std::unique_ptr<Widget> upw2(new Widget);

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

//好处二:异常安全处理

代码语言:javascript
复制
//好处二:异常安全处理
//假设有一个函数依据某种优先级来处理一个 Widget对象
void processWidget(std::shared_ptr<Widget> spw, int priority){
    
}
//同时,假设有一个函数用来计算相对优先级别
int computePriority(){
    return 1;
}
代码语言:javascript
复制
//测试二:

    //调用该函数
    processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
    //以上由于 new得存在会发生资源泄漏,具体原因如下:
    // 运行期间,传递给函数得实参必须再函数调用被发起之前完成评估求值,因此,再processWidget得调用过程中,下列事件必须再 processWidget开始执行前发生
    /*
    1,表达式 new Widget 必须先完成评估求值,一个Widget对象必须先在堆上创建
    2, 由 new 产生得裸指针得托管对象 std::shared_ptr<Widget> 得构造函数必须执行
    3, computePriority必须运行

    编译器不必按以上顺序执行,但是必须保证 new完成之后才 std::shared_ptr,因为前者是后者得传参,如果顺序变成了这样
    1,3,2
    并且在运行期间 3 产生了异常,那么由 1动态分配得 Widget会被泄露,因为他不会被存储到 2 中去了,没人释放了

    因此,使用 std::make_shared可以避免以上问题
    */

    //改进二
    processWidget(std::make_shared<Widget>(), computePriority());

    /*
    因为:不管哪个先被调用,总能析构掉
    1, std::make_shared首先被调用,指涉到动态分配得 Wiget得裸指针会在 computePriority被调用前被安全存储在返回得 std::shared_ptr对象中,即使compute产生异常,
    std::shared_ptr得析构函数也能知道他拥有得Widget已被析构
    2, 如果 computePriority先被调用并产生了异常,std::make_shared将不会被调用,因此也不用担心
    */

    //同理:std::unique_ptr和 std::make_unique 一样得一对

//好处三:性能提升

代码语言:javascript
复制
//好处三:性能提升
std::shared_ptr<Widget> spww(new Widget);
//直接使用 new ,除了要为 Widget 进行一次内存分配,还要为与其相关联得控制块再进行一次内存分配
auto spwww = std::make_shared<Widget>();
//一次内存分配,分配单块内存即保存 Widget 对象又保存与其相关联得控制块

//结论:相对于直接使用 new 表达式,优先选用 make 系列函数

//但是,什么情境下,不使用make系列函数呢?

//1, make系列函数不允许使用自定义析构器

代码语言:javascript
复制
//1, make系列函数不允许使用自定义析构器
//但是 std::unique 和 std::shared_ptr却可以
//自定义析构器
auto widgetDeleter = [](Widget* pw){

};
//创建自定义析构器得智能指针
std::unique_ptr<Widget,decltype(widgetDeleter)> upw(new Widget,widgetDeleter);
std::shared_ptr<Widget> spw(new Widget,widgetDeleter);

//2, () 和 {} 得用法特别

代码语言:javascript
复制
//2, () 和 {} 得用法特别
//包含10个元素,每个元素都是20
auto upv = std::make_unique<std::vector<int>>(10,20);
auto spv = std::make_shared<std::vector<int>>(10,20);

//如果用 {}则必须这样做
//创建 std::initializer_list型别得对象
auto initList = {10,20};
//利用 std::initializer_list型别得构造函数构造 std::vector
auto spv1 = std::make_shared<std::vector<int>>(initList);

// 相比于直接使用 new 表达式, make 系列函数消除了重复代码、改进了异常

// 安全性,并且对于 std: :make_shared std: : all coated_ shared 而言,生

// 成的目标代码会尺寸史小、速度更快

// • 不适于使用 make 系列函数的场景包括需要定制删除器,以及期望直接传递

// 大括号初始化物

// • 对于 std::shared_ptr 不建议使用 make 系列函数的额外场景包括:@自

// 定义内存管理的类;@内存紧张的系统、非常大的对象、以及存在比指涉

// 到相同对象的 std: :shared_ptr 生存期史久的 std: :weak_ptr

条款22:使用 Pimpl习惯用法时,将特殊成员函数得定义放到实现文件中

//Pimpl :pointer to implementation 指涉到实现得指针

//实现技巧是把某类得数据成员用一个指涉到某实现类 或结构体得指针代替,之后把原来再主类中得数据成员放置到实现类中

//并通过指针间接访问这些数据成员

/**

Pimpl 习惯用法:

第1 部分,是声明 个指针型别的数据成员,指涉到 个非完整型别,

第2 部分,是动态分配和回收持有从前在原始类里的那些数据成员的对象,而分配和回收代码则放在实现文件中。

*/

//如下例子:

代码语言:javascript
复制
//gadget.h
class Gadget{
    public:
        string name;
};
//Widget.h
class Widget{
    public:
        Widget();
    
    private:
        std::string name;
        std::vector<double> data;
        Gadget g1, g2, g3;
};

//以上设计存在得问题是:

//Widget.h得头文件依赖 gadget.h得头文件,后者内容改变,前者需要重新编译

//改进:Pimpl习惯用法:用一个指涉到已声明但未定义得结构得裸指针来替换 Widget得数据成员

代码语言:javascript
复制
//改进:Pimpl习惯用法:用一个指涉到已声明但未定义得结构得裸指针来替换 Widget得数据成员
class Widget1{
    public:
        Widget1();
        ~Widget1();

        Widget1(const Widget1& rhs);
        Widget1& operator=(const Widget1& rhs);

        //仅仅声明
        Widget1(Widget1&& rhs);
        Widget1& operator=(Widget1&& rhs);

    //private:
        //声明实现结构体以及指涉到他得指针
        //C++98
        struct Impl;
        Impl *pImpl;

        //c++11 智能指针用起来,而不是裸指针
        std::unique_ptr<Impl> pImpl11;
};

//已声明但是未定义得型别成为非完整型别 Widget::Impl就是这样得型别

//实现再 Widget.cpp中

代码语言:javascript
复制
//Widget::Impl得实现包括此前在 Widget中得数据成员
struct Widget1::Impl{
    std::string name;
    std::vector<double> data;
    Gadget g1,g2,g3;
};
//为本Widget对象分配数据成员所需内存
//C++98
// Widget1::Widget1():pImpl(new Impl){

// }
//C++11 :
Widget1::Widget1():pImpl11(std::make_unique<Impl>())
{
    //使用 std::make_unqiue创建std::unique_ptr
}
//C++98
//为本 Widget对象析构数据成员
Widget1::~Widget1(){
    delete pImpl;
}
//C++11 不需要析构函数了

//复制构造函数
Widget1::Widget1(const Widget1& rhs):pImpl11(std::make_unique<Impl>(*rhs.pImpl11)){

}
//复制赋值运算符
Widget1& Widget1::operator=(const Widget1& rhs)
{
    *pImpl11 = *rhs.pImpl11;
    return *this;
}

客户端:

代码语言:javascript
复制
int main()
{
    //测试1:
    // Widget1 *wd = new Widget1();
    // wd->pImpl->name = "liyy";
    // wd->pImpl->data = {1,2,3};
    // wd->pImpl->g1.name = "liyshu";

    //测试2:
    auto wdd = std::make_unique<Widget1>();
    wdd->pImpl11->name = "liyy";
    wdd->pImpl11->data = {1,2,3};
    wdd->pImpl11->g1.name = "liyshu";


}

// • Pimp! 惯用法通过降低类的客户和类实现者之间的依赖性,减少了构建遍数

// • 对于采用 std: :unique_ptr 来实现的 plmpl 指针,须在类的头文件中声明

// 特种成员函数,但在实现文件中实现它们 即使默认函数实现有着 正确行为,

// 也必须这样做

// • 上述建议仅适用于 std::unique_ptr, 但并不适用 std::shared_ptr

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码出名企路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第4章 智能指针
    • 条款18:使用 std::unique_ptr管理具备专属所有权的资源
      • 条款19:使用std::shared_ptr管理具备共享所有权的资源
        • 条款20:对于类似 std::shared_ptr但有可能空悬地指针使用 std::weak_ptr
          • 条款21:优先选用std::make_unique和std::make_shared, 而非直接使用 new
            • 条款22:使用 Pimpl习惯用法时,将特殊成员函数得定义放到实现文件中
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档