菜单

Administrator
发布于 2026-05-22 / 0 阅读
0
0

Java 开发者学 C++(三):指针、引用与内存管理

从 Java 引用说起

在 Java 中,所有对象都在堆上分配,变量只是对象的引用(reference):

// Java:所有对象都在堆上,变量是引用
Person p = new Person("Alice");
Person q = p;                     // q 是同一个对象的另一个引用
q.setName("Bob");                 // p.getName() 也变成 "Bob"

在 C++ 中,这个问题分裂成了三种不同的机制:值语义(value semantics)引用(reference)指针(pointer)。理解这三者的区别,是 Java 开发者学好 C++ 的第一道门槛。


值语义:C++ 的默认行为

#include <string>
#include <iostream>

struct Person {
    std::string name;
    int age;
};

int main() {
    Person p{"Alice", 30};   // p 就是对象本身,在栈上
    Person q = p;             // q 是 p 的独立副本(值拷贝)!
    q.name = "Bob";          // p.name 仍然是 "Alice"!

    std::cout << p.name;     // 输出 "Alice"
    std::cout << q.name;     // 输出 "Bob"
}

这是 Java 开发者最需要适应的转变:在 C++ 里,等号就是拷贝,不是创建引用。

值语义是 C++ 的基石之一。intdoublestd::stringstd::vector 都是值语义——它们可以独立复制,每个副本拥有自己的生命周期。

💡 好的编码习惯:默认使用值语义。只有当确定需要共享或避免拷贝开销时,才使用引用或指针。


引用:C++ 的"别名"

C++ 的引用(reference)是变量的别名,必须在创建时绑定,之后不能改绑:

#include <iostream>

int main() {
    int a = 10;
    int& ref = a;     // ref 是 a 的别名
    ref = 20;          // a 也变成 20

    int b = 30;
    ref = b;           // 不是让 ref 指向 b!而是把 b 的值赋给 a(a 变成 30)

    std::cout << a;    // 输出 30
}

引用最常见的用途是函数传参和返回值:

#include <vector>
#include <iostream>

// 引用传参(避免拷贝代价)
void print(const std::vector<int>& data) {
    for (int v : data) {
        std::cout << v << " ";
    }
}

// 引用返回(允许修改容器元素)
int& get_element(std::vector<int>& vec, size_t index) {
    return vec[index];  // 返回引用,可被修改
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4};
    get_element(vec, 2) = 100;   // vec[2] 变为 100
    print(vec);                  // 输出: 1 2 3 100
}

⚠️ 坏味道:返回局部变量的引用

int& bad() {
    int local = 42;
    return local;  // ❌ 局部变量在函数返回时销毁,返回的是悬空引用!
}
// bad() 的返回值是未定义行为(undefined behavior)

正确做法:返回值(按值返回),或确保返回的引用指向函数外部分配的对象。

引用 vs 指针:快速对比

特性 指针 引用
语法 * 声明和访问 & 声明,直接使用
可空 可以 nullptr 必须绑定到合法对象
可重新绑定 可以指向不同对象 一生只绑定一个对象
可做算术运算 支持 不支持
占用内存 通常占用一个指针大小的内存 不额外占用(只是别名)

指针:C++ 的终极武器

指针存储的是内存地址。Java 参考 ≈ 自动管理的指针。

#include <iostream>

int main() {
    int a = 10;
    int* ptr = &a;      // ptr 存储 a 的地址
    *ptr = 20;          // 解引用:修改 ptr 指向的值

    std::cout << a;     // 输出 20

    int* null_ptr = nullptr;  // 空指针
    if (null_ptr) {           // 检查指针是否为空
        *null_ptr = 42;
    }
}

const 指针的四重境界

int value = 10;

int* p1 = &value;                // 普通指针:值和指向都可改
const int* p2 = &value;          // 指向 const int:不能改值,可改指向
int* const p3 = &value;          // const 指针:可改值,不能改指向
const int* const p4 = &value;    // 两者皆 const:既不能改值,也不能改指向

记忆口诀:const 在 * 左边表示指向的值不能改,const 在 * 右边表示指针本身不能改。

指针算术

