引言

C++作为一种系统级编程语言,手动管理内存是其核心特性之一。然而,手动管理内存也带来了两个经典问题:

内存泄漏:程序员忘记释放动态分配的内存,导致程序运行期间不断消耗系统资源。

共享所有权问题:当多个代码路径需要访问同一个对象时,如何确保对象在所有使用者都不再需要时才释放?

C++11之前,程序员只能依靠纪律和代码规范来避免这些问题。C++11引入的智能指针则从语言层面提供了自动、可靠的内存管理方案。


一、智能指针概述

1.1 四种智能指针对比

指针类型 C++标准 所有权模式 能否复制 能否移动 典型场景
auto_ptr C++98 独占转移 不能(转移后原指针失效) 可以 已废弃,不推荐使用
unique_ptr C++11 独占拥有 不能 可以 独占资源,一个对象只被一个拥有者管理
shared_ptr C++11 共享引用 可以 可以 共享所有权,多个对象共享同一资源
weak_ptr C++11 非拥有观察 可以(从shared_ptr构造) 可以 打破循环引用,或临时观察shared_ptr资源

1.2 基本原理

智能指针的核心思想很简单:把指针封装成一个栈上对象,当栈对象析构时自动释放堆内存。

栈上对象生命周期结束时自动析构
         |
         v
    调用delete释放堆内存

这利用了C++的基本规则:局部对象(栈对象)在离开作用域时自动析构。


二、shared_ptr详解

2.1 基本结构

shared_ptr的实现包含两部分:

  1. 指向堆对象的裸指针(raw pointer):指向用户实际分配的对象
  2. 共享控制块(control block):包含引用计数器和删除器
shared_ptr对象
+------------------+
| raw_ptr          | -----> 堆上的T对象
| control_block    | -----> +------------------+
+------------------+        | reference_count |
                             | deleter         |
                             | weak_count      |
                             +------------------+

2.2 创建方式

// 方式1:直接构造
std::shared_ptr<int> sp1(new int(100));

// 方式2:使用make_shared(推荐,更高效)
std::shared_ptr<int> sp2 = std::make_shared<int>(100);

// 方式3:指定删除器
void DeleteIntPtr(int* p) {
    std::cout << "custom deleter called" << std::endl;
    delete p;
}
std::shared_ptr<int> sp3(new int(200), DeleteIntPtr);

重要原则:永远不要把同一个裸指针同时赋给多个shared_ptr。

// 错误示例
int* raw = new int(100);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw);  // 危险!sp1和sp2各自认为自己是唯一拥有者
// 当sp1析构时delete raw,sp2就成为了野指针

// 正确做法
std::shared_ptr<int> sp1(new int(100));
std::shared_ptr<int> sp2 = sp1;  // 正确,引用计数变为2

2.3 线程安全问题

shared_ptr本身的引用计数是线程安全的,但指向的对象数据修改不是线程安全的

reference_count:原子操作,多线程安全
T对象数据修改:需要额外加锁保护
std::shared_ptr<int> sp = std::make_shared<int>(100);

// 安全:多个线程同时读写sp本身
// 不安全:多个线程同时通过sp修改int值,需要加锁

2.4 常用操作

std::shared_ptr<int> sp = std::make_shared<int>(100);

// 获取原始指针(慎用!)
int* raw = sp.get();  // 可能导致二次释放

// 重置shared_ptr
sp.reset();           // 释放资源,sp变为空
sp.reset(new int(200)); // 重新绑定到新对象

// 获取引用计数
std::cout << sp.use_count() << std::endl;  // 输出1

// 检查是否为空
if (sp) {
    std::cout << "sp is valid" << std::endl;
}

2.5 enable_shared_from_this

当你需要在对象内部获取一个shared_ptr指向自身时,不能直接用this构造shared_ptr,而应该让类继承std::enable_shared_from_this。

class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getShared() {
        return shared_from_this();  // 返回指向自身的shared_ptr
    }
};

// 正确用法
std::shared_ptr<Node> node = std::make_shared<Node>();
std::shared_ptr<Node> node2 = node->getShared();  // 引用计数变为2

