C++移动语义和智能指针

一、移动语义:C++11 最革命性的性能优化

1.1 为什么需要移动语义?

传统拷贝的性能灾难 在 C++11 之前,对象的传递只能通过拷贝实现。对于包含大量动态分配资源的对象(如 std::string、std::vector),深拷贝会带来巨大的性能开销:

cpp 复制代码
std::string create_large_string() {
    std::string s(1000000, 'a'); // 分配1MB内存
    return s; // 传统上会调用拷贝构造函数,再销毁原对象
}

int main() {
    std::string s = create_large_string(); 
    // 总共:1次构造 + 1次拷贝构造 + 1次析构
    // 实际执行了2次内存分配和2次数据拷贝!
}

问题本质:临时对象在拷贝后会立即被销毁,我们实际上在 "复制一个即将被扔掉的东西"。如果能直接 "偷走" 临时对象的资源,就能避免不必要的拷贝。

1.2 左值与右值:移动语义的基石

核心定义

  • 左值 (lvalue):可以取地址、有名字的表达式。代表一个持久的对象。
  • 右值 (rvalue):不能取地址、没有名字的表达式。代表一个临时对象或即将销毁的对象。

直观判断:能放在赋值运算符左边的是左值,只能放在右边的是右值。

cpp 复制代码
int a = 10; // a是左值,10是右值
int* p = &a; // 正确,可以取左值地址
// int* p2 = &10; // 错误,不能取右值地址

std::string s1 = "hello"; // s1是左值,"hello"是右值
std::string s2 = s1 + s1; // s1+s1的结果是右值(临时对象)

C++11 右值细分

  • 纯右值 (prvalue):字面量、临时对象、lambda 表达式等
  • 将亡值 (xvalue):通过 std::move 转换的左值,代表即将被移动的对象

1.3 右值引用:绑定到右值的引用

语法T&&

  • 左值引用T&只能绑定到左值
  • 右值引用T&&只能绑定到右值
  • const 左值引用const T&可以绑定到左值和右值(这是 C++11 之前临时对象能被传递的原因)
cpp 复制代码
int a = 10;
int& lref = a; // 正确,左值引用绑定左值
// int& lref2 = 10; // 错误,左值引用不能绑定右值

int&& rref = 10; // 正确,右值引用绑定右值
// int&& rref2 = a; // 错误,右值引用不能绑定左值

const int& clref = a; // 正确
const int& clref2 = 10; // 正确,const左值引用可以绑定右值

右值引用的意义:让我们能够区分 "普通对象" 和 "临时对象",从而为临时对象提供特殊的处理逻辑(移动)。

1.4 移动构造函数与移动赋值运算符

移动构造函数

cpp 复制代码
class MyString {
private:
    char* data;
    size_t size;

public:
    // 构造函数
    MyString(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
        std::cout << "构造函数:分配了" << size+1 << "字节\n";
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
        std::cout << "拷贝构造函数:深拷贝了" << size+1 << "字节\n";
    }

    // 移动构造函数(转移资源,不分配新内存)
    MyString(MyString&& other) noexcept {
        // 直接"偷"资源
        data = other.data;
        size = other.size;
        
        // 将原对象置空,防止析构时释放我们已经拥有的资源
        other.data = nullptr;
        other.size = 0;
        
        std::cout << "移动构造函数:转移了" << size+1 << "字节\n";
    }

    // 析构函数
    ~MyString() {
        delete[] data;
        std::cout << "析构函数:释放了内存\n";
    }
};

移动赋值运算符

cpp 复制代码
MyString& operator=(MyString&& other) noexcept {
    if (this != &other) { // 防止自赋值
        // 先释放自己当前的资源
        delete[] data;
        
        // 偷取对方的资源
        data = other.data;
        size = other.size;
        
        // 置空对方
        other.data = nullptr;
        other.size = 0;
        
        std::cout << "移动赋值运算符:转移了" << size+1 << "字节\n";
    }
    return *this;
}

关键要点

  1. noexcept 关键字:必须加上!否则标准库容器(如 std::vector)在重新分配内存时会选择拷贝而不是移动,因为移动可能抛出异常会导致容器处于不一致状态。
  2. 原对象置空:移动后原对象必须处于 "可析构" 的状态,不能让它持有任何资源。
  3. 移动后原对象不可用:除了赋值和析构,不要对移动后的对象做任何其他操作。