#include <iostream>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* ptr = arr;           // 指向 arr[0]
    ptr++;                    // 指向 arr[1]
    std::cout << *ptr;        // 输出 20
    std::cout << *(ptr + 2);  // 输出 40(从 arr[1] 后移 2 个 int)
}

⚠️ 坏味道:裸指针管理所有权

void leaky() {
    int* p = new int(42);
    // 使用 p ...
    // ❌ 忘记 delete p,内存泄漏!
}

正确做法:使用智能指针或栈对象。需要动态生命周期时,优先 std::unique_ptr


栈 vs 堆

特性 栈(Stack) 堆(Heap)
分配方式 自动(函数调用压栈) 手动(new/malloc)
释放方式 自动(函数返回弹栈) 手动(delete/free)
速度 快(仅移动栈指针) 较慢(需要查找空闲块)
大小 有限(通常 1-8 MB) 大(受限于系统内存)
生命周期 函数作用域内 程序员控制
struct Person {
    std::string name;
    int age;
};

void example() {
    Person p{"Alice", 30};                // 栈上分配,自动销毁
    Person* hp = new Person{"Bob", 25};   // 堆上分配
    delete hp;                            // 必须手动释放!
}

C++ 黄金法则:优先栈对象,用 RAII

// ❌ Java 思维:什么都 new
Person* p = new Person("Alice");
delete p;

// ✅ C++ 思维:栈对象优先
Person p{"Alice"};                    // 栈上
std::vector<int> v{1, 2, 3};         // 栈上

// ✅ 必须用堆时,使用智能指针
auto v2 = std::make_unique<std::vector<int>>(10);
auto v3 = std::make_shared<Person>("Bob");

RAII:C++ 的灵魂

RAII(Resource Acquisition Is Initialization)是 C++ 最核心的设计思想:将资源的生命周期与对象的生命周期绑定。 构造时获取资源,析构时释放资源。

基本原理

#include <cstdio>
#include <stdexcept>
#include <iostream>

class FileHandle {
    FILE* file;
public:
    FileHandle(const char* filename, const char* mode = "r") {
        file = fopen(filename, mode);
        if (!file) {
            throw std::runtime_error("打开文件失败");
        }
        std::cout << "文件已打开: " << filename << std::endl;
    }

    ~FileHandle() {
        if (file) {
            fclose(file);
            std::cout << "文件已自动关闭" << std::endl;
        }
    }

    // 禁止拷贝(RAII 类通常不可复制)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 读取一行
    std::string readLine() {
        char buf[256];
        if (fgets(buf, sizeof(buf), file)) {
            return buf;
        }
        return "";
    }
};

void process_file() {
    FileHandle fh("data.txt");  // 构造时打开
    // 使用文件...
    std::string line = fh.readLine();
}  // 离开作用域时 fh 析构,文件自动关闭——即使有异常也会析构!

RAII 的三大优势:

  1. 异常安全:即使抛出异常,栈回溯也会调用析构函数
  2. 确定性释放:离开作用域立刻释放,没有 GC 延迟
  3. 代码简洁:不需要 try-finally

RAII 真实场景 1:互斥锁

#include <mutex>
#include <iostream>

std::mutex g_mutex;
int shared_counter = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(g_mutex);  // 构造时 lock
    ++shared_counter;                           // 安全操作共享数据
    if (shared_counter == 3) {
        throw std::runtime_error("模拟异常");
    }
}  // lock 析构时自动 unlock,即使是异常路径也保证解锁!

int main() {
    try {
        safe_increment();
    } catch (...) {
        // g_mutex 已经被 unlock(lock_guard 析构保证)
        std::cout << "异常被捕获,互斥锁已安全释放" << std::endl;
    }
}

RAII 真实场景 2:数据库连接

#include <iostream>
#include <stdexcept>

// 模拟数据库 API
struct DBConnection {
    bool connected = false;
    void connect() {
        std::cout << "数据库连接已建立" << std::endl;
        connected = true;
    }
    void disconnect() {
        if (connected) {
            std::cout << "数据库连接已关闭" << std::endl;
            connected = false;
        }
    }
    void query(const std::string& sql) {
        if (!connected) throw std::runtime_error("未连接");
        std::cout << "执行查询: " << sql << std::endl;
    }
};