// 错误用法(会导至双重析构)
Node* raw = new Node();
std::shared_ptr<Node> sp1(raw);
std::shared_ptr<Node> sp2(raw);  // 两次析构同一对象

2.6 循环引用问题

循环引用会导致两个对象都无法释放。

class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

void testCycleRef() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b;  // B引用计数变为1
    b->a_ptr = a;  // A引用计数变为1
    
    // 离开作用域时:
    // a引用计数从1减为0,析构A,但B的引用计数不能减为0(A还持有B的shared_ptr)
    // b引用计数从1减为0,析构B,但A的引用计数不能减为0(B还持有A的shared_ptr)
    // 结果:两个对象都无法释放,内存泄漏
}

三、weak_ptr详解

3.1 概念

weak_ptr是一种不控制对象生命周期的智能指针。它指向一个shared_ptr管理的对象,但不影响该对象的引用计数。

shared_ptr: 强引用,影响对象生命周期
weak_ptr:   弱引用,不影响对象生命周期

3.2 主要用途

用途一:打破循环引用

class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;  // 换成weak_ptr即可解决循环引用
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

用途二:临时观察对象

class Observer {
    std::weak_ptr<Data> data_ptr;  // 观察但不拥有
public:
    void observe(const std::shared_ptr<Data>& d) {
        data_ptr = d;
    }
    void process() {
        // 检查资源是否仍存在
        if (auto sp = data_ptr.lock()) {  // lock()返回shared_ptr或空
            // 数据仍然有效,可以安全使用
            sp->doSomething();
        } else {
            // 数据已释放
            std::cout << "data already released" << std::endl;
        }
    }
};

3.3 常用方法

std::shared_ptr<int> sp = std::make_shared<int>(100);
std::weak_ptr<int> wp = sp;

// 检查资源是否已释放
if (wp.expired()) {  // 等价于 use_count() == 0
    std::cout << "resource released" << std::endl;
}

// 获取shared_ptr(如果资源已释放,返回空的shared_ptr)
if (auto sp2 = wp.lock()) {
    std::cout << "resource valid, value = " << *sp2 << std::endl;
} else {
    std::cout << "resource already released" << std::endl;
}

// 获取引用计数(不常用)
std::cout << wp.use_count() << std::endl;  // 1

四、unique_ptr详解

4.1 独占特性

unique_ptr拥有独占的所有权,不能复制,只能移动。

std::unique_ptr<int> up1(new int(100));

// 不能复制,编译失败
// std::unique_ptr<int> up2 = up1;  // 错误

// 可以移动
std::unique_ptr<int> up2 = std::move(up1);  // up1变为空,up2拥有资源

// 可以作为函数返回值
std::unique_ptr<int> create() {
    return std::unique_ptr<int>(new int(200));
}

4.2 自定义删除器

void FileDeleter(FILE* f) {
    if (f) fclose(f);
}

std::unique_ptr<FILE, decltype(&FileDeleter)> 
    file_ptr(fopen("test.txt", "r"), FileDeleter);

4.3 与shared_ptr的关系

std::unique_ptr<int> up(new int(100));

// 可以转换为shared_ptr
std::shared_ptr<int> sp = std::move(up);  // up变为空

但反向转换不行:shared_ptr不能赋给unique_ptr。


五、右值引用与移动语义

5.1 左值与右值

左值(lvalue):可以取地址、位于等号左边的表达式。

int a = 5;  // a是左值,可以取地址
int& ref_a = a;  // 正确,左值引用指向左值

右值(rvalue):不能取地址、位于等号右边的表达式。

int& ref_wrong = 5;  // 错误,不能用左值引用指向右值

const左值引用可以指向右值(这是C++98就有的特性):

const int& ref = 6;  // 正确,const引用延长右值生命周期

5.2 右值引用

C++11引入右值引用,使用&&语法:

int a = 5;
int&& rref = 5;  // 正确,右值引用指向右值

// 使用std::move将左值转为右值
int&& rref2 = std::move(a);  // 正确,move后a的内容可以"移动"给其他对象

5.3 移动语义

