在本章伊始,我们讨论了裸指针在某些场景下如何导致错误与内存泄漏:例如函数提前返回或抛出异常时,指针未能得到正确释放。
#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
能正确区分 delete
与 delete[]
,因此可同时用于单个对象与数组。
最佳实践
优先使用 std::array
、std::vector
或 std::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 的两种常见方式
多个 unique_ptr 管理同一资源
Resource* r{ new Resource() }; std::unique_ptr<Resource> p1{ r }; std::unique_ptr<Resource> p2{ r }; // 未定义行为:双重删除
手动删除托管对象
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;
}