class DatabaseGuard {
    DBConnection& conn_;
public:
    DatabaseGuard(DBConnection& conn) : conn_(conn) {
        conn_.connect();
    }
    ~DatabaseGuard() {
        conn_.disconnect();
    }
    DatabaseGuard(const DatabaseGuard&) = delete;
    DatabaseGuard& operator=(const DatabaseGuard&) = delete;
};

int main() {
    DBConnection conn;
    {
        DatabaseGuard guard(conn);  // 连接数据库
        conn.query("SELECT * FROM users");
        // 即使这里抛出异常,guard 析构也会断开连接
    }  // guard 析构 → conn.disconnect() 被调用
}

智能指针:RAII 的内存管理

C++11 引入了三种智能指针,它们都是 RAII 思想的体现——将堆内存的生命周期与智能指针对象的生命周期绑定。

std::unique_ptr(独占所有权)

unique_ptr 是"独享所有权"的智能指针。它不能被拷贝,只能被移动(转移所有权)。

#include <memory>
#include <iostream>

struct Person {
    std::string name;
    Person(const std::string& n) : name(n) {
        std::cout << "Person: " << name << " 构造" << std::endl;
    }
    ~Person() {
        std::cout << "Person: " << name << " 析构" << std::endl;
    }
};

int main() {
    auto ptr = std::make_unique<Person>("Alice");  // C++14

    // auto ptr2 = ptr;   // ❌ 编译错误!不能复制 unique_ptr
    auto ptr3 = std::move(ptr);   // ✅ 转移所有权,ptr 变为空

    // 在容器中使用
    std::vector<std::unique_ptr<Person>> people;
    people.push_back(std::make_unique<Person>("Bob"));

    // 自定义删除器
    auto deleter = [](FILE* f) {
        if (f) {
            fclose(f);
            std::cout << "文件已关闭(自定义删除器)" << std::endl;
        }
    };
    std::unique_ptr<FILE, decltype(deleter)> file_ptr(
        fopen("test.txt", "w"), deleter
    );
}

💡 make_unique 的实现原理

// 简化版 make_unique 实现
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    return std::unique_ptr<T>(
        new T(std::forward<Args>(args)...)  // 完美转发参数
    );
}

make_unique 的优势:

  1. 异常安全:避免 new T(args)unique_ptr 构造之间抛出异常导致的泄漏
  2. 代码简洁:不用写 new 关键字
  3. 避免重复类型:类型只写一次

std::shared_ptr(共享所有权)

shared_ptr 使用引用计数来管理共享所有权。当最后一个 shared_ptr 销毁时,资源被释放。

#include <memory>
#include <iostream>

struct Person {
    std::string name;
    Person(const std::string& n) : name(n) {}
    ~Person() { std::cout << name << " 析构" << std::endl; }
};

int main() {
    auto ptr1 = std::make_shared<Person>("Alice");
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 1

    {
        auto ptr2 = ptr1;  // 引用计数 +1
        std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 2
    }  // ptr2 析构,引用计数 -1

    std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 1
}  // ptr1 析构,引用计数为 0,Person 析构

💡 引用计数机制详解

shared_ptr 内部包含两个指针:

  1. 指向管理对象的指针
  2. 指向控制块(control block)的指针

控制块包含:

  • 引用计数(shared count):记录共享所有权的 shared_ptr 数量
  • 弱引用计数(weak count):记录 weak_ptr 数量(用于控制块的存活)
  • 删除器(deleter)和分配器(allocator)
// 示意图(伪代码)
template<typename T>
class shared_ptr_inner {
    T* ptr_;
    ControlBlock* cb_;
};

struct ControlBlock {
    long shared_count_;   // 引用计数
    long weak_count_;     // 弱引用计数
    void (*deleter_)(void*);
};

为什么推荐 make_shared

  1. 一次分配make_shared 将对象和控制块分配在同一块内存中,比分开 new + 控制块少一次内存分配
  2. 异常安全:同 make_unique
  3. 性能更好:缓存局部性更优