移动语义的核心是避免不必要的内存拷贝。当你"移动"一个对象时,实际上是把资源的所有权转移,而不是复制。

class Buffer {
private:
    char* data;
    size_t size;
public:
    // 拷贝构造函数(深拷贝)
    Buffer(const Buffer& other) : size(other.size) {
        data = new char[size];
        memcpy(data, other.data, size);
    }
    
    // 移动构造函数
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;  // 原对象不再拥有资源
        other.size = 0;
    }
};

// 使用效果
Buffer b1(1000000);  // 分配大内存
Buffer b2 = b1;      // 调用拷贝构造函数,深拷贝1MB数据
Buffer b3 = std::move(b1);  // 调用移动构造函数,只需移动指针,不拷贝数据
// 注意:移动后b1.data变为nullptr,使用b1会导致未定义行为

5.4 std::move的作用

std::move只是一个类型转换工具,它本身不移动任何东西:

template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) {
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

std::move的作用是将左值"转换"为右值引用,从而触发移动语义。

5.5 完美转发

std::forward可以在函数模板中保持参数的原始值类别(左值或右值):

void process(int&) { std::cout << "lvalue" << std::endl; }
void process(int&&) { std::cout << "rvalue" << std::endl; }

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));  // 保持原始值类别
}

int a = 5;
wrapper(a);      // 调用lvalue版本
wrapper(5);      // 调用rvalue版本

5.6 emplace系列函数

emplace操作可以直接在容器中构造对象,避免不必要的拷贝和移动:

std::vector<std::string> v;

// push_back需要先构造对象,再拷贝/移动
v.push_back(std::string("hello"));  // 先构造临时string,再移动

// emplace直接在容器内构造
v.emplace_back("hello");  // 直接在vector内构造string,避免额外操作

// 结合move进一步优化
std::string s = getLargeString();
v.emplace_back(std::move(s));  // 移动而非拷贝

六、lambda表达式

6.1 基本语法

[捕获列表](参数列表) mutable(可选)异常属性 -> 返回类型 {
    // 函数体
}

6.2 捕获方式

int x = 10;
int y = 20;

// 按值捕获
auto f1 = [x](int a) { return a + x; };  // 拷贝x的值

// 按引用捕获(可修改外部变量)
auto f2 = [&x, y](int a) mutable { 
    x = 100;  // 可以修改x
    return a + x + y; 
};

// 隐式捕获
auto f3 = [=]() { return x + y; };      // 按值捕获所有外部变量
auto f4 = [&]() { return x + y; };      // 按引用捕获所有外部变量
auto f5 = [&, y]() { return x + y; };   // 除y按值外,其他按引用
auto f6 = [=, &x]() { return x + y; };  // 除x按引用外,其他按值

6.3 示例

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
    
    // 使用lambda排序
    std::sort(v.begin(), v.end(), [](int a, int b) {
        return a > b;  // 降序排列
    });
    
    // 使用lambda查找
    auto it = std::find_if(v.begin(), v.end(), [](int n) {
        return n > 5;
    });
    
    // 完整lambda语法示例
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    
    std::cout << add(3, 4) << std::endl;  // 输出7
    
    return 0;
}

6.4 泛型lambda(C++14)

auto generic = [](auto x, auto y) {
    return x + y;
};

int a = generic(1, 2);        // int
std::string s = generic(std::string("hello"), std::string(" world"));  // string

七、综合示例

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

class DataBuffer {
private:
    std::vector<int> data;
public:
    DataBuffer(size_t size) : data(size, 0) {
        std::cout << "constructor called, size=" << size << std::endl;
    }
    
    ~DataBuffer() {
        std::cout << "destructor called" << std::endl;
    }
    
    void fill(int value) {
        std::fill(data.begin(), data.end(), value);
    }
    
    size_t size() const { return data.size(); }
};