1.5 std::move:强制转换为右值

std::move 的本质:一个模板函数,将左值无条件转换为右值引用。它本身不移动任何数据,只是触发移动语义。

cpp 复制代码
MyString s1("hello");
MyString s2 = s1; // 调用拷贝构造函数(s1是左值)
MyString s3 = std::move(s1); // 调用移动构造函数!

// 此时s1已经被移动,内部data为nullptr,不能再使用
// std::cout << s1 << std::endl; // 未定义行为!

使用场景

  • 当你确定一个左值不再需要使用时,可以用 std::move 将其资源转移给其他对象
  • 标准库容器的插入操作:vec.push_back(std::move(obj))

1.6 完美转发与 std::forward

问题:如何编写一个函数模板,将参数原封不动地转发给另一个函数,保持其左值 / 右值属性?

引用折叠规则

  • T& & 折叠为 T&
  • T& && 折叠为 T&
  • T&& & 折叠为 T&
  • T&& && 折叠为 T&&

std::forward 的作用:根据模板参数的类型,有条件地将参数转换为右值引用。如果传入的是左值,转发后还是左值;如果传入的是右值,转发后还是右值。

cpp 复制代码
template<typename T, typename Arg>
std::unique_ptr<T> create_unique(Arg&& arg) {
    // 完美转发arg给T的构造函数
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

int main() {
    std::string s = "hello";
    auto p1 = create_unique<std::string>(s); // s是左值,转发为左值,调用拷贝构造
    auto p2 = create_unique<std::string>(std::move(s)); // 转发为右值,调用移动构造
}

1.7 移动语义的常见误区

  1. std::move 不是万能的:如果一个类没有定义移动构造函数 / 移动赋值运算符,即使使用 std::move,也会调用拷贝构造函数 / 拷贝赋值运算符。

  2. 不要返回局部对象的 std::move

    cpp 复制代码
    MyString func() {
        MyString s("hello");
        return std::move(s); // 错误!会阻止返回值优化(RVO)
        // 正确:直接return s; 编译器会自动进行RVO,比移动更高效
    }
  3. 移动后的对象只能析构或赋值:不要对移动后的对象进行任何其他操作,除非文档明确说明该操作是安全的。

二、智能指针:解决内存管理的终极方案

2.1 原始指针的七大原罪

  1. 内存泄漏:忘记 delete
  2. 野指针:指针指向已经释放的内存
  3. 重复释放:同一个内存被 delete 多次
  4. 悬空指针:指针指向的对象已经被销毁
  5. 所有权不明确:谁负责释放内存?
  6. 异常安全问题:函数抛出异常时,delete 语句可能不会执行
  7. 多线程安全问题:多个线程同时访问和修改同一个指针

智能指针通过 RAII(资源获取即初始化)机制,自动管理内存,解决了上述所有问题。

2.2 C++11 及以后的三种智能指针

智能指针 所有权 拷贝 / 移动 适用场景 底层实现
std::unique_ptr<T> 独占所有权 不可拷贝,只能移动 单个对象的所有权管理 裸指针 + 析构函数
std::shared_ptr<T> 共享所有权 可拷贝,可移动 多个对象共享同一个资源 裸指针 + 引用计数
std::weak_ptr<T> 无所有权 可拷贝,可移动 解决 shared_ptr 的循环引用问题 指向 shared_ptr 的控制块

2.3 std::unique_ptr:独占所有权的智能指针

基本用法

cpp 复制代码
#include <memory>

int main() {
    // 创建方式1:C++11及以后
    std::unique_ptr<int> p1(new int(10));
    
    // 创建方式2:C++14推荐,更安全(异常安全)
    std::unique_ptr<int> p2 = std::make_unique<int>(20);
    
    // 访问方式:和原始指针一样
    std::cout << *p1 << std::endl; // 10
    std::cout << p1.get() << std::endl; // 获取原始指针
    
    // 释放资源:当unique_ptr离开作用域时,自动调用delete
    // 不需要手动delete!
}

移动语义与 unique_ptr unique_ptr 不可拷贝,但可以移动:

cpp 复制代码
std::unique_ptr<int> p1 = std::make_unique<int>(10);
// std::unique_ptr<int> p2 = p1; // 错误!unique_ptr不可拷贝

std::unique_ptr<int> p3 = std::move(p1); // 正确!移动所有权
// 此时p1为空,不再拥有任何资源

自定义删除器 unique_ptr 支持自定义删除器,用于管理非 new 分配的资源:

cpp 复制代码
// 管理文件指针
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "w"), fclose);
if (file) {
    fprintf(file.get(), "Hello, World!\n");
}
// 离开作用域时自动调用fclose关闭文件

