深入 C++ 的 unique_ptr

本文深入探讨了C++中的unique_ptr智能指针,包括其如何避免内存泄漏、异常安全性的保证机制、实现原理及陷阱等。通过具体代码示例展示了如何正确使用unique_ptr,并介绍了make_unique函数的作用。

深入 C++ 的 unique_ptr

从异常安全说起

  使用 raw pointer 管理动态内存时,经常会遇到这样的问题:

  • 忘记delete内存,造成内存泄露。
  • 出现异常时,不会执行delete,造成内存泄露。

  下面的代码解释了,当一个操作发生异常时,会导致delete不会被执行:

1
2
3
4
5
6
7
8
9
void func()
{
auto ptr = new Widget;
// 执行一个会抛出异常的操作
func_throw_exception();
delete ptr;
}

  在 C++98 中我们需要用一种笨拙的方式,写出异常安全的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func()
{
auto ptr = new Widget;
try {
func_throw_exception();
}
catch(...) {
delete ptr;
throw;
}
delete ptr;
}

  使用智能指针能轻易写出异常安全的代码,因为当对象退出作用域时,智能指针将自动调用对象的析构函数,避免内存泄露:

1
2
3
4
5
6
void func()
{
std::unique_ptr<Widget> ptr{ new Widget };
func_throw_exception();
}

unique_ptr 的原理

  让我们了解一下unique_ptr的实现细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace std {
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept;
~unique_ptr() noexcept;
T& operator*() const;
T* operator->() const noexcept;
unique_ptr(const unique_ptr &) = delete;
unique_ptr& operator=(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&) noexcept;
unique_ptr& operator=(unique_ptr &&) noexcept;
// ...
private:
pointer __ptr;
};
}

  从上面的代码中,我们可以了解到:

  • unique_ptr内部存储一个 raw pointer,当unique_ptr析构时,它的析构函数将会负责析构它持有的对象。
  • unique_ptr提供了operator*()operator->()成员函数,像 raw pointer 一样,我们可以使用*解引用unique_ptr,使用->来访问unique_ptr所持有对象的成员。
  • unique_ptr并不提供 copy 操作,这是为了防止多个unique_ptr指向同一对象。
  • unique_ptr提供了 move 操作,因此我们可以用std::move()来转移unique_ptr

  很显然,缺省情况下,unique_ptr会使用delete析构对象,不过我们可以使用自定义的 deleter。

1
2
3
4
5
6
7
struct Widget{ };
// ...
auto deleter = []( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
};
unique_ptr<Widget, decltype(deleter)> ptr{ new Widget, deleter };

  当然,我们可以使用 C++11 的 alias template 特性,这样就可以避免指定 deleter 的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Widget{ };
template <typename T>
using uniquePtr = unique_ptr<T, void(*)(T*)>;
void func()
{
uniquePtr<Widget> ptr( new Widget,
[]( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
});
}

  unique_ptr为数组提供了模板偏特化,因此unique_ptr也可以指向数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace std {
template <typename T, typename D>
class unique_ptr<T[], D>
{
public:
// ...
T& operator[]( size_t i ) const;
};
template <typename T>
class default_delete<T[]>
{
public:
// ...
void operator()( T *p ) const; // call delete[] p
};
}

  当unique_ptr指向数组时,可以使用[]来访问数组元素。default_delete也为数组提供模板偏特化,因此当unique_ptr被销毁时,会调用delete []释放数组内存。

1
2
3
unique_ptr<string[]> ptr{ new string[100] };
ptr[0] = "hello";
ptr[1] = "world";

一些陷阱

  unique_ptr是用来独占地持有对象的,所以通过同一原生指针来初始化多个unique_ptr,下面是一种错误的使用方式:

1
2
3
4
struct Widget{ };
Widget *ptr = new Widget;
unique_ptr<Widget> p1{ ptr };
unique_ptr<Widget> p2{ ptr }; // ERROR: multiple ownership

  当p1p2各自被销毁的时候,它们指向的Widget将被delete两次。

再谈异常安全

  C++14 提供了std::make_unique<T>()函数用来直接创建unique_ptr,但 C++11 并没有提供,不过其实现并不复杂:

1
2
3
4
5
6
template <typename T, typename... Ts>
std::unique_ptr<T> make_unique( Ts&&... params ) {
return std::unique_ptr<T>( new T( std::forward<Ts>(params)... ) );
}
// ...
auto ptr = make_unique<std::string>("senlin");

  思考一下使用make_unique的好处?


  使用unique_ptr并不能绝对地保证异常安全,你可能很惊讶于这个结论。让我们看看一个例子:

