C++11中有unique_ptr、shared_ptr与weak_ptr等智能指针(smart pointer),定义在中。可以对动态资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。

1. auto_ptr

auto_ptr主要是用来解决资源自动释放的问题,比如如下代码:

void Function()
{
    Obj*p = new Obj(20);
    ...
    if (error occor)
    throw ... or retrun;
    delete p;
}

在函数遇到错误之后,一般会抛异常,或者返回,但是这时很可能遗漏之前申请的资源,即使是很有经验的程序员也有可能出现这种错误,而使用auto_ptr会在自己的析够函数中进行资源释放。也就是所说的RAII

使用auto_ptr代码如下

void Function()
{
    auto_ptr<Obj> ptr( new Obj(20) );
    ...
    if (error occur)
    throw exception...
}

这样无论函数是否发生异常,在何处返回,资源都会自动释放。需要提一下的是这是一个被c++11标准废弃的一个智能指针,为什么会被废弃,先看一下下面的代码:

auto_ptr<Obj> ptr1( new Obj() );
ptr1->FuncA();
auto_ptr<Obj> ptr2 = ptr1;
ptr2->FuncA();
ptr1->FuncA();  // 这句话会异常

为什么在把ptr1复制给ptr2之后ptr1再使用就异常了呢?
这也正是他被抛弃的主要原因。因为auto_ptr复制构造函数中把真是引用的内存指针进行的转移,也就是从ptr1转移给了ptr2,此时,ptr2引用了Obj内存地址,而ptr1引用的内存地址为空,此时再使用ptr1就异常了。

2. unique_ptr

unique_ptr持有对对象的独有权,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

std::unique_ptr<int> up1(new int(11));   // 无法复制的unique_ptr
//unique_ptr<int> up2 = up1;        // err, 不能通过编译
std::cout << *up1 << std::endl;   // 11

std::unique_ptr<int> up3 = std::move(up1);    // 现在p3是数据的唯一的unique_ptr

std::cout << *up3 << std::endl;   // 11
//std::cout << *up1 << std::endl;   // err, 运行时错误
up3.reset();            // 显式释放内存
up1.reset();            // 不会导致运行时错误
//std::cout << *up3 << std::endl;   // err, 运行时错误

std::unique_ptr<int> up4(new int(22));   // 无法复制的unique_ptr
up4.reset(new int(44)); //"绑定"动态对象
std::cout << *up4 << std::endl; // 44

up4 = nullptr;//显式销毁所指对象,同时智能指针变为空指针。与up4.reset()等价

std::unique_ptr<int> up5(new int(55));
int *p = up5.release(); //只是释放控制权,不会释放内存
std::cout << *p << std::endl;
//cout << *up5 << endl; // err, 运行时错误
delete p; //释放堆区资源

3. shared_ptr

shared_ptr允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除

std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::cout << "cout: " << sp2.use_count() << std::endl; // 2

std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22

sp1.reset(); // 显示让引用计数减一
std::cout << "count: " << sp2.use_count() << std::endl; // count: 1

std::cout << *sp2 << std::endl; // 22

4. weak_ptr

weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载 * 和 -> 但可以使用lock获得一个可用的shared_ptr对象

weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。

注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权。

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <memory>

void check(std::weak_ptr<int> &wp)
{
    std::shared_ptr<int> sp = wp.lock(); // 转换为shared_ptr<int>
    if (sp != nullptr)
    {
        std::cout << "still: " << *sp << std::endl;
    } 
    else
    {
        std::cout << "still: " << "pointer is invalid" << std::endl;
    }
}


void mytest()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象
    // std::cout << *wp << std::endl; 编译不过

    std::cout << "count: " << wp.use_count() << std::endl; // count: 2
    std::cout << *sp1 << std::endl; // 22
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22
    
    sp1.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 1
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22

    sp2.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 0
    check(wp); // still: pointer is invalid
}

5. ComPtr

IUnknown接口类

DirectX11的API是由一系列的COM组件来管理的,这些前缀带I的接口类最终都继承自IUnknown接口类。IUnknown的三个方法如下:

方法 描述
IUnknown::AddRef 内部引用计数加1。在每次复制了一个这样的指针后,应当调用该方法以保证计数准确性
IUnknown::QueryInterface 查询该实例是否实现了另一个接口,如果存在则返回该接口的指针,并且对该接口的引用计数加1
IUnknown::Release 内部引用数减1。只有当内部引用数到达0时才会真正释放

在实际的使用情况来看,通常我们几乎不会使用第一个方法。而用的最多的就是第三个方法了,每次用完该实例后,我们必须要使用类似下面的宏来释放:

#define ReleaseCOM(x) { if(x){ x->Release(); x = nullptr; } }

而且如果出现了忘记释放某个接口指针的情况话,内存泄漏的提醒就有可能够你去调试一整天了。

ComPtr智能指针