make_shared 的缺点:当仍有 weak_ptr 存在时,即使 shared_count 已为 0,控制块无法释放,导致对象内存无法释放。对内存敏感的场景,可考虑 new + shared_ptr 分开分配。

std::weak_ptr(弱引用——打破循环引用)

weak_ptrshared_ptr 的"观察者",它不增加引用计数。主要用于打破循环引用

⚠️ 坏味道:循环引用导致内存泄漏

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // ❌ shared_ptr 导致循环引用

    ~Node() { std::cout << "Node 析构" << std::endl; }
};

void circular_reference_demo() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a;  // a 和 b 互相持有 shared_ptr,引用计数永远不为 0
    // 函数结束:a 和 b 都不会析构,内存泄漏!
}

解决方案:用 weak_ptr 打破循环

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;   // ✅ weak_ptr 不增加引用计数

    ~Node() { std::cout << "Node 析构" << std::endl; }

    void print_prev() {
        if (auto sp = prev.lock()) {  // lock() 返回 shared_ptr,若已释放返回空
            std::cout << "前驱节点存在" << std::endl;
        } else {
            std::cout << "前驱节点已释放" << std::endl;
        }
    }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a;   // ✅ weak_ptr,不增加引用计数

    std::cout << "a use_count: " << a.use_count() << std::endl;  // 1(只有 next)
    std::cout << "b use_count: " << b.use_count() << std::endl;  // 1(只有 main 持有)

    a->next->print_prev();  // "前驱节点存在"
    a.reset();              // 释放 a
    b->print_prev();        // "前驱节点已释放"
}  // b 正常析构,无内存泄漏

💡 智能指针选择原则

优先栈对象 → 需要堆内存时用 unique_ptr
              → 确实需要共享所有权时用 shared_ptr
                → 需要打破循环引用时用 weak_ptr

这是一个金字塔形选择原则:从上层开始考虑,只有当前层不能满足需求时才降级到下一层。


移动语义:C++11 最重要的革命

Java 程序员完全没有接触过这个概念——移动语义。它是 C++11 引入的最重要的性能特性。

问题:不必要的拷贝

#include <vector>
#include <string>
#include <iostream>

// 返回大量数据时,传统 C++98 会拷贝
std::vector<int> create_large_vector() {
    std::vector<int> v(1000000, 42);
    return v;  // C++98 会拷贝整个数组(一!百!万!个!整!数!)
}

C++11 之前,上面的代码会拷贝整个数组。C++11 之后,编译器会移动而非拷贝。

左值与右值

int a = 10;       // a 是左值(有名字、可取地址、表达式结束后依然存在)
int b = a + 1;    // (a + 1) 是右值(临时对象、表达式结束后不再存在)

根据 changkun 的《现代 C++ 教程》,C++11 将右值进一步划分为:

类别 英文 说明 示例
左值 lvalue 表达式结束后依然存在的持久对象 变量名、函数返回的左值引用
纯右值 prvalue 纯粹的字面量或临时对象 10, true, 1+2, Lambda 表达式
将亡值 xvalue 资源即将被转移的右值 std::move() 的返回值

理解左值和右值的关键区别:左值可以取地址,右值不能。右值引用(&&)可以绑定到右值并"窃取"其资源。

右值引用

#include <iostream>
#include <string>
#include <vector>

void process(int& x) {
    std::cout << "左值引用: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "右值引用: " << x << std::endl;
}

int main() {
    int a = 42;
    process(a);           // 调用左值版本
    process(100);         // 调用右值版本
    process(std::move(a)); // std::move 将 a 转换为右值,调用右值版本
}

std::move:将左值"伪装"成右值

#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<int> v1(1000000, 42);
    std::cout << "v1 size: " << v1.size() << std::endl;  // 1000000

    std::vector<int> v2 = std::move(v1);  // 移动:v1 的资源被"偷"给 v2
    std::cout << "v1 size after move: " << v1.size() << std::endl;  // 0(v1 被置空)
    std::cout << "v2 size: " << v2.size() << std::endl;  // 1000000
}