1
func(unique_ptr<T>{ new T }, func_throw_exception());

  C++ 标准并没有规定编译器对函数参数的求值次序,所以有可能出现这样的次序:

  • 调用new T分配动态内存。
  • 调用func_throw_exception()函数。
  • 调用unique_ptr的构造函数。

  调用func_throw_exception()函数会抛出异常,所以无法构造unique_ptr,导致new T所分配的内存不能回收,造成了内存泄露。解决这个问题,需要使用make_unique函数:

1
func(make_unique<T>(), func_throw_exception());

  这种情况下,成功解决了内存泄露的问题。


  make_unique在初始化对象的时候使用的()而不是{},所以下面的代码显然是初始化10个元素:

1
2
3
auto up = make_unique<vector<int>>( 10, 100 );
cout << "size: " << up->size() << endl;
// size: 10

  但是如果使用std::initializer_list来初始化对象时,要怎样做呢?嗯嗯,看看下面的代码:

1
2
3
4
auto initList = { 1, 2, 3, 4, 5 };
auto up = make_unique<vector<int>>( initList );
cout << "size: " << up->size() << endl;
// size: 5
<think>嗯,用户遇到了C++中ut::make_unique报错的问题,提示未找到make_unique。我需要先分析可能的原因。首先,make_uniqueC++14引入的标准库函数,位于std命名空间。用户使用的是ut::make_unique,可能是在使用某个第三方库或自定义的ut命名空间。 首先应该检查编译器是否支持C++14或更高版本。如果编译器版本过低或者没有启用C++14标准,就无法使用std::make_unique。用户可能在编译时没有指定-std=c++14或更高的标志。这时候需要建议用户检查编译器版本和编译选项。 另一个可能性是用户错误地使用了ut命名空间,而实际上应该使用std。可能用户误以为make_unique在ut中,或者第三方库有自定义实现,但未正确包含头文件或命名空间。需要确认用户是否包含了<memory>头文件,因为标准库的智能指针都在这里。 如果用户确实在使用某个第三方库的ut::make_unique,那可能是该库未正确安装或包含路径有问题。需要检查是否安装了相关库,头文件路径是否正确,以及是否需要链接相应的库文件。 还有可能是用户自己实现了ut::make_unique,但存在语法错误或未在正确的作用域内定义。这时候需要检查实现代码是否符合C++标准,比如是否正确处理了数组版本,或者是否有模板参数错误。 可能的解决方案包括:确保使用C++14或更高标准,包含<memory>头文件,使用std::make_unique代替ut::make_unique,检查第三方库的安装和包含情况,或者检查自定义实现的正确性。 需要分步骤指导用户检查这些可能性,并提供示例代码说明正确用法。同时,提醒用户注意异常安全和内存管理的最佳实践,比如避免直接使用new和delete,而是使用智能指针。</think>关于C++中`ut::make_unique`报错的原因及解决方案分析如下: ### 一、常见报错原因 1. **C++标准版本不匹配** `std::make_unique`是C++14引入的标准库函数。若编译器未启用C++14或更高标准,会报`make_unique is not a member of std/ut`错误。验证方法:检查编译命令是否包含`-std=c++14`或更高标志[^1]。 2. **命名空间误用** - 标准库函数应通过`std::make_unique`调用 - 第三方库UTPP的`ut::make_unique`需要单独安装头文件 - 错误示例: ```cpp ut::make_unique<int>(42); // 未正确包含UTPP库时报错 ``` 3. **头文件缺失** 标准智能指针需要包含头文件: ```cpp #include <memory> // 必须包含此头文件 ``` ### 二、分步解决方案 1. **启用C++14标准** 在编译命令中添标准指定: ```bash g++ -std=c++14 your_file.cpp -o output ``` 2. **修正命名空间** 标准库正确用法: ```cpp auto ptr = std::make_unique<int>(42); ``` 3. **检查第三方库** 若使用UTPP库: ```cpp #include <utpp/memory.h> // 假设UTPP的头文件路径 auto ptr = ut::make_unique<MyClass>(); ``` 4. **自定义实现(临时方案)** C++11环境下可添: ```cpp namespace ut { template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } } ``` ### 三、验证示例 ```cpp #include <memory> int main() { // 标准库正确用法 auto p1 = std::make_unique<int>(10); // 自定义实现用法 auto p2 = ut::make_unique<double>(3.14); return 0; } ``` ### 四、扩展建议 1. 使用`std::unique_ptr`替代原始指针管理资源 2. 避免直接使用`new`/`delete`运算符 3. 注意自定义实现的异常安全问题(推荐优先使用标准库实现)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值