std::unique_ptr

在本章伊始,我们讨论了裸指针在某些场景下如何导致错误与内存泄漏:例如函数提前返回或抛出异常时,指针未能得到正确释放。

#include <iostream>

void someFunction()
{
    auto* ptr{ new Resource() };

    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        throw 0; // 函数提前返回,ptr 不会被释放!

    // 在此处使用 ptr

    delete ptr;
}

在掌握了移动语义的基础之后,我们重新回到智能指针类这一主题。智能指针的核心职责是:接管用户授予的动态分配资源,并确保该资源在适当时机(通常是智能指针离开作用域时)被正确清理。

因此,智能指针本身绝不应被动态分配。否则,一旦智能指针自身未能正确释放,其托管对象亦无法释放,造成内存泄漏。将智能指针始终分配于栈上(作为局部变量或类的组合成员),便可确保其作用域结束时自动析构,进而保证托管对象被安全回收。


C++11 标准库中的智能指针

C++11 标准库提供了 4 种智能指针类:

  • std::auto_ptr(C++17 已移除)
  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

其中 std::unique_ptr 使用最为广泛,故先予讨论。


std::unique_ptr

std::unique_ptr 是 C++11 取代 std::auto_ptr 的独占型智能指针。
它应仅用于管理不被多个对象共享的动态分配对象,即 std::unique_ptr 对其托管对象拥有唯一所有权
头文件:<memory>

基本用法示例

#include <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
    Resource()  { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // 创建 Resource 并由 std::unique_ptr 接管
    std::unique_ptr<Resource> res{ new Resource() };

    return 0;
} // res 离开作用域,托管对象自动销毁

由于 res 分配于栈上,作用域结束时其析构函数被调用,从而释放所托管的 Resource

std::auto_ptr 的差异

std::unique_ptr 正确实现了移动语义:

#include <iostream>
#include <memory>
#include <utility>

class Resource { /* 同上 */ };

int main()
{
    std::unique_ptr<Resource> res1{ new Resource{} };
    std::unique_ptr<Resource> res2{}; // 初始为空

    std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
    std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

    // res2 = res1; // 编译错误:拷贝赋值被禁用
    res2 = std::move(res1); // 转移所有权,res1 置空

    std::cout << "Ownership transferred\n";
    std::cout << "res1 is " << (res1 ? "null\n" : "null\n");
    std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

    return 0;
}

输出:

Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

std::unique_ptr 禁用拷贝构造与拷贝赋值,若要转移托管资源,必须使用 std::move


访问托管对象

std::unique_ptr 重载了 operator*operator->

  • *ptr 返回托管对象的引用;
  • ptr-> 返回托管对象的指针。

使用前应先检查是否持有资源(布尔转换):

#include <iostream>
#include <memory>

class Resource { /* 同上 */ };

std::ostream& operator<<(std::ostream& out, const Resource&)
{
    return out << "I am a resource";
}

int main()
{
    std::unique_ptr<Resource> res{ new Resource{} };

    if (res) // 隐式转换为 bool
        std::cout << *res << '\n';

    return 0;
}

std::unique_ptr 与数组

std::auto_ptr 不同,std::unique_ptr 能正确区分 deletedelete[],因此可同时用于单个对象数组

最佳实践
优先使用 std::arraystd::vectorstd::string,而非让 std::unique_ptr 管理固定/动态数组或 C 风格字符串。


std::make_unique

C++14 引入 std::make_unique:模板函数,按给定实参构造对象并返回 std::unique_ptr

#include <memory>
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
public:
    Fraction(int n = 0, int d = 1) : m_numerator{ n }, m_denominator{ d } {}

    friend std::ostream& operator<<(std::ostream& out, const Fraction& f)
    {
        return out << f.m_numerator << '/' << f.m_denominator;
    }
};

int main()
{
    auto f1{ std::make_unique<Fraction>(3, 5) };
    std::cout << *f1 << '\n';

    auto f2{ std::make_unique<Fraction[]>(4) }; // 动态数组
    std::cout << f2[0] << '\n';

    return 0;
}

输出:

3/5
0/1

最佳实践
优先使用 std::make_unique 而非手动 new + std::unique_ptr

异常安全细节

C++14 之前,如下写法:

some_function(std::unique_ptr<T>(new T), may_throw());

编译器可自由重排求值顺序,若 may_throw() 抛异常,已分配 T 将泄漏。
std::make_unique 在内部一次性完成对象与智能指针创建,避免此问题。
C++17 起,函数实参求值顺序规定已阻止该漏洞。


函数返回 std::unique_ptr

可按值安全返回 std::unique_ptr

#include <memory>

std::unique_ptr<Resource> createResource()
{
    return std::make_unique<Resource>();
}

int main()
{
    auto ptr{ createResource() };
    // ...
}
  • C++14 及更早:使用移动语义转移所有权;
  • C++17 起:通常触发返回值优化(NRVO),无需移动。

一般不应以指针或引用返回 std::unique_ptr,除非有明确理由。


向函数传递 std::unique_ptr

若函数需取得所有权

按值接收,并用 std::move 传递:

void takeOwnership(std::unique_ptr<Resource> res)
{
    if (res) std::cout << *res << '\n';
} // 此处销毁

int main()
{
    auto ptr{ std::make_unique<Resource>() };
    takeOwnership(std::move(ptr));
}

输出:

Resource acquired
I am a resource
Resource destroyed
Ending program

若函数仅需使用,不取得所有权

  • 直接传托管对象的指针或引用;
  • 通过 get() 获取裸指针:
void useResource(const Resource* res)
{
    std::cout << (res ? *res : "No resource") << '\n';
}

int main()
{
    auto ptr{ std::make_unique<Resource>() };
    useResource(ptr.get());
}

std::unique_ptr 作为类成员

可将 std::unique_ptr 作为类的组合成员,析构时自动释放资源。
若类对象本身未正确销毁(如动态分配后未 delete),则其 std::unique_ptr 成员也不会析构,导致泄漏。


误用 std::unique_ptr 的两种常见方式

  1. 多个 unique_ptr 管理同一资源

    Resource* r{ new Resource() };
    std::unique_ptr<Resource> p1{ r };
    std::unique_ptr<Resource> p2{ r }; // 未定义行为:双重删除
    
  2. 手动删除托管对象

    Resource* r{ new Resource() };
    std::unique_ptr<Resource> p{ r };
    delete r; // 未定义行为:重复删除
    

std::make_unique 可完全避免上述两种误用。


小测验

问题 1
将下列使用裸指针的程序改写为使用 std::unique_ptr

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
public:
    Fraction(int n = 0, int d = 1) : m_numerator{ n }, m_denominator{ d } {}

    friend std::ostream& operator<<(std::ostream& out, const Fraction& f)
    {
        return out << f.m_numerator << '/' << f.m_denominator;
    }
};

void printFraction(const Fraction* ptr)
{
    std::cout << (ptr ? *ptr : "No fraction") << '\n';
}

int main()
{
    auto* ptr{ new Fraction{ 3, 5 } };
    printFraction(ptr);
    delete ptr;
    return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"