unique_ptr

目录

[一、 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_ptrshared_ptr更轻量,适合不需要共享所有权的场景,是 Linux 开发的首选智能指针。

2. unique_ptr能否作为函数参数 / 返回值?

作为函数参数

可以,但推荐三种方式:

  1. 传值 :需要通过std::move转移所有权,函数内部获得所有权,外部的unique_ptr变为空。
  2. 传左值引用 :函数内部可以修改外部的unique_ptr(如reset()),但不会转移所有权。
  3. 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仍然不能拷贝,只能通过移动操作修改容器(如eraseinsert)。

四、 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++ 开发中最轻量、最高效的智能指针,其核心价值在于:

  1. 零基础友好:语法简单,无需关注内存释放,新手也能快速上手。
  2. 面试核心:所有权模型、移动语义、与其他智能指针的对比是必考点。
  3. 工业级利器:通过定制删除器,完美适配 Linux 各类系统资源的管理,从根源上杜绝资源泄漏。
相关推荐
lihui_cbdd2 小时前
GROMACS 2026 Beta 异构集群完全部署手册(5090可用)
linux·计算化学
炬火初现2 小时前
C++17特性(3)
开发语言·c++
晨非辰2 小时前
Linux权限实战速成:用户切换/文件控制/安全配置15分钟掌握,解锁核心操作与权限模型内核逻辑
linux·运维·服务器·c++·人工智能·后端
草莓熊Lotso2 小时前
Linux 进程创建与终止全解析:fork 原理 + 退出机制实战
linux·运维·服务器·开发语言·汇编·c++·人工智能
枫叶丹42 小时前
【Qt开发】Qt系统(九)-> Qt TCP Socket
c语言·开发语言·网络·c++·qt·tcp/ip
JERRY. LIU3 小时前
Mac 笔记本通用快捷键大全
linux·macos
信创新态势4 小时前
财经媒体研判:内存疯涨多米诺效应推倒,服务器涨价箭在弦上
运维·服务器·媒体
007php0074 小时前
PHP与Java项目在服务器上的对接准备与过程
java·服务器·开发语言·分布式·面试·职场和发展·php
EverydayJoy^v^4 小时前
RH134简单知识点——第6章——管理SELinux安全性
linux·服务器·网络