Skip to content

指针、资源与智能指针

指针与引用

维度指针引用
本质用法保存对象地址,可间接访问对象已绑定对象的别名语义
初始化可不初始化(但不应如此),可为 nullptr声明时必须绑定有效对象;标准 C++ 中不能有空引用
重新绑定可指向另一对象初始化后不能改为引用另一对象
访问*p 解引用,成员用 p->x像原对象一样使用
典型用途可选对象、动态结构、所有权表达、C API函数参数、返回别名、操作符重载

引用不是“没有内存的指针”的语言级保证,编译器实现可不同;应把它理解为更受约束的别名接口。不要解引用空指针,也不要通过构造非法引用来规避类型系统。

const 与指针

从右向左读声明更直观:

cpp
const int* p = &a;       // 指向 const 的指针:*p 不可改,p 可改指向
int* const p2 = &a;      // const 指针:p2 不可改指向,*p2 可改
const int* const p3 = &a; // 两者都不可改

术语“常量指针/指针常量”在口头表达中易混淆,面试时直接说明“指向的值是否能改、指针是否能改指向”最清楚。

new/deletemalloc/free

维度new / deletemalloc / free
属性C++ 语言表达式/运算符C 标准库函数
分配分配内存并构造对象仅分配原始字节
释放delete 调析构并释放;数组用 delete[]free 只释放内存,不调析构
失败默认 newstd::bad_allocnew (std::nothrow) 返回空返回 nullptr
类型按目标类型构造在 C++ 中 void* 不能隐式转为其他对象指针

必须严格配对:newdeletenew[]delete[]malloc/calloc/reallocfree。不要混用;对 C++ 非平凡对象使用 malloc 通常是错误设计。现代 C++ 更推荐容器、值对象和智能指针,而非显式 new

delete 后变量仍保存旧地址,继续解引用是未定义行为。置为 nullptr 可避免该变量被再次误用,但无法同步清空其他别名。

智能指针与 RAII

RAII 指“资源获取即初始化”:把资源的生命周期绑定到对象析构。智能指针是其典型工具。

  • std::unique_ptr<T>:独占所有权,不可拷贝、可移动;作用域结束时删除对象。默认首选。
  • std::shared_ptr<T>:共享所有权,控制块维护强引用计数;最后一个强引用离开时销毁对象。
  • std::weak_ptr<T>:观察 shared_ptr 管理的对象,不增加强计数;用 lock() 暂时取得 shared_ptr,用于打破循环引用。
cpp
struct Node {
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent; // 防止 parent 与 child 的强引用环
};

std::make_unique / std::make_shared 通常优于直接 new:异常安全、表达简洁,后者通常还能减少一次分配。unique_ptr 管理的是动态对象或自定义资源,不是“管理栈上对象”。

智能指针的引用计数通常是线程安全的,但它管理的对象并不会因此自动线程安全。weak_ptr::lock()如果对象还活着,就原子地拿到一份 shared_ptr;否则返回空指针。

cpp
std::weak_ptr<Node> weak = /* ... */;

if (auto strong = weak.lock()) {
    // lock() 原子地检查:
    // 1. 对象是否还存活;
    // 2. 若存活,将强引用计数 +1;
    // 3. 返回可安全使用对象的 shared_ptr。
    strong->do_work();
} else {
    // 对象已销毁
}

内存泄漏与悬空/野指针

  • 内存泄漏:已分配资源仍不可达或永不释放,导致可用资源持续减少。
  • 悬空指针(dangling pointer):仍指向已销毁/释放对象,例如 delete 后未清理的指针、返回局部对象地址。
  • 野指针常泛指未初始化、无效或悬空的指针。

预防:优先使用 RAII;清楚表达所有权;不要返回局部对象的地址/引用;基类多态删除时使用虚析构;避免 shared_ptr 循环;使用 AddressSanitizer、LeakSanitizer、Valgrind(Linux)或 Visual Studio 诊断工具检测。

深拷贝与浅拷贝

浅拷贝仅复制成员值;若成员是拥有资源的裸指针,副本会指向同一资源,可能双重释放。深拷贝为资源创建独立副本。

自定义 RAII 包装与三/五/零法则

优先让标准类型管理资源;若必须封装 C API 的 malloc 内存、文件描述符或句柄,应把释放操作放入析构函数,并让所有权语义明确:

cpp
#include <cstddef>
#include <cstdlib>
#include <new>
#include <utility>

class MallocBuffer {
public:
    explicit MallocBuffer(std::size_t size) : size_(size), data_(std::malloc(size)) {
        // malloc(0) 可以合法返回空指针;只有正长度的空结果才是失败。
        if (size != 0 && data_ == nullptr) throw std::bad_alloc{};
    }

    ~MallocBuffer() { std::free(data_); }

    MallocBuffer(const MallocBuffer&) = delete;
    MallocBuffer& operator=(const MallocBuffer&) = delete;

    MallocBuffer(MallocBuffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {}

    MallocBuffer& operator=(MallocBuffer&& other) noexcept {
        if (this != &other) {
            std::free(data_);
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

    void* data() const noexcept { return data_; }
    std::size_t size() const noexcept { return size_; }

private:
    std::size_t size_ = 0;
    void* data_ = nullptr;
};
  • 三法则:若必须自定义析构、拷贝构造、拷贝赋值中的一个,通常要审视另外两个;
  • 五法则:C++11 后还要考虑移动构造、移动赋值;
  • 零法则:优先让 std::vectorstd::string、智能指针等成员承担资源管理,类自己不定义这些特殊成员函数。这是现代 C++ 的首选。

上例的 malloc 内存只是原始字节,不会构造 C++ 对象;如果需要存放非平凡类型,应使用标准容器或正确的 allocator/placement new 生命周期管理,而不是直接强转后写入。

enable_shared_from_this

当对象已经由 shared_ptr 管理,成员函数又需要安全地取得“指向自己”的 shared_ptr(例如注册异步回调保持对象存活)时,可公开继承 std::enable_shared_from_this<T>

cpp
class Session : public std::enable_shared_from_this<Session> {
public:
    void start_async() {
        auto self = shared_from_this(); // 与现有 shared_ptr 共用控制块
        // executor.post([self] { self->handle_event(); });
    }

    static std::shared_ptr<Session> create() {
        return std::make_shared<Session>();
    }
};

enable_shared_from_this 内部维护一个 weak 关联;对象首次被正确的 shared_ptr 接管后,shared_from_this() 才能提升为共享所有权。常见错误:

  • 在构造函数中调用 shared_from_this():控制块关联尚未完成,通常抛 std::bad_weak_ptr
  • 对已被 shared_ptr 管理的裸 this 再构造 shared_ptr(this):会建立第二个控制块,导致双重释放;
  • 对栈对象调用 shared_from_this():没有 shared_ptr 所有者,同样失败。

C++17 的 weak_from_this() 在未被接管时返回空 weak_ptr,适合需要探测式处理的场景。

std::auto_ptr 的拷贝会隐式转移所有权,容易产生意外空指针,已在 C++17 移除;使用 std::unique_ptr,它要求显式 std::move 转移所有权。

使用 Markdown 与 VitePress 构建