Appearance
指针、资源与智能指针
指针与引用
| 维度 | 指针 | 引用 |
|---|---|---|
| 本质用法 | 保存对象地址,可间接访问对象 | 已绑定对象的别名语义 |
| 初始化 | 可不初始化(但不应如此),可为 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/delete 与 malloc/free
| 维度 | new / delete | malloc / free |
|---|---|---|
| 属性 | C++ 语言表达式/运算符 | C 标准库函数 |
| 分配 | 分配内存并构造对象 | 仅分配原始字节 |
| 释放 | delete 调析构并释放;数组用 delete[] | free 只释放内存,不调析构 |
| 失败 | 默认 new 抛 std::bad_alloc,new (std::nothrow) 返回空 | 返回 nullptr |
| 类型 | 按目标类型构造 | 在 C++ 中 void* 不能隐式转为其他对象指针 |
必须严格配对:new ↔ delete,new[] ↔ delete[],malloc/calloc/realloc ↔ free。不要混用;对 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::vector、std::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 转移所有权。