C++11 引入了 智能指针来解决手动管理动态内存的复杂性。它们能够自动管理堆内存,并在不再需要时自动释放,避免内存泄漏和悬空指针问题。C++11 提供了三种主要的智能指针类型:std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
。
1. std::unique_ptr
:独占所有权
std::unique_ptr
是一种 独占所有权 的智能指针,它保证同一时间只能有一个智能指针拥有对象的所有权。这意味着对象只能被一个 unique_ptr
所管理,不能被多个指针共享。使用 std::unique_ptr
的场景通常是当一个对象的生命周期只需要在单个作用域内管理时。
特点:
- 独占所有权:不能复制,只能移动。
- 生命周期:当
unique_ptr
离开其作用域时,会自动销毁并释放所管理的对象。 - 轻量、高效:由于不涉及引用计数,
unique_ptr
的效率通常比shared_ptr
更高。
示例:
#include <memory>
#include <iostream>struct MyClass {MyClass() { std::cout << "MyClass constructed\n"; }~MyClass() { std::cout << "MyClass destroyed\n"; }
};int main() {std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>(); // 自动管理动态分配的 MyClass// std::unique_ptr<MyClass> ptr2 = ptr1; // 错误,不能复制 unique_ptrstd::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 正确,通过移动所有权// ptr1 现在为空
}
重要操作:
- 创建智能指针:使用
std::make_unique<T>()
动态分配内存并返回std::unique_ptr<T>
,避免手动使用new
。 - 移动所有权:使用
std::move
进行所有权转移,不能复制。
2. std::shared_ptr
:共享所有权
std::shared_ptr
实现了 共享所有权,即可以有多个智能指针共同拥有同一个对象。对象的生命周期会被多个 shared_ptr
共享,只有当最后一个 shared_ptr
被销毁时,托管的对象才会被释放。
特点:
- 共享所有权:多个
shared_ptr
可以指向同一个对象。 - 引用计数:
shared_ptr
维护一个引用计数器,当指向对象的所有shared_ptr
被销毁时,引用计数器变为 0,对象会自动释放。 - 适用场景:多个对象需要共享同一个资源时,例如在复杂的资源共享或对象关系管理中使用。
示例:
#include <memory>
#include <iostream>struct MyClass {MyClass() { std::cout << "MyClass constructed\n"; }~MyClass() { std::cout << "MyClass destroyed\n"; }
};int main() {std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();{std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数增加std::cout << "Reference count: " << ptr1.use_count() << "\n"; // 输出 2}// 离开作用域,ptr2 被销毁,引用计数减少为 1std::cout << "Reference count: " << ptr1.use_count() << "\n"; // 输出 1
}
重要操作:
- 创建智能指针:使用
std::make_shared<T>()
创建共享指针,比手动分配内存效率更高。 - 引用计数:使用
use_count()
查看当前引用计数。
注意事项:
- 循环引用问题:如果两个或多个
shared_ptr
对象形成循环引用(即 A 指向 B,B 又指向 A),那么即使引用计数变为 0,内存也不会被释放,导致内存泄漏。为了解决这个问题,引入了std::weak_ptr
。
3. std::weak_ptr
:弱引用
std::weak_ptr
是为了防止 std::shared_ptr
之间的 循环引用问题 而设计的。weak_ptr
并不影响引用计数,不拥有对象的所有权。它只是一个观察者,可以访问由 shared_ptr
管理的对象。如果 shared_ptr
管理的对象已经被释放,那么 weak_ptr
变为无效。
特点:
- 不增加引用计数:
weak_ptr
只是持有对象的引用,不会影响shared_ptr
的引用计数。 - 生命周期观察:使用
weak_ptr
观察对象的生命周期,可以通过lock()
函数将weak_ptr
转换为shared_ptr
,安全地访问对象。
示例:
#include <memory>
#include <iostream>struct MyClass {MyClass() { std::cout << "MyClass constructed\n"; }~MyClass() { std::cout << "MyClass destroyed\n"; }
};int main() {std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();std::weak_ptr<MyClass> wptr = sptr; // weak_ptr 引用 sptr,但不增加引用计数std::cout << "Reference count: " << sptr.use_count() << "\n"; // 输出 1if (auto shared = wptr.lock()) {std::cout << "Object is still alive\n";} else {std::cout << "Object has been destroyed\n";}sptr.reset(); // 释放 shared_ptr 管理的对象if (auto shared = wptr.lock()) {std::cout << "Object is still alive\n";} else {std::cout << "Object has been destroyed\n";}
}
shared_ptr
内部维护的关键成员变量:
1. 指向对象的原始指针(Managed Object Pointer)
- 这是指向实际动态分配对象的原始指针。它是
shared_ptr
管理的对象,shared_ptr
使用这个指针来访问对象。 - 例如:
T* ptr
,它指向动态分配的对象(例如通过make_shared
或new
分配的对象)。
2. 控制块(Control Block)
- 控制块 是
shared_ptr
背后维护的一个数据结构,包含了引用计数、弱引用计数以及其他额外的信息。控制块并不与所管理的对象共享地址,它是shared_ptr
机制中的核心,包含多个关键数据:
-
- 引用计数(Reference Counter):
-
-
- 一个用于记录有多少个
shared_ptr
实例同时引用该对象的计数器,称为共享引用计数。当创建一个新的shared_ptr
实例指向相同对象时,该计数器递增;当shared_ptr
被销毁或重置时,该计数器递减。 - 当引用计数降为 0 时,托管的对象会被销毁。
- 一个用于记录有多少个
-
-
- 弱引用计数(Weak Reference Counter):
-
-
- 另一个计数器,记录有多少个
weak_ptr
实例引用该对象。weak_ptr
不会影响共享引用计数,但会影响控制块的生命周期。 - 当共享引用计数和弱引用计数都降为 0 时,控制块本身也会被销毁。
- 另一个计数器,记录有多少个
-
-
- 自定义删除器(Custom Deleter):
-
-
shared_ptr
可以绑定一个自定义删除器,用于控制对象的销毁方式。如果你不指定删除器,shared_ptr
会使用默认的delete
运算符释放对象。- 删除器保存在控制块中,当最后一个
shared_ptr
离开作用域时,它会调用这个删除器来销毁对象。
-
3. 弱引用管理(Weak Pointer Management)
shared_ptr
与weak_ptr
的结合使用涉及到对控制块中弱引用计数的维护。weak_ptr
不会增加对对象的共享引用计数,但会使控制块的弱引用计数增加。这样,即使没有任何shared_ptr
实例指向对象,控制块依然存在,直到所有weak_ptr
也被销毁。
具体控制块结构的解释
可以将控制块理解为包含以下信息的结构体(这是一个概念上的简化):
struct ControlBlock {int shared_count; // 共享引用计数int weak_count; // 弱引用计数void(*deleter)(T*); // 自定义删除器函数指针
};
每次创建 shared_ptr
时,shared_count
会增加,而 weak_count
仅在创建 weak_ptr
时增加。
控制块与对象的关系
- 独立于对象的存储:控制块是一个独立的数据结构,和对象的内存分配是分开的。当使用
make_shared
时,对象和控制块可能会被分配到同一块内存区域中,但它们的作用是不同的。 - 生命周期管理:当
shared_count
变为 0 时,shared_ptr
管理的对象会被销毁;当weak_count
也为 0 时,控制块才会被销毁。
shared_ptr
循环引用的例子:
#include <iostream>
#include <memory>struct B;struct A {std::shared_ptr<B> ptrB; // A持有B的shared_ptr~A() { std::cout << "A destroyed\n"; }
};struct B {std::shared_ptr<A> ptrA; // B持有A的shared_ptr~B() { std::cout << "B destroyed\n"; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->ptrB = b; // A拥有Bb->ptrA = a; // B拥有A// 循环引用导致A和B永远不会被销毁return 0;
}
在这个例子中:
A
持有一个指向B
的shared_ptr
,而B
也持有一个指向A
的shared_ptr
。- 由于
shared_ptr
会增加引用计数,a
和b
的引用计数永远无法减为 0,导致它们的析构函数不会被调用,从而造成内存泄漏。
解决循环引用:使用 std::weak_ptr
要解决循环引用问题,可以将其中一个 shared_ptr
替换为 std::weak_ptr
。weak_ptr
不会影响引用计数,因此可以打破循环引用。
解决后的代码:
#include <iostream>
#include <memory>struct B;struct A {std::shared_ptr<B> ptrB; // A持有B的shared_ptr~A() { std::cout << "A destroyed\n"; }
};struct B {std::weak_ptr<A> ptrA; // B持有A的weak_ptr,打破循环引用~B() { std::cout << "B destroyed\n"; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->ptrB = b; // A拥有Bb->ptrA = a; // B弱引用A,不增加引用计数// 正常销毁A和B,输出"A destroyed"和"B destroyed"return 0;
}
关键点:
- 将
B
对A
的引用改为std::weak_ptr
。weak_ptr
不增加引用计数,不会阻止对象的销毁。 - 在需要使用
weak_ptr
指向的对象时,可以调用lock()
方法将其转换为shared_ptr
,如果对象已被销毁,则返回nullptr
。