适用场景

  • 函数返回值:返回一个动态分配的对象
  • 类成员变量:管理类的动态资源
  • 容器元素:在容器中存储动态分配的对象

2.4 std::shared_ptr:共享所有权的智能指针

基本原理 shared_ptr 内部维护一个引用计数,当有新的 shared_ptr 指向同一个对象时,引用计数加 1;当 shared_ptr 被销毁时,引用计数减 1。当引用计数变为 0 时,自动释放对象。

基本用法

cpp 复制代码
// 创建方式1:C++11及以后
std::shared_ptr<int> p1(new int(10));

// 创建方式2:推荐,更高效(一次内存分配)
std::shared_ptr<int> p2 = std::make_shared<int>(20);

// 拷贝:引用计数加1
std::shared_ptr<int> p3 = p2;
std::cout << p2.use_count() << std::endl; // 输出2

// 重置:引用计数减1
p2.reset();
std::cout << p3.use_count() << std::endl; // 输出1

// 当p3离开作用域时,引用计数变为0,自动释放内存

引用计数的存储位置 shared_ptr 包含两个指针:

  1. 指向实际对象的指针
  2. 指向控制块的指针

控制块中存储:

  • 引用计数
  • 弱引用计数(用于 weak_ptr)
  • 删除器
  • 分配器

注意:使用 new 创建 shared_ptr 时,会进行两次内存分配(一次分配对象,一次分配控制块)。而 std::make_shared 会进行一次内存分配,同时分配对象和控制块,效率更高。

适用场景

  • 多个对象需要共享同一个资源
  • 资源的生命周期不确定

2.5 std::weak_ptr:解决循环引用问题

循环引用的灾难

cpp 复制代码
class A {
public:
    std::shared_ptr<A> next;
    ~A() { std::cout << "A被销毁\n"; }
};

int main() {
    std::shared_ptr<A> p1 = std::make_shared<A>();
    std::shared_ptr<A> p2 = std::make_shared<A>();
    
    p1->next = p2;
    p2->next = p1;
    
    // 离开作用域时,p1和p2被销毁,但它们的引用计数都从2变为1,永远不会变为0
    // 内存泄漏!
}

weak_ptr 的作用 weak_ptr 是一种 "弱引用",它指向一个 shared_ptr 管理的对象,但不增加引用计数。它的存在不会阻止对象被销毁。

解决循环引用

cpp 复制代码
class A {
public:
    std::weak_ptr<A> next; // 使用weak_ptr而不是shared_ptr
    ~A() { std::cout << "A被销毁\n"; }
};

int main() {
    std::shared_ptr<A> p1 = std::make_shared<A>();
    std::shared_ptr<A> p2 = std::make_shared<A>();
    
    p1->next = p2;
    p2->next = p1;
    
    // 离开作用域时,p1和p2被销毁,引用计数变为0,对象被正确释放
}

weak_ptr 的用法

cpp 复制代码
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 不增加引用计数

// 检查对象是否还存在
if (!wp.expired()) {
    // 升级为shared_ptr,此时引用计数加1
    std::shared_ptr<int> sp2 = wp.lock();
    std::cout << *sp2 << std::endl;
}