int main() {
    // shared_ptr示例
    std::cout << "=== shared_ptr示例 ===" << std::endl;
    {
        std::shared_ptr<DataBuffer> sp1 = std::make_shared<DataBuffer>(100);
        std::shared_ptr<DataBuffer> sp2 = sp1;
        
        std::cout << "use_count: " << sp1.use_count() << std::endl;  // 2
        
        sp2->fill(42);
        std::cout << "sp1->size(): " << sp1->size() << std::endl;   // 100
        
        sp1.reset();  // 引用计数变为1
        std::cout << "after reset, use_count: " << sp2.use_count() << std::endl;  // 1
        
    }  // sp2析构,DataBuffer最终释放
    
    // unique_ptr示例
    std::cout << "\n=== unique_ptr示例 ===" << std::endl;
    {
        std::unique_ptr<int> up1(new int(100));
        std::unique_ptr<int> up2 = std::move(up1);
        
        std::cout << "*up2: " << *up2 << std::endl;  // 100
        // std::cout << *up1 << std::endl;  // 未定义行为,up1已为空
        
        // 可以转换为shared_ptr
        std::shared_ptr<int> sp = std::move(up2);
        std::cout << "*sp: " << *sp << std::endl;  // 100
    }
    
    // 移动语义示例
    std::cout << "\n=== 移动语义示例 ===" << std::endl;
    {
        std::vector<std::string> v;
        std::string s = "hello world";  // 长字符串
        
        v.push_back(s);              // 拷贝,s不变
        v.push_back(std::move(s));   // 移动,s变为空
        
        std::cout << "v[0]: " << v[0] << std::endl;
        std::cout << "v[1]: " << v[1] << std::endl;
        std::cout << "s after move: \"" << s << "\"" << std::endl;  // 空字符串
    }
    
    return 0;
}

编译运行:

g++ -std=c++11 -o main main.cpp
./main

输出:

=== shared_ptr示例 ===
constructor called, size=100
use_count: 2
sp1->size(): 100
after reset, use_count: 1
destructor called

=== unique_ptr示例 ===
*up2: 100
*sp: 100

=== 移动语义示例 ===
v[0]: hello world
v[1]: hello world
s after move: ""

八、常见面试追问

问题 解答
shared_ptr的引用计数是线程安全的吗? 是的,引用计数的增减都是原子操作。但指向的对象本身修改不是线程安全的,需要额外加锁。
能否把同一个裸指针赋给两个shared_ptr? 绝对不能。这会导致双重析构,应该使用shared_ptr的复制或make_shared创建。
shared_ptr和unique_ptr的主要区别是什么? shared_ptr共享所有权,引用计数决定释放时机;unique_ptr独占所有权,不能复制只能移动。
weak_ptr什么时候使用? 主要用于打破shared_ptr的循环引用,或者临时观察某个资源但不拥有。
std::move和移动构造的区别是什么? std::move是将左值转换为右值引用的工具函数;移动构造是类的一种构造函数,利用右值引用实现资源的转移。
lambda的捕获列表和参数列表有什么区别? 捕获列表用于捕获外部变量(类成员或全局作用域的变量),参数列表是lambda的输入参数。捕获发生在lambda创建时,参数传递发生在lambda调用时。
emplace_back和push_back有什么区别? push_back通常先创建临时对象,再拷贝或移动到容器中;emplace_back直接在容器内构造对象,避免额外的拷贝或移动。

九、总结

特性 核心要点
智能指针 利用栈对象自动析构特性,自动释放堆内存
shared_ptr 共享所有权,引用计数为0时析构,注意循环引用问题
unique_ptr 独占所有权,不能复制只能移动,性能优于shared_ptr
weak_ptr 不控制生命周期,用于打破循环引用或临时观察
右值引用 使用&&语法,让右值可以被引用
移动语义 资源所有权转移,避免不必要的深拷贝
std::move 类型转换工具,将左值转为右值引用
std::forward 保持参数的原始值类别(左值/右值)
lambda 匿名函数对象,捕获列表实现闭包
emplace 直接构造,避免额外拷贝移动

C++11的这些特性大幅改进了C++的性能表现。右值引用和移动语义减少了不必要的内存拷贝,智能指针自动管理内存避免了泄漏和重复释放,lambda表达式让代码更加简洁和表达力强。


根据零声教育教学写作https://github.com/0voice

更多推荐