如果你曾经在C++的海洋中遨游,那么你一定遇到过那些令人头疼的内存管理问题:忘记 delete 导致的内存泄漏,或者对已经释放的内存进行操作(悬空指针)导致的程序崩溃。这些问题不仅难以调试,而且是C++程序稳定性的主要威胁。
幸运的是,C++标准库为我们提供了强大的工具来自动化内存管理,它们就是——智能指针。今天,我们就来深入了解这些现代C++编程的基石。
为什么我们需要智能指针?(传统指针的痛点)
在智能指针出现之前,我们通常这样管理动态分配的内存:
void old_school_memory_management(){
int *raw_ptr = new int(10); //分配内存
// ... 使用raw_ptr ...
if (*raw_ptr>5){
std::cout<< "Value is "<< *raw_ptr <<std::endl;
//如果在这里提前返回或者发生异常,delete就不会被执行!
//return;
}
delete raw_ptr; //手动释放内存
raw_ptr = nullptr; //好习惯;放置悬空指针
}
这种方式存在几个显而易见的问题:
1.内存泄露:如果忘记delete,或者在delete执行前因为异常、提前返回等原因跳出了作用域,那么分配的内存就永远无法回收。
2.悬空指针:当一个指针指向的内存已经被释放,但指针本省没有被置空,此时它就成了悬空指针。再次使用它就会导致未定义行为。
3.重复释放:对同一块内存执行两次delete也会导致程序崩溃。
4.所有权混乱:当多个指针指向同一块内存时,谁负责释放它?这很容易造成混乱。
智能指针的出现,正是为了解决这些问题。它们利用了C++的RAII(Resource Acquisition Is Initialization)技术,即资源(这里是动态内存)在对象构造时获取,在对象析构时自动释放。
智能指针的核心成员
C++11标准库引入了三种主要的智能指针,它们都定义在<memory>头文件中:
1.std::unique_ptr:独占所有权的智能指针。
2.std::shared_ptr:共享所有权的智能指针。
3.std::weak_ptr:一种弱引用,配合std::shared_ptr使用,用于解决循环应用的问题。
让我们逐一深入的了解它们。
1.std::unique_ptr(独占指针)
std::unique_ptr 对其指向的对象拥有独占所有权。这意味着在任何时刻,最多只有一个unique_ptr指向给定的对象。当unique_ptr被销毁时(例如离开作用域),它所指向的对象也会被自动删除。
特点:
轻量级:性能开销几乎与原始指针相同。
不可复制:你不能通过拷贝构造函数或赋值操作符来复制一个unique_ptr。
std::unique_ptr<int> ptrl(new int(10));
//std::unique_ptr<int> ptr2 = ptr1; //编译错误!
//std::unique_ptr<int> ptr3;
//ptr3 = ptr1; //编译错误!
可移动:所有权可以通过std::move从一个unique_ptr 转移到另一个。
std::unique_ptr<int> ptr1(new int(10));
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1 失去所有权,变为 nullptr
// ptr2 现在拥有对象
if (ptr1) {
std::cout << "ptr1 still valid (this won't happen)\n";
}
if (ptr2) {
std::cout << "ptr2 has value: " << *ptr2 << std::endl; // 输出 10
}
如何创建和使用?
推荐使用std::make_unique(C++14及以后版本)来创建unique_ptr,因为它更安全且简洁:
#include <iostram>
#include <memory> //必须包含
class MyClass{
public:
MyClass(int v): value(v) {std<<cout << "MyClass(" << value << "constructed. \n";}
~MyClass() {std::cout<< "MyClass(" << value << ") destructed. \n";}
void print() const {std::cout << "Value: " << value << std::endl;}
private:
int value;
};
int main(){
//使用std::make_unique (推荐,C++14+)
std::unique_ptr<MyClass> u_ptr1 = std::make_unique<MyClass>(100);
u_ptr1->print(); //像普通指针一样使用 -> 和 *
// 如果没有C++14,可以这样创建(但不推荐直接使用 new)
// std::unique_ptr<MyClass> u_ptr2(new MyClass(200));
// u_ptr2->print();
// 转移所有权
std::unique_ptr<MyClass> u_ptr3 = std::move(u_ptr1);
if(!u_ptr1){
std::cout << "u_ptr1 is now null. \n";
}
if(u_ptr3) {
u_ptr3->print();
}
// u_ptr3 在main函数结束时离开作用域,MyClass(100)对象会自动析构
// u_ptr1 已经是nullptr, 不会做任何事
return 0;
}
//输出:
// MyClass(100) constructed.
// Value: 100
// u_ptr1 is now null.
// Value: 100
// MyClass(100) destructed.
unique_ptr也支持数组:
std::unique_ptr<int[]> arr_ptr = std::make_unique<int[]>(5); // 创建一个包含5个整数的数组
for (int i = 0; i < 5; ++i) {
arr_ptr[i] = i * 10;
std::cout << arr_ptr[i] << " "; // 0 10 20 30 40
}
std::cout << std::endl;
// arr_ptr 离开作用域时,数组会被 delete[] 自动删除
自定义删除器:
unique_ptr还可以指定一个自定义的删除器,用于在对象不再需要时执行特定的清理操作,而不仅仅是delete.
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) {
std::cout << "Closing file.\n";
fclose(fp);
}
}
};
// 使用 fopen 打开文件,并用 unique_ptr 管理
std::unique_ptr<FILE, FileCloser> file_ptr(fopen("test.txt", "w"), FileCloser());
if (file_ptr) {
fprintf(file_ptr.get(), "Hello unique_ptr with custom deleter!\n");
}
// file_ptr 离开作用域时,FileCloser::operator() 会被调用,文件自动关闭
何时使用unique_ptr?
当你需要一个指针来管理动态分配的资源,并且明确这个资源只有一个所有者时,unique_ptr是首选。例如,工厂函数返回的对象,或者类成员指向的独占资源。
2.std::shared_ptr(共享指针)
std::shared_ptr实现了共享所有权。多个shared_ptr可以指向同一个对象。它内部维护了一个引用计数,记录有多少个shared_ptr指向该对象。当最后一个指向对象的shared_ptr被销毁或重置时,对象才会被删除。
特点:
引用计数:这是shared_ptr 的核心机制
可复制:复制一个shared_ptr 会增加引用计数
std::shared_ptr<int> s_ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> s_ptr2 = s_ptr1; // 复制,引用计数变为 2
std::cout << "s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 输出 2
std::cout << "s_ptr2 use_count: " << s_ptr2.use_count() << std::endl; // 输出 2
线程安全:引用计数本身的增减是原子操作,因此在多线程环境下,对引用计数的操作是安全的。但对所管理对象的访问不是线程安全的,需要用户自己同步。
如何创建和使用?
同样,推荐使用std::make_shared 来创建shared_ptr:
#include <iostream>
#include <memory>
#include <vector>
// 使用上面定义的 MyClass
void process_widget(std::shared_ptr<MyClass> sp) {
std::cout << "In process_widget, use_count: " << sp.use_count() << std::endl;
sp->print();
// sp 离开作用域,引用计数减 1
}
int main() {
// 使用 std::make_shared (推荐)
std::shared_ptr<MyClass> s_ptr1 = std::make_shared<MyClass>(200);
std::cout << "After make_shared, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 1
{
std::shared_ptr<MyClass> s_ptr2 = s_ptr1; // 复制
std::cout << "s_ptr2 created, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 2
s_ptr2->print();
process_widget(s_ptr1); // 传递 shared_ptr,引用计数会增加
std::cout << "After process_widget, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 2
} // s_ptr2 离开作用域,引用计数减 1
std::cout << "After s_ptr2 scope, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 1
// s_ptr1 在 main 函数结束时离开作用域,引用计数变为 0,MyClass(200) 对象会自动析构
return 0;
}
//输出:
// MyClass(200) constructed.
// After make_shared, s_ptr1 use_count: 1
// s_ptr2 created, s_ptr1 use_count: 2
// Value: 200
// In process_widget, use_count: 3
// Value: 200
// After process_widget, s_ptr1 use_count: 2
// After s_ptr2 scope, s_ptr1 use_count: 1
// MyClass(200) destructed.
为什么make_shared比shared_ptr<T>(new T())更好?
make_shared 通常更高效,因此它能一次性分配对象本身和shared_ptr控制块(包含引用计数等)所需的内存。而shared_ptr<T>(new T())会有两次内存分配(一次new T(),一次为控制块),这可能导致性能稍差,并且在某些复杂表达式中存在微小的异常安全风险。
何时使用shared_ptr?
当你需要多个指针共享同一个对象的所有权,例如,在数据结构中(如链表、图的节点,但要注意循环引用),或者当对象的生命周期需要多个独立的观察者共同决定时。
3.std::weak_ptr(弱指针)
std::weak_ptr是一种非拥有型智能指针。它指向由shared_ptr管理的对象,但不会增加对象的引用计数。weak_ptr主要用于解决shared_ptr可能导致的循环引用的问题。
什么是循环引用?
考虑两个对象A和B,它们都使用shared_ptr互相指向对方:
struct B; // 前向声明
struct A {
std::shared_ptr<B> b_ptr;
A() { std::cout << "A constructed\n"; }
~A() { std::cout << "A destructed\n"; }
};
struct B {
std::shared_ptr<A> a_ptr;
B() { std::cout << "B constructed\n"; }
~B() { std::cout << "B destructed\n"; }
};
void cycle_example() {
std::shared_ptr<A> spa = std::make_shared<A>(); // A的引用计数为1
std::shared_ptr<B> spb = std::make_shared<B>(); // B的引用计数为1
spa->b_ptr = spb; // B的引用计数变为2 (spb 和 spa->b_ptr)
spb->a_ptr = spa; // A的引用计数变为2 (spa 和 spb->a_ptr)
std::cout << "spa use_count: " << spa.use_count() << std::endl; // 2
std::cout << "spb use_count: " << spb.use_count() << std::endl; // 2
} // spa 和 spb 离开作用域,A 和 B 的引用计数都从 2 减为 1
// 但由于 A 持有 B,B 持有 A,它们的引用计数永远不会降到 0
// 结果:A 和 B 都不会被析构,造成内存泄漏!
在这个例子中,当spa和spb离开作用域时,A和B对象的引用计数都变成了1(因为它们内部互相只有对方的shared_ptr)。由于引用计数不为0,它们的析构函数永远不会被调用,导致内存泄露。
weak_ptr如何解决?
如果其中一个(或两个)指针使用weak_ptr,循环就被打破了。
#include <iostream>
#include <memory>
struct BetterB;
struct BetterA {
std::shared_ptr<BetterB> b_ptr_shared; // A 强引用 B
// 或者,如果 A 只需要观察 B,不拥有 B
// std::weak_ptr<BetterB> b_ptr_weak;
BetterA() { std::cout << "BetterA constructed\n"; }
~BetterA() { std::cout << "BetterA destructed\n"; }
void set_b(std::shared_ptr<BetterB> b) { b_ptr_shared = b; }
};
struct BetterB {
std::weak_ptr<BetterA> a_ptr_weak; // B 弱引用 A,打破循环
BetterB() { std::cout << "BetterB constructed\n"; }
~BetterB() { std::cout << "BetterB destructed\n"; }
void set_a(std::shared_ptr<BetterA> a) { a_ptr_weak = a; }
void check_a() {
if (auto locked_a = a_ptr_weak.lock()) { // 尝试获取一个 shared_ptr
std::cout << "BetterA is still alive. Use count: " << locked_a.use_count() << std::endl;
// 可以安全使用 locked_a
} else {
std::cout << "BetterA has been destroyed.\n";
}
}
};
int main() {
std::shared_ptr<BetterA> spa = std::make_shared<BetterA>();
std::shared_ptr<BetterB> spb = std::make_shared<BetterB>();
spa->set_b(spb); // A 指向 B (shared_ptr), B 的引用计数 +1
spb->set_a(spa); // B 指向 A (weak_ptr), A 的引用计数不变
std::cout << "spa use_count: " << spa.use_count() << std::endl; // 1 (只有 spa 自己)
std::cout << "spb use_count: " << spb.use_count() << std::endl; // 2 (spb 和 spa->b_ptr_shared)
spb->check_a();
// 当 spa 和 spb 离开作用域:
// 1. spa 析构,A 的引用计数变为 0,A 被销毁。
// 2. A 销毁时,其成员 spa->b_ptr_shared 析构,B 的引用计数减 1。
// 3. spb 析构,B 的引用计数再减 1,变为 0,B 被销毁。
// 没有内存泄漏!
return 0;
}
// BetterA constructed
// BetterB constructed
// spa use_count: 1
// spb use_count: 2
// BetterA is still alive. Use count: 2
// BetterA destructed
// BetterB destructed
如何使用weak_ptr?
weak_ptr不能直接访问对象,因为它不保证对象仍然存在。要访问对象,必须调用lock()方法。lock()会检查对象是否存在,如果存在,它返回一个指向该对象的shared_ptr(增加引用计数,保证在shared_ptr存在期间对象有效)。如果对象已被销毁,他返回一个空的shared_ptr。相对应的也可以用expired()方法检查对象是否已被销毁,但通常直接用lock()的结果判断更方便。
何时使用weak_ptr?
1.打破shared_ptr的循环引用:这是最常见的用途。
2.观察对象:当你需要一个指向对象的指针,但不希望影响其生命周期时。例如,在缓存实现中,缓存项可以被weak_ptr引用,当对象不再被其他地方需要时,缓存项可以安全地失效。
选择合适的智能指针
默认使用std::unique_ptr,它是最轻量级且最能清晰表达所有权的。只有当你确定需要共享所有权时,才考虑其他选项。
当你需要多个地方共享对象的所有权,并且对象的生命周期由最后一个引用者决定时,使用std::shared_ptr。
当你有一个shared_ptr指向的对象,但你需要一个非拥有型的指针来观察它(通常是为了打破循环引用),使用std::weak_ptr。
智能指针使用最佳实践
优先使用std::make_unique和std::make_shared:
- std::unique_ptr<MyClass> p = std::make_unique<MyClass>();
- std::shared_ptr<MyClass> p = std::make_shared<MyClass>();
它们比直接使用 new 更安全(异常安全)和高效(特别是 make_shared)。
不要混用原始指针和智能指针来管理同一个对象的所有权:
MyClass* raw_ptr = new MyClass(1);
std::shared_ptr<MyClass> sp1(raw_ptr);
// std::shared_ptr<MyClass> sp2(raw_ptr); // 错误!会导致 double free
// sp1 和 sp2 会有各自的引用计数,都以为自己是唯一从原始指针创建的
正确的做法是从一个已有的shared_ptr创建另一个shared_ptr:
std::shared_ptr<MyClass> sp2 = sp1;
将new 的结果立即传递给智能指针构造函数:
// 好:
std::shared_ptr<MyClass> sp(new MyClass());
// 或者更好:
auto sp = std::make_shared<MyClass>();
// 不好(有风险):
// process_widget(std::shared_ptr<Widget>(new Widget()), compute_priority());
// 如果 compute_priority() 抛出异常,new Widget() 分配的内存可能泄漏。
// make_shared 可以避免这个问题。
函数参数传递:
如果函数需要取得对象的所有权,按值传递unqiue_ptr(使用std::move)或直接传递unique_ptr&&。
void take_ownership(std::unique_ptr<MyClass> ptr) { /* ... */ }
std::unique_ptr<MyClass> p = std::make_unique<MyClass>(1);
take_ownership(std::move(p));
如果函数需要共享对象的所有权,按值传递shared_ptr(会增加引用计数)。
void share_ownership(std::shared_ptr<MyClass> ptr) { /* ... */ }
如果函数只是使用/观察对象,并且不影响其生命周期,传递原始指针(T*)或引用(T&)。可以从智能指针通过.get()(获取原始指针)或*(解引用)获得。
void observe_object(const MyClass* obj) { if(obj) obj->print(); }
void observe_object_ref(const MyClass& obj) { obj.print(); }
std::unique_ptr<MyClass> u_ptr = std::make_unique<MyClass>(10);
observe_object(u_ptr.get());
observe_object_ref(*u_ptr);
std::shared_ptr<MyClass> s_ptr = std::make_shared<MyClass>(20);
observe_object(s_ptr.get());
observe_object_ref(*s_ptr);
注意:传递.get()获取的原始指针时,要确保原始指针的生命周期长于函数调用。
对于类成员,如果该成员是类独有的,使用unique_ptr。如果需要与其他对象共享,使用shared_ptr。如果可能形成循环引用,则考虑使用weak_ptr.
std::enable_shared_from_this:如果一个类的方法需要返回指向当前对象(this)的shared_ptr,那么这个类应该公有继承自std::enable_shared_from_this<YourClass>,然后使用其shared_from_this()方法。
class MyObject : public std::enable_shared_from_this<MyObject> {
public:
std::shared_ptr<MyObject> get_shared_this() {
return shared_from_this();
}
// 注意:不能在构造函数中调用 shared_from_this(),
// 因为此时 shared_ptr 的控制块还未完全建立。
};
结语
智能指针是现代C++编程中不可或缺的一部分。它们通过自动化内存管理,极大地减少了内存泄露和悬空指针等常见错误,使我们能够编写更安全、更健壮、更易于维护的代码。
掌握unique_ptr、shared_ptr 和 weak_ptr 的特性和使用场景,将使你的C++技能更上一层楼。开始在你的项目中拥抱它们吧!
希望这篇博客能帮助你更好地理解C++智能指针。如果你有任何问题或想法,欢迎在评论区分享!