2.6 智能指针的常见陷阱

  1. 不要用同一个原始指针初始化多个智能指针

    cpp 复制代码
    int* p = new int(10);
    std::shared_ptr<int> sp1(p);
    std::shared_ptr<int> sp2(p); // 错误!两个shared_ptr指向同一个对象,会导致重复释放
  2. 不要在函数参数中直接使用 new

    cpp 复制代码
    void func(std::shared_ptr<int> sp, int x) {}
    
    // 可能导致内存泄漏!
    func(new int(10), some_function_that_may_throw());
    // 正确写法
    func(std::make_shared<int>(10), some_function_that_may_throw());
  3. 不要用 shared_ptr 管理数组

    cpp 复制代码
    // 错误!会调用delete而不是delete[]
    std::shared_ptr<int> sp(new int[10]);
    
    // 正确写法
    std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
    // 或者C++17以后
    std::shared_ptr<int[]> sp(new int[10]);
  4. 避免裸指针与智能指针混用 一旦将原始指针交给智能指针管理,就不要再使用原始指针访问或释放对象。

  5. 不要返回 this 的 shared_ptr

    cpp 复制代码
    class A {
    public:
        std::shared_ptr<A> get_self() {
            return std::shared_ptr<A>(this); // 错误!会导致重复释放
        }
    };
    
    // 正确写法:继承自std::enable_shared_from_this
    class A : public std::enable_shared_from_this<A> {
    public:
        std::shared_ptr<A> get_self() {
            return shared_from_this();
        }
    };

三、移动语义与智能指针的结合使用

3.1 unique_ptr 的移动特性

unique_ptr 的实现完全依赖于移动语义。它不可拷贝,但可以移动,这完美体现了 "独占所有权" 的语义:

  • 当你移动一个 unique_ptr 时,原 unique_ptr 不再拥有对象的所有权,新的 unique_ptr 成为唯一的所有者。
  • 这确保了在任何时刻,只有一个 unique_ptr 指向同一个对象,避免了重复释放和悬空指针的问题。

3.2 shared_ptr 的移动优化

shared_ptr 也支持移动语义。移动一个 shared_ptr 比拷贝一个 shared_ptr 更高效:

  • 拷贝 shared_ptr 需要原子操作增加引用计数
  • 移动 shared_ptr 不需要修改引用计数,只需要转移两个指针的值
cpp 复制代码
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = std::move(sp1); // 比拷贝快得多
// 此时sp1为空

3.3 函数返回智能指针

返回 unique_ptr

cpp 复制代码
std::unique_ptr<int> create_int(int value) {
    return std::make_unique<int>(value);
    // 编译器会自动进行RVO,不需要std::move
}

int main() {
    std::unique_ptr<int> p = create_int(10);
}

返回 shared_ptr

cpp 复制代码
std::shared_ptr<int> create_int(int value) {
    return std::make_shared<int>(value);
}

3.4 在容器中使用智能指针

cpp 复制代码
#include <vector>
#include <memory>

int main() {
    std::vector<std::unique_ptr<int>> vec;
    
    // 插入元素
    vec.push_back(std::make_unique<int>(10));
    vec.push_back(std::make_unique<int>(20));
    
    // 遍历元素
    for (const auto& p : vec) {
        std::cout << *p << std::endl;
    }
    
    // 移动元素
    std::unique_ptr<int> p = std::move(vec[0]);
    vec.erase(vec.begin());
}
相关推荐
JAVA面经实录9171 小时前
Elasticsearch 完整版完整知识体系
java·elasticsearch·搜索引擎·es
不负岁月无痕1 小时前
C++继承与多态知识点及其高频面试问题
开发语言·c++·面试
hikktn1 小时前
ORA-01861 日期格式错误的根治方案:从 SQL 层到 Java 层的标准化治理
java·python·sql
June`1 小时前
如何组织一个并行程序
开发语言·cuda
雪宫街道1 小时前
SpringBoot 静态资源映射规则与定制
java·spring boot·后端·spring
宸津-代码粉碎机1 小时前
Spring AI企业级实战|智能记忆摘要+自动遗忘机制落地,彻底解决上下文爆炸与Token冗余
java·大数据·人工智能·后端·python·spring
dtq04241 小时前
C语言刷题函数1-判断素数(分支语句,函数两种方法)
c语言·开发语言·学习
南极企鹅1 小时前
springboot项目不退出的原因
java·spring boot·后端
乘浪初心1 小时前
python调用API接口,免费API调取,学习如何调取API接口并反馈你输入的内容
开发语言·python·api·免费