移动语义的本质:把"拷贝整个数组"变成"拷贝指针 + 置空源对象"——O(1) 操作代替 O(n) 操作。

💡 std::move 实现原理

// 简化版 std::move 实现
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

std::move 实际上什么也不移动——它只是一个强制类型转换,将参数转换为右值引用。移动操作发生在接收右值的移动构造函数或移动赋值运算符中。

移动构造函数(以 std::string 为例)

#include <iostream>
#include <string>

class MyString {
    char* data_;
    size_t size_;
public:
    // 构造函数
    MyString(const char* str) : size_(std::strlen(str)), data_(new char[size_ + 1]) {
        std::copy(str, str + size_ + 1, data_);
        std::cout << "构造" << std::endl;
    }

    // 拷贝构造函数
    MyString(const MyString& other) : size_(other.size_), data_(new char[size_ + 1]) {
        std::copy(other.data_, other.data_ + size_ + 1, data_);
        std::cout << "拷贝构造 (深拷贝)" << std::endl;
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;   // 置空源对象!关键步骤
        other.size_ = 0;
        std::cout << "移动构造 (窃取资源)" << std::endl;
    }

    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data_;            // 释放已有资源
            data_ = other.data_;       // 窃取指针
            size_ = other.size_;
            other.data_ = nullptr;     // 置空源
            other.size_ = 0;
        }
        return *this;
    }

    ~MyString() {
        delete[] data_;
    }

    void print() const {
        if (data_) std::cout << data_ << std::endl;
        else std::cout << "(空)" << std::endl;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2 = s1;              // 拷贝构造(深拷贝)
    MyString s3 = std::move(s1);   // 移动构造(窃取资源,s1 变空)

    s2.print();  // Hello
    s3.print();  // Hello
    s1.print();  // (空)
}

⚠️ 坏味道:不必要的拷贝

void process(const std::vector<int>& v) {
    std::vector<int> copy = v;  // ❌ 不必要地拷贝了整个 vector
    // ... 使用 copy
}

如果确实需要一个副本以便修改,可以按值传参让调用者决定是拷贝还是移动:

void process(std::vector<int> v) {  // ✅ 调用者传左值则拷贝,传右值则移动
    // ... 使用 v
}

完美转发:std::forward

完美转发解决的问题:在函数模板中,将参数以原始的值类别(左值/右值)转发给另一个函数。

#include <iostream>
#include <memory>
#include <utility>

struct Person {
    std::string name;
    int age;

    Person(const std::string& n, int a)
        : name(n), age(a) {
        std::cout << "Person 构造 (拷贝 name)" << std::endl;
    }

    Person(std::string&& n, int a)
        : name(std::move(n)), age(a) {
        std::cout << "Person 构造 (移动 name)" << std::endl;
    }
};

// 转发引用(forwarding reference):T&& 在模板中具有特殊语义
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    // std::forward 保持参数的值类别:
    // 如果原始参数是左值 → Args&& 解析为 Args& → forward 返回左值引用
    // 如果原始参数是右值 → Args&& 解析为 Args&& → forward 返回右值引用
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

int main() {
    std::string name = "Alice";

    // name 是左值 → 调用 Person(const std::string&, int)
    auto p1 = my_make_unique<Person>(name, 30);

    // std::move(name) 是右值 → 调用 Person(std::string&&, int)
    auto p2 = my_make_unique<Person>(std::move(name), 25);
}

💡 转发引用(Forwarding Reference)与右值引用的区别

template<typename T>
void foo(T&& arg);    // T&& 是转发引用(在模板中)

void bar(int&& arg);  // int&& 是右值引用(非模板)

转发引用可以根据实参自动推导为左值引用或右值引用:

  • 传左值时:T 推导为 int&int& && 折叠为 int&
  • 传右值时:T 推导为 intint&& 就是右值引用

这正是完美转发的基础。


编码习惯与坏味道总结

✅ 良好编码习惯