为了解决上述问题,从繁杂的人工释放中解脱,在本教程中大量使用了ComPtr智能指针。而且在龙书12的教程源码中也用到了该智能指针。该智能指针可以帮助我们来管理这些COM组件实现的接口实例,而无需过多担心内存的泄漏。该智能指针的大小和一般的指针大小是一致的,没有额外的内存空间占用。所以本教程可以不需要用到接口类ID3D11Debug来协助检查内存泄漏。

使用该智能指针需要包含头文件wrl/client.h,并且智能指针类模板ComPtr位于名称空间Microsoft::WRL内。

首先有五个比较常用的方法需要了解一下:

方法 描述
Get 该方法返回T*,并且不会触发引用计数加1,常用在COM组件接口的函数输入
GetAddressOf 该方法返回T**,常用在COM组件接口的函数输出
Reset 相当于先调用Reset方法,再调用GetAddressOf方法获取T**,常用在COM组件接口的函数输出,适用于实例可能会被反复构造
ReleaseAndGetAddressOf 可以和nullptr,或者另一个ComPtr实例进行比较
As 一个模板函数,可以替代IUnknown::QueryInterface的调用,需要传递一个ComPtr实例的地址

然后是一些运算符重载的方法:

运算符 描述
& 相当于调用了ReleaseAndGetAddressOf方法,不推荐使用
-> 和裸指针的行为一致
= 不要将裸指针指向的实例赋给它,若传递的是ComPtr的不同实例则发生交换
==和!= 可以和nullptr,或者另一个ComPtr实例进行比较

注意:大致在比10.0.16299.0更早的Windows SDK版本中,ComPtr使用了一个RemoveIUnknownBase类模板将IUnknown的三个接口都设为了private,以防止用户直接操作这些方法,这也就使得ComPtr无法直接使用COM组件的QueryInterface方法。因此,使用ComPtr::As是一种合适的选择。

个人建议,在使用该智能指针后就应该要避免使用IUnknown提供的三个接口方法来进行操作。虽然替换成ComPtr后代码量变长了,但是带来的收益肯定比你自己花费大量时间在检查释放内存上强的多。下面的D3DApp将所有COM组件指针都换成了ComPtr:

class D3DApp
{
public:
    D3DApp(HINSTANCE hInstance);        // 在构造函数的初始化列表应当设置好初始参数
    virtual ~D3DApp();

    HINSTANCE AppInst()const;           // 获取应用实例的句柄
    HWND      MainWnd()const;           // 获取主窗口句柄
    float     AspectRatio()const;       // 获取屏幕宽高比
    int Run();                          // 运行程序,进行游戏主循环

    // 框架方法。客户派生类需要重载这些方法以实现特定的应用需求                                     
    virtual bool Init();                      // 该父类方法需要初始化窗口和Direct3D部分
    virtual void OnResize();                  // 该父类方法需要在窗口大小变动的时候调用
    virtual void UpdateScene(float dt) = 0;   // 子类需要实现该方法,完成每一帧的更新
    virtual void DrawScene() = 0;             // 子类需要实现该方法,完成每一帧的绘制
    virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    // 窗口的消息回调函数

protected:
    bool InitMainWindow();      // 窗口初始化
    bool InitDirect3D();        // Direct3D初始化
    void CalculateFrameStats(); // 计算每秒帧数并在窗口显示

protected:
    HINSTANCE m_hAppInst;        // 应用实例句柄
    HWND      m_hMainWnd;        // 主窗口句柄
    bool      m_AppPaused;       // 应用是否暂停
    bool      m_Minimized;       // 应用是否最小化
    bool      m_Maximized;       // 应用是否最大化
    bool      m_Resizing;        // 窗口大小是否变化
    bool      m_Enable4xMsaa;    // 是否开启4倍多重采样
    UINT      m_4xMsaaQuality;   // MSAA支持的质量等级
    GameTimer m_Timer;           // 计时器

    // 使用模板别名(C++11)简化类型名
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;
    // Direct3D 11
    ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11设备
    ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11设备上下文
    ComPtr<IDXGISwapChain> m_pSwapChain;                  // D3D11交换链
    // Direct3D 11.1
    ComPtr<ID3D11Device1> m_pd3dDevice1;                  // D3D11.1设备
    ComPtr<ID3D11DeviceContext1> m_pd3dImmediateContext1; // D3D11.1设备上下文
    ComPtr<IDXGISwapChain1> m_pSwapChain1;                // D3D11.1交换链
    // 常用资源
    ComPtr<ID3D11Texture2D> m_pDepthStencilBuffer;        // 深度模板缓冲区
    ComPtr<ID3D11RenderTargetView> m_pRenderTargetView;   // 渲染目标视图
    ComPtr<ID3D11DepthStencilView> m_pDepthStencilView;   // 深度模板视图
    D3D11_VIEWPORT m_ScreenViewport;                      // 视口

    // 派生类应该在构造函数设置好这些自定义的初始参数
    std::wstring m_MainWndCaption;                       // 主窗口标题
    int m_ClientWidth;                                   // 视口宽度
    int m_ClientHeight;                                  // 视口高度
};