1.使用资源句柄和RAII(资源获取即初始化)自动管理资源
RAII的理念很简单,你为资源创建一种代理对象。代理的构造函数获取资源,代理的析构函数释放资源。
RAII的中心思想是,这个代理作为一个局部对象,在作用域结束时自动释放资源。
RAII在C++生态系统中被大量使用,RAII的例子有标准模板库的容器、智能指针和锁。容器管理元素,智能指针管理内存,而锁则管理互斥量。
class ResouceGurad
{
public:explicit ResouceGurad(const std::string& resouce) : resouce_(resouce) {std::cout << "Acquire the " << resouce_ << "." << std::endl;}~ResouceGurad() {std::cout << "Release the " << resouce_ << "." << std::endl;}private:std::string resouce_;
};int main()
{std::cout << '\n';ResouceGurad resGuard1{"memoryBlock1"};std::cout << "\nBefore lock scope.\n";{ResouceGurad resGuard2{"memoryBlock2"};}std::cout << "After lock scope.\n";std::cout << '\n';std::cout << "\nBefore try-catch block.\n";try{ResouceGurad resGuard3{"memoryBlock3"};throw std::bad_alloc{};}catch(const std::bad_alloc& e){std::cerr << e.what() << '\n';}std::cout << "\nAfter try-catch block.\n";std::cout << '\n';
}
2.unique_ptr和shared_ptr表示所有权
std::unique_ptr : 独占所有者
std::shared_ptr : 共享所有者
std::weak_ptr : 对std::shared所管理资源的非占有的引用
std::weak_ptr并不是智能指针。它有一个引用,引用指向被std::shared_ptr所管理的对象。它的接口颇为有限,不可以透明地访问底层资源。通过对std::weak_ptr调用其成员函数lock,可以从某个std::weak_ptr创建出一个std::shared_ptr。
auto sharedPtr = std::make_shared<int>(1998); //引用计数为1
std::weak_ptr<int> weakPtr(sharedPtr); //引用计数为1
auto sharedPtr2 = weakPtr.lock(); //引用计数为2
3.除非需要共享所有权,否则能用unique_ptr就别用shared_ptr
当你需要智能指针的时候,应该首选std::unique_ptr,在设计上,std::unique_ptr和原始指针一样快,且一样可以高效利用内存。
这一结论对于std::shared_ptr则不成立。std::shared_ptr需要管理他的引用计数,并且需要分配额外的内存来维护其控制块。为了管理被控制对象的生存期,控制块是必需的。在你需要共享所有权时std::shared_ptr就能大显身手了,这种情况下,只做一次共享资源的分配,反而可以节省内存和时间。
不要为了做拷贝而贪图方便地使用std::shared_ptr.
void takeUniquePtr(std::unique_ptr<int> p) {std::cout << "*unique: " << *p << std::endl;
}int main()
{std::cout << '\n';auto uniquePtr = std::make_unique<int>(2011);takeUniquePtr(std::move(uniquePtr));auto uniquePtr2 = std::make_unique<int>(2014);auto uniquePtr3 = std::make_unique<int>(2017);std::vector<std::unique_ptr<int>> v;v.push_back(std::move(uniquePtr2));v.push_back(std::move(uniquePtr3));v.push_back(std::make_unique<int>(2020));std::cout << '\n';std::for_each(v.begin(), v.end(), [](std::unique_ptr<int>& p) {std::cout << "*unique: " << *p << std::endl;});
}
4.使用make_shared创建shared_ptr和使用make_unique创建unique_ptr
理由一:异常安全
理由二:只对std::shared_ptr成立
auto sharPtr1 = std::shared_ptr<int>(new int(1998));
auto sharPtr2 = std::shared_ptr<int>(1998);
当你调用std::shared_ptr<int>(new int(1998))时,会发生两次内存分配;一次是针对new int(1998),还有一次是针对std::shared_ptr的控制块。内存分配代价较高,所以尽量避免,std::make_shared<int>(1998)可将两次内存分配变成一次。
5.使用std::weak_ptr来打破shared_ptr形成环
如果std::shared_ptr互相引用,就会形成环状引用。
#include <memory>struct B; // 前向声明struct A {std::shared_ptr<B> b_ptr;
};struct B {std::shared_ptr<A> a_ptr;
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;// 这里 a 和 b 互相引用,导致内存无法释放
}
解决方法:
#include <memory>struct B; // 前向声明struct A {std::shared_ptr<B> b_ptr;
};struct B {std::weak_ptr<A> a_ptr; // 使用 weak_ptr
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;// 现在不会导致内存泄漏
}
6.只在显式表达生存期语义时以智能指针作为参数
函数签名 | 语义 |
---|---|
func(std::unique_ptr<Widget>) | func拿走所有权 |
func(std::unique_ptr<Widget>&) | func要重装Widget |
func(std::shared_ptr<Widget>) | func共享所有权 |
func(std::shared_ptr<Widget>&) | func可能要重装Widget |
func(const std::shared_ptr<Widget>&) | func可能保有一份引用计 |
7.不要传递从智能指针别名中获得的指针或引用。
📌 什么是 "智能指针别名" ?
在 std::shared_ptr
中,我们可以使用 operator*
或 operator->
获取原始指针或引用:
cpp
复制编辑
std::shared_ptr<T> ptr = ...; T* raw_ptr = ptr.get(); // 获取原始指针 T& ref = *ptr; // 获取引用
错误地传递这些指针/引用可能会导致生命周期问题!
🚨 不推荐的错误示例
❌ 1. 传递 shared_ptr
获取的裸指针
#include <iostream>#include <memory>
void process(int* p)
{ // 接收裸指针 std::cout << "Value: " << *p << std::endl;
} int main()
{ std::shared_ptr<int> ptr = std::make_shared<int>(42); process(ptr.get()); // ❌ 传递 get() 返回的裸指针
}
⚠️ 为什么有问题?
如果 ptr
在 process()
执行时已经销毁,p
就会变成悬空指针,导致 未定义行为(UB)。
❌ 2. 传递 shared_ptr
获取的引用
#include <iostream>
#include <memory>void modify(int& x) { // 接收引用x += 10;
}int main() {int* raw_ptr = nullptr;{std::shared_ptr<int> ptr = std::make_shared<int>(42);raw_ptr = ptr.get(); // ❌ 获取裸指针modify(*ptr); // ✅ 此时仍然安全} // `ptr` 离开作用域,控制的对象已被释放modify(*raw_ptr); // ❌ 使用已释放的对象,UB!
}
⚠️ 为什么有问题?
ptr
离开作用域后被销毁,raw_ptr
仍然指向已经释放的内存。modify(*raw_ptr);
访问悬空引用,可能导致程序崩溃。
❌ 3. 在 std::shared_ptr
作用域外存储裸指针
#include <iostream>
#include <memory>int* global_ptr = nullptr;void use_global() {std::cout << "Using global pointer: " << *global_ptr << std::endl; // 可能 UB!
}int main() {{std::shared_ptr<int> ptr = std::make_shared<int>(99);global_ptr = ptr.get(); // ❌ 获取裸指针并存储到全局变量} // `ptr` 离开作用域,内存被释放use_global(); // ❌ 访问悬空指针,UB!
}
⚠️ 为什么有问题?
ptr
释放了对象,但global_ptr
仍然指向旧的内存,导致 悬空指针 访问。
✅ 推荐的正确做法
✔️ 1. 直接传递 std::shared_ptr
void process(std::shared_ptr<int> p) { std::cout << "Value: " << *p << std::endl;
}int main() {std::shared_ptr<int> ptr = std::make_shared<int>(42);process(ptr); // ✅ 传递 shared_ptr,确保生命周期
}
✔️ 2. 使用 const std::shared_ptr<T>&
避免额外拷贝
void process(const std::shared_ptr<int>& p) { std::cout << "Value: " << *p << std::endl;
}int main() {std::shared_ptr<int> ptr = std::make_shared<int>(42);process(ptr); // ✅ 传递引用,避免增加引用计数
}
✔️ 3. 使用 std::weak_ptr
避免循环引用
void process(std::weak_ptr<int> wp) { if (auto p = wp.lock()) { // 检查是否仍然有效std::cout << "Value: " << *p << std::endl;} else {std::cout << "对象已释放!" << std::endl;}
}int main() {std::shared_ptr<int> ptr = std::make_shared<int>(42);process(ptr); // ✅ 使用 weak_ptr 避免不必要的引用增加
}
📌 总结
做法 | 是否推荐 | 原因 |
---|---|---|
std::shared_ptr<T>& | ✅ 推荐 | 避免额外拷贝,保证对象有效 |
const std::shared_ptr<T>& | ✅ 推荐 | 只读访问,不增加引用计数 |
std::weak_ptr<T> | ✅ 推荐 | 避免循环引用,安全访问 |
传递 ptr.get() 返回的裸指针 | ❌ 不推荐 | 可能导致悬空指针 |
传递 *ptr 获取的引用 | ❌ 不推荐 | 可能导致悬空引用 |
在作用域外存储 ptr.get() 的指针 | ❌ 不推荐 | 可能导致未定义行为 |