习惯 说明
栈对象优先 除非需要动态生命周期,默认用栈
传参用 const & 避免不必要的拷贝(只读场景)
使用智能指针层级 unique_ptrshared_ptrweak_ptr
使用 make_shared/make_unique 异常安全,代码简洁
RAII 封装资源 互斥锁、文件句柄、数据库连接都用 RAII 包装
移动代替拷贝 对于大对象,用 std::move 转移所有权
返回局部对象用值 编译器会做 RVO(返回值优化)或自动移动
nullptr 而不是 NULL 类型安全,避免重载歧义
Rule of Five / Rule of Zero 要么自定义所有五个特殊成员函数,要么都不自定义

❌ 需要避免的坏味道

坏味道 问题 解决方案
裸指针管理所有权 忘记 delete,异常安全差 unique_ptr
忘记 delete 内存泄漏 智能指针 / RAII
shared_ptr 循环引用 内存泄漏 weak_ptr 打破循环
返回局部变量的引用 悬空引用(UB) 返回值或传给输出参数
不必要的拷贝 性能差 const &,或用 std::move
手动 new/delete 代码啰嗦,易错 make_unique/make_shared
在容器中存裸指针 生命周期管理困难 unique_ptr

完整示例:综合应用

下面是一个综合应用 RAII、智能指针和移动语义的例子:

#include <iostream>
#include <memory>
#include <mutex>
#include <vector>
#include <string>
#include <fstream>

// 1. RAII 封装互斥锁
class SafeCounter {
    mutable std::mutex mtx_;
    int count_ = 0;
public:
    int increment() {
        std::lock_guard<std::mutex> lock(mtx_);
        return ++count_;
    }
};

// 2. RAII 封装文件
class LogFile {
    std::ofstream file_;
public:
    LogFile(const std::string& path) {
        file_.open(path, std::ios::app);
        if (!file_.is_open()) {
            throw std::runtime_error("无法打开日志文件");
        }
    }
    ~LogFile() {
        if (file_.is_open()) {
            file_.close();
        }
    }
    void write(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mtx_);
        file_ << msg << std::endl;
    }
    LogFile(const LogFile&) = delete;
    LogFile& operator=(const LogFile&) = delete;
private:
    std::mutex mtx_;
};

// 3. 智能指针 + 移动语义
struct BigData {
    std::vector<double> data;
    BigData(size_t n) : data(n, 3.14) {
        std::cout << "BigData 构造, size=" << n << std::endl;
    }
    ~BigData() {
        std::cout << "BigData 析构" << std::endl;
    }
};

using BigDataPtr = std::unique_ptr<BigData>;

BigDataPtr create_big_data(size_t n) {
    return std::make_unique<BigData>(n);
}

int main() {
    // RAII 互斥锁
    SafeCounter counter;
    std::cout << "计数: " << counter.increment() << std::endl;

    // RAII 文件
    try {
        LogFile log("app.log");
        log.write("应用启动");
        log.write("处理数据...");
    } catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << std::endl;
    }

    // unique_ptr 移动语义
    auto data = create_big_data(1000);
    auto data2 = std::move(data);  // 转移所有权
    // data 现在为空

    // vector 中的 unique_ptr
    std::vector<BigDataPtr> dataset;
    dataset.push_back(create_big_data(100));
    dataset.push_back(create_big_data(200));

    std::cout << "数据处理完成" << std::endl;
    // 所有资源自动释放
}

原理解析:从 changkun 现代 C++ 教程看右值引用

引用 changkun 的《现代 C++ 教程》中对右值引用的总结:

“右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题,消除了诸如 std::vectorstd::string 之类的额外开销,也才使得函数对象容器 std::function 成为了可能。”

右值引用解决的核心问题:

  1. 临时对象的效率问题:C++ 长期以来被诟病临时对象拷贝开销大,右值引用允许"窃取"临时对象的资源
  2. 完美转发:使得编写能够保持参数值类别的通用转发函数成为可能
  3. 移动语义:使得只可移动(move-only)的类型成为可能,如 std::unique_ptrstd::threadstd::future

核心概念一句话总结:

左值持久,右值短暂;左值引用绑定左值,右值引用绑定右值;move 把左值变右值(允许窃取),forward 保持原始值类别(完美转发)。

下一篇我们将学习 C++ 的面向对象编程,看看和 Java 的类有什么异同。


评论