目录
[一、 unique_ptr是什么?怎么用?](#一、 unique_ptr是什么?怎么用?)
[1. 核心定义](#1. 核心定义)
[2. 基本语法与使用示例](#2. 基本语法与使用示例)
[二、 底层实现原理](#二、 底层实现原理)
[1. 禁用拷贝构造与拷贝赋值](#1. 禁用拷贝构造与拷贝赋值)
[2. 可定制的删除器](#2. 可定制的删除器)
[1. unique_ptr vs auto_ptr vs shared_ptr](#1. unique_ptr vs auto_ptr vs shared_ptr)
[2. unique_ptr能否作为函数参数 / 返回值?](#2. unique_ptr能否作为函数参数 / 返回值?)
[3. 为什么返回unique_ptr不需要std::move?](#3. 为什么返回unique_ptr不需要std::move?)
[4. unique_ptr能否放入 STL 容器?](#4. unique_ptr能否放入 STL 容器?)
[四、 Linux 开发实践](#四、 Linux 开发实践)
[1. 实战 1:定制删除器管理 Linux 系统资源](#1. 实战 1:定制删除器管理 Linux 系统资源)
[场景 1:管理文件描述符](#场景 1:管理文件描述符)
[场景 2:管理 pthread 互斥锁](#场景 2:管理 pthread 互斥锁)
[2. 实战 2:在 Linux 网络编程中封装 Socket](#2. 实战 2:在 Linux 网络编程中封装 Socket)
[坑 1:滥用get()方法返回裸指针](#坑 1:滥用get()方法返回裸指针)
[坑 2:将unique_ptr管理的裸指针传递给第三方库](#坑 2:将unique_ptr管理的裸指针传递给第三方库)
[坑 3:在继承体系中使用unique_ptr](#坑 3:在继承体系中使用unique_ptr)
[六、 总结](#六、 总结)
在 Linux C++ 开发领域,内存管理是永恒的核心话题 ------ 裸指针的滥用会导致内存泄漏、野指针访问等一系列棘手问题,而**
unique_ptr作为 C++11 引入的独占式智能指针**,凭借轻量、高效、安全的特性,成为 Linux 下资源管理的首选工具之一。
一、 unique_ptr是什么?怎么用?
1. 核心定义
unique_ptr是 C++ 标准库 <memory> 头文件中的模板类,它独占所管理的动态内存资源的所有权:
- 同一时间,只有一个
unique_ptr可以指向某一块动态内存。 - 当
unique_ptr被销毁(如离开作用域)时,会自动释放所管理的内存,无需手动调用delete。 - 禁止拷贝 ,仅支持移动 (通过
std::move转移所有权)。
2. 基本语法与使用示例
对比裸指针和unique_ptr的区别
环境准备
Linux 下编译命令(需支持 C++11 及以上标准):
cpp
g++ -std=c++11 unique_ptr_demo.cpp -o unique_ptr_demo
./unique_ptr_demo
示例 1:基础创建与使用
cpp
#include <iostream>
#include <memory> // 必须包含的头文件
int main() {
// 方式1:直接通过裸指针构造(不推荐,推荐方式2)
std::unique_ptr<int> up1(new int(10));
// 方式2:使用 std::make_unique(C++14 引入,推荐!)
// 优势:避免裸指针暴露,且能防止内存泄漏(如异常安全问题)
auto up2 = std::make_unique<int>(20);
// 访问所管理的对象:和裸指针用法一致,支持 * 和 ->
std::cout << *up1 << std::endl; // 输出 10
std::cout << *up2 << std::endl; // 输出 20
// 获取裸指针(谨慎使用!)
int* raw_ptr = up1.get();
std::cout << *raw_ptr << std::endl; // 输出 10
// 重置:释放当前内存,指向新的内存
up1.reset(new int(30));
std::cout << *up1 << std::endl; // 输出 30
// 释放所有权:返回裸指针,unique_ptr 变为空
int* released_ptr = up1.release();
if (up1 == nullptr) {
std::cout << "up1 is now null" << std::endl; // 会执行
}
delete released_ptr; // 手动释放,因为 release 不会自动释放
return 0;
}
std::make_unique是 C++14 才支持的,如果你的编译器版本较低(如 GCC 4.8),则只能用裸指针构造。- 禁止拷贝 :
std::unique_ptr<int> up3 = up2;会直接编译报错!因为拷贝会导致两个unique_ptr管理同一块内存,违背 "独占" 原则。
示例 2:移动语义的正确使用
既然不能拷贝,那如何转移unique_ptr的所有权?答案是 std::move
cpp
#include <iostream>
#include <memory>
#include <utility> // std::move
int main() {
auto up1 = std::make_unique<int>(100);
// 移动构造:将 up1 的所有权转移给 up2
std::unique_ptr<int> up2 = std::move(up1);
if (up1 == nullptr) {
std::cout << "up1 失去所有权" << std::endl; // 会执行
}
std::cout << *up2 << std::endl; // 输出 100
// 移动赋值
auto up3 = std::make_unique<int>(200);
up3 = std::move(up2);
std::cout << *up3 << std::endl; // 输出 100
return 0;
}
std::move并不是 "移动" 内存,而是转移所有权 ------ 本质是将原unique_ptr的内部裸指针置空,把指针值转移给新的unique_ptr。
示例 3:管理动态数组
在 Linux 开发中,我们经常需要管理动态数组(如new int[10]),unique_ptr对数组有原生支持:
cpp
#include <iostream>
#include <memory>
int main() {
// 管理动态数组:模板参数加 []
std::unique_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
// 访问数组元素:支持 [] 运算符
for (int i = 0; i < 5; ++i) {
std::cout << arr_ptr[i] << " "; // 输出 1 2 3 4 5
}
std::cout << std::endl;
// 自动调用 delete[] 释放内存,无需手动处理
return 0;
}
管理普通对象时,unique_ptr默认用delete释放;管理数组时,默认用delete[]释放 ------ 这是unique_ptr比早期auto_ptr更安全的原因之一。
二、 底层实现原理
1. 禁用拷贝构造与拷贝赋值
unique_ptr之所以能保证 "独占所有权",核心是将拷贝构造函数和拷贝赋值运算符声明为删除(= delete)。
简化版伪代码如下:
cpp
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
// 拷贝构造:禁用
unique_ptr(const unique_ptr&) = delete;
// 拷贝赋值:禁用
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造:允许
unique_ptr(unique_ptr&& other) noexcept {
ptr_ = other.ptr_;
other.ptr_ = nullptr; // 原指针置空
}
// 移动赋值:允许
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
deleter_(ptr_); // 释放当前内存
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
private:
T* ptr_; // 内部管理的裸指针
Deleter deleter_; // 删除器
};
为什么unique_ptr不能拷贝?
如果允许拷贝,会导致多个unique_ptr指向同一块内存,当它们被销毁时,会重复调用delete,引发未定义行为。
2. 可定制的删除器
在 Linux 开发中,我们管理的资源不只是动态内存 ------ 还包括文件描述符(fd)、socket 句柄、互斥锁等。unique_ptr支持定制删除器,完美适配这些场景。
删除器的本质是一个可调用对象 (函数指针、函数对象、lambda 表达式),用于替代默认的std::default_delete。
伪代码中的删除器设计
cpp
// 默认删除器:针对普通对象
template <typename T>
struct default_delete {
void operator()(T* ptr) const {
delete ptr;
}
};
// 针对数组的特化版本
template <typename T>
struct default_delete<T[]> {
void operator()(T* ptr) const {
delete[] ptr;
}
};
核心优势 :删除器是unique_ptr的模板参数,而非成员变量 ------ 这意味着定制删除器不会增加unique_ptr的内存开销 (unique_ptr的大小和裸指针一致),这也是它比shared_ptr更轻量的原因。
三、补充知识点
1. unique_ptr vs auto_ptr vs shared_ptr
| 特性 | unique_ptr |
auto_ptr(C++17 已废弃) |
shared_ptr |
|---|---|---|---|
| 所有权模型 | 独占所有权 | 独占所有权(但设计缺陷) | 共享所有权(引用计数) |
| 拷贝支持 | 禁止拷贝,仅支持移动 | 允许拷贝(转移所有权,易出错) | 允许拷贝(增加引用计数) |
| 内存开销 | 与裸指针一致(无额外开销) | 与裸指针一致 | 额外存储引用计数(开销较大) |
| 线程安全 | 自身非线程安全 | 非线程安全 | 引用计数的修改是线程安全的 |
| 适用场景 | 独占资源(如局部动态对象、数组) | 已废弃,不建议使用 | 共享资源(如多对象共享同一内存) |
auto_ptr的拷贝会转移所有权,导致原auto_ptr变为空,容易引发野指针问题;而unique_ptr通过禁用拷贝,从语法层面杜绝了这个问题。unique_ptr比shared_ptr更轻量,适合不需要共享所有权的场景,是 Linux 开发的首选智能指针。
2. unique_ptr能否作为函数参数 / 返回值?
作为函数参数
可以,但推荐三种方式:
- 传值 :需要通过
std::move转移所有权,函数内部获得所有权,外部的unique_ptr变为空。 - 传左值引用 :函数内部可以修改外部的
unique_ptr(如reset()),但不会转移所有权。 - 传
const左值引用:函数内部只能读取,不能修改,适合仅访问资源的场景。
示例代码:
cpp
// 传值:转移所有权
void func1(std::unique_ptr<int> ptr) {
std::cout << *ptr << std::endl;
}
// 传引用:不转移所有权
void func2(std::unique_ptr<int>& ptr) {
ptr.reset(new int(100));
}
int main() {
auto ptr = std::make_unique<int>(10);
func1(std::move(ptr)); // 必须用 move
// func1(ptr); // 编译报错,不能拷贝
auto ptr2 = std::make_unique<int>(20);
func2(ptr2);
std::cout << *ptr2 << std::endl; // 输出 100
return 0;
}
作为函数返回值
可以直接返回,无需手动std::move------ 编译器会自动优化为移动语义。
cpp
std::unique_ptr<int> create_ptr() {
return std::make_unique<int>(100); // 自动移动
}
int main() {
auto ptr = create_ptr();
std::cout << *ptr << std::endl; // 输出 100
return 0;
}
3. 为什么返回unique_ptr不需要std::move?
C++ 标准规定,当函数返回值是右值引用类型或可移动类型时,编译器会优先调用移动构造函数,而非拷贝构造函数,这一优化被称为返回值优化(RVO)。
4. unique_ptr能否放入 STL 容器?
C++11 之前,STL 容器要求元素支持拷贝,因此unique_ptr不能放入容器;C++11 及以后,容器支持移动语义,unique_ptr可以放入容器。
示例代码
cpp
#include <iostream>
#include <memory>
#include <vector>
int main() {
std::vector<std::unique_ptr<int>> vec;
// 方式1:push_back + std::move
auto ptr1 = std::make_unique<int>(10);
vec.push_back(std::move(ptr1));
// 方式2:emplace_back(直接构造,更高效)
vec.emplace_back(std::make_unique<int>(20));
// 遍历容器
for (const auto& p : vec) {
std::cout << *p << " "; // 输出 10 20
}
std::cout << std::endl;
return 0;
}
容器中的unique_ptr仍然不能拷贝,只能通过移动操作修改容器(如erase、insert)。
四、 Linux 开发实践
在 Linux C++ 工业级项目中,unique_ptr的价值远不止管理动态内存 ------ 它是RAII 机制的完美载体,能解决各类资源泄漏问题。
1. 实战 1:定制删除器管理 Linux 系统资源
Linux 系统中,除了动态内存,还有大量需要手动释放的资源:
- 文件描述符(
open/close) - Socket 句柄(
socket/close) - 互斥锁(
pthread_mutex_init/pthread_mutex_destroy)
unique_ptr的定制删除器可以完美封装这些资源,实现自动释放。
场景 1:管理文件描述符
cpp
#include <iostream>
#include <memory>
#include <fcntl.h>
#include <unistd.h>
// 自定义删除器:关闭文件描述符
struct FdDeleter {
void operator()(int* fd) const {
if (*fd != -1) {
close(*fd);
std::cout << "File descriptor closed" << std::endl;
}
delete fd;
}
};
// 封装文件描述符的 unique_ptr
using UniqueFd = std::unique_ptr<int, FdDeleter>;
UniqueFd open_file(const char* path, int flags) {
int fd = open(path, flags);
if (fd == -1) {
perror("open failed");
return UniqueFd(nullptr);
}
return UniqueFd(new int(fd));
}
int main() {
// 打开文件:离开作用域时自动关闭
auto fd_ptr = open_file("test.txt", O_RDONLY);
if (fd_ptr) {
std::cout << "File opened successfully" << std::endl;
}
return 0;
}
核心优势 :即使函数中途抛出异常,unique_ptr也会保证删除器被调用,资源不会泄漏 ------ 这是裸指针无法做到的。
场景 2:管理 pthread 互斥锁
cpp
#include <memory>
#include <pthread.h>
// 互斥锁删除器
struct MutexDeleter {
void operator()(pthread_mutex_t* mutex) const {
pthread_mutex_destroy(mutex);
}
};
// 封装互斥锁的 unique_ptr
using UniqueMutex = std::unique_ptr<pthread_mutex_t, MutexDeleter>;
UniqueMutex create_mutex() {
auto mutex = new pthread_mutex_t;
pthread_mutex_init(mutex, nullptr);
return UniqueMutex(mutex);
}
int main() {
auto mutex = create_mutex();
pthread_mutex_lock(mutex.get());
// 临界区代码
pthread_mutex_unlock(mutex.get());
// 离开作用域自动销毁互斥锁
return 0;
}
2. 实战 2:在 Linux 网络编程中封装 Socket
在 muduo 等 Linux 网络库的开发中,unique_ptr常被用于封装 Socket 句柄,避免因忘记close导致的资源泄漏。
简化示例:
cpp
#include <memory>
#include <sys/socket.h>
#include <unistd.h>
struct SocketDeleter {
void operator()(int* sock_fd) const {
if (*sock_fd != -1) {
close(*sock_fd);
}
delete sock_fd;
}
};
using UniqueSocket = std::unique_ptr<int, SocketDeleter>;
UniqueSocket create_tcp_socket() {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
return UniqueSocket(nullptr);
}
return UniqueSocket(new int(fd));
}
int main() {
auto sock = create_tcp_socket();
if (sock) {
// 进行 bind、listen 等操作
}
// 自动关闭 socket
return 0;
}
五、避坑指南
坑 1:滥用get()方法返回裸指针
get()方法返回unique_ptr管理的裸指针,但不会转移所有权 ------ 如果用delete释放这个裸指针,会导致unique_ptr二次释放,引发崩溃。
错误示例:
cpp
auto ptr = std::make_unique<int>(10);
int* raw = ptr.get();
delete raw; // 错误!double free
正确用法 :get()仅用于临时访问资源,不用于长期持有或释放。
坑 2:将unique_ptr管理的裸指针传递给第三方库
在 Linux 开发中,我们经常需要调用第三方库函数,这些函数可能需要裸指针。此时要注意:确保第三方库不会保存这个裸指针 ,否则当unique_ptr销毁后,第三方库会访问野指针。
正确示例:
cpp
// 第三方库函数:仅临时使用指针,不保存
void third_party_func(int* ptr) {
std::cout << *ptr << std::endl;
}
int main() {
auto ptr = std::make_unique<int>(10);
third_party_func(ptr.get()); // 安全
return 0;
}
坑 3:在继承体系中使用unique_ptr
当unique_ptr管理基类指针时,基类的析构函数必须是虚函数,否则会导致派生类的析构函数无法被调用,引发资源泄漏。
正确示例:
cpp
#include <iostream>
#include <memory>
class Base {
public:
virtual ~Base() { // 虚析构函数
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// 离开作用域时,会先调用 Derived 析构,再调用 Base 析构
return 0;
}
六、 总结
unique_ptr是 Linux C++ 开发中最轻量、最高效的智能指针,其核心价值在于:
- 零基础友好:语法简单,无需关注内存释放,新手也能快速上手。
- 面试核心:所有权模型、移动语义、与其他智能指针的对比是必考点。
- 工业级利器:通过定制删除器,完美适配 Linux 各类系统资源的管理,从根源上杜绝资源泄漏。