目录
1. 为什么需要智能指针
1.1 C++ 需要智能指针的原因
-
手动内存管理:
-
C++ 要求开发者手动管理内存分配和释放,使用
new
和delete
操作符。 -
手动管理容易导致内存泄漏、悬空指针等问题。
-
-
作用域和生命周期:
-
C++ 中对象的生命周期由作用域决定,离开作用域后对象会被销毁。
-
对于动态分配的对象,需要确保在适当的时候释放内存。
-
1.2 Java 不需要智能指针的原因
-
自动垃圾回收:
-
Java 使用垃圾回收机制(GC)自动管理内存,开发者无需手动释放内存。
-
GC 会定期清理不再使用的对象,防止内存泄漏。
-
-
对象生命周期:
-
Java 中对象的生命周期由垃圾回收器管理,开发者只需创建对象,GC 会在对象不再被引用时回收内存。
-
-
引用类型:
-
Java 提供了强引用、软引用、弱引用和虚引用,帮助开发者更灵活地管理对象生命周期,而不需要智能指针。
-
2. 智能指针的使用及原理
C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针
2.1 RAII
RAII是一种利用对象生命周期来控制程序资源的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两个好处:
1.不需要显式的释放资源
2.采用这种方式,对象所需的资源在其生命周期内始终保持有效
智能指针的原理:
1.具有RAII的特性
2.重载operator* 和 operator->,具有像指针一样的行为。
其中重载这两个操作符是很重要的:
1.operator*
的作用
operator*
是解引用运算符,用于获取智能指针所管理的对象的引用。它的实现通常如下:
T& operator*() const
{
return *_ptr;
}
解释:
-
_ptr
是智能指针内部管理的裸指针。 -
*_ptr
解引用裸指针,返回它所指向的对象的引用。 -
返回类型是
T&
,即对象的引用,允许用户直接修改所管理的对象。
auto_ptr<int> ptr(new int(42));
int value = *ptr; // 解引用,获取 ptr 管理的 int 值
*ptr = 100; // 修改 ptr 管理的 int 值
2. operator->
的作用
operator->
是成员访问运算符,用于访问智能指针所管理的对象的成员。它的实现通常如下:
T* operator->() const
{
return _ptr;
}
解释:
-
operator->
返回裸指针_ptr
,使得用户可以通过智能指针直接访问所管理对象的成员。 -
返回类型是
T*
,即裸指针。
struct MyStruct
{
int value;
void print() { std::cout << value << std::endl; }
};
auto_ptr<MyStruct> ptr(new MyStruct{42});
ptr->value = 100; // 访问 MyStruct 的成员 value
ptr->print(); // 调用 MyStruct 的成员函数 print
operator->
的返回值必须是裸指针(T*
),因为 C++ 语言规定 ->
运算符必须作用在指针上。
-
ptr->print()
被解析为ptr.operator->()->print()
。 -
ptr.operator->()
返回_ptr
(一个裸指针)。 -
通过
_ptr
调用print()
。
2.1 auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示了auto_ptr的使用及其问题。
auto_ptr的实现原理:管理权转移的思想。
template <typename T>
class auto_ptr {
public:
// 构造函数,接受一个裸指针并接管其所有权
explicit auto_ptr(T* ptr = nullptr) : ptr_(ptr) {}
// 析构函数,释放所管理的内存
~auto_ptr() {
delete ptr_;
}
// 拷贝构造函数,转移所有权
auto_ptr(auto_ptr& other) : ptr_(other.release()) {}
// 拷贝赋值运算符,转移所有权
auto_ptr& operator=(auto_ptr& other) {
if (this != &other) {
delete ptr_;
ptr_ = other.release();
}
return *this;
}
// 解引用运算符,获取所管理对象的引用
T& operator*() const {
return *ptr_;
}
// 箭头运算符,访问所管理对象的成员
T* operator->() const {
return ptr_;
}
// 获取所管理的裸指针
T* get() const {
return ptr_;
}
// 释放所有权,返回裸指针并将内部指针置为空
T* release() {
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
// 重置所管理的指针,释放原有内存
void reset(T* ptr = nullptr) {
if (ptr_ != ptr) {
delete ptr_;
ptr_ = ptr;
}
}
private:
T* ptr_;
};
int main()
{
auto_ptr<int> p1(new int(10));
auto_ptr<int> p2(p1);//管理权转移
// cout << *p1 << endl;
cout << *p2 << endl;
return 0;
}
auto_ptr
的拷贝构造函数和拷贝赋值运算符会转移所有权。这意味着当一个 auto_ptr
对象被拷贝或赋值给另一个 auto_ptr
对象时,原对象会失去对所管理内存的所有权,新对象会接管所有权。
2.2 unique_ptr
C++11开始提供更合理的unique_ptr。
既然拷贝构造有问题,那干脆别拷贝了,这也是unique_ptr的实现原理,简单粗暴。
namespace zzy
{
template <class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if(_ptr)
{
cout << "~unique_ptr()" << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
private:
T* _ptr;
};
}
禁用拷贝时顺便把赋值也禁了。
2.3 shared_ptr
C++11开始提供更靠谱且支持拷贝的shared_ptr,很多时候让手撕的都是这个。
原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在内部给每个资源都维护了一份计数,同来记录该份资源被几个对象共享
- 在对象被销毁时(析构时),说明该对象不使用该资源了,对象的引用计数减1
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放资源
- 如果不是0,说明除了自己还有其他对象在使用资源,不能释放该资源,否则其他对象就成野指针了
namespace zzy
{
template <class T>
class shared_ptr<T>
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
,_count(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
,_count(sp._count)
{
++(*_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if(_ptr == sp._ptr)
return *this;
if(--(*_count)==0)
{
delete _ptr;
delete _count;
}
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
return *this;
}
~shared_ptr()
{
if(--(*_count)==0)
{
cout << "delete:" << _ptr <<endl;
delete _ptr;
delete _count;
}
}
int use_count()
{
return *_count;
}
T* operator->() const
{
return _ptr;
}
T& operator*() const
{
return *_ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
}
我们分析几个要点:
1. private中定义了两个变量,一个指向资源,一个指向计数,为什么要使用指针指向计数呢?
我们期望一个资源有一个count,它可以有多个ptr去管理它。如果每个指针都有自己的一个,那肯定是不行的,此时,如果我们使用静态变量,当存在多份资源需要管理时,每个资源本应有一个,现在不管多少资源都只有一个count,这肯定也是不行的。所以还是使用指针指向某空间,多个指针管理同一块空间时,只要他们的_count一样就好了。
2.构造函数中,为什么使用new int(1);
有一个资源要交给指针管理时,肯定调用构造,_ptr不用多说,而使用new是因为这个引用计数必须是一个动态分配的对象,而不是栈上的局部变量。如果引用计数是栈上的局部变量,那么当 shared_ptr
对象被复制或销毁时,引用计数会被错误地修改或销毁。
3.在赋值重载中,比如 sp1 = sp5
if(--(*_count)==0);这时--的是sp1的 *_count ,因为sp1不再管理它原来的空间,原来空间的引用计数一定会--,如果减到0则释放。而后 _ptr = sp._ptr; _count = sp._count; ++(*_count); 此时++的是将sp5的_count赋给sp1的_count后的值,也就是sp1管理了sp5的空间,所以要++。
2.4 weak_ptr
weak_ptr没有RAII特性,它不管理资源,它是对shared_ptr
的补充,用于解决 shared_ptr
可能导致的循环引用问题(循环引用问题我们下面马上会讲到),它的主要作用是观察资源,而不会影响资源的生命周期。
template <class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
//支持用shared_ptr来构造
explicit weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
//赋值也一样
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T* operator->() const
{
return _ptr;
}
T& operator*() const
{
return *_ptr;
}
private:
T* _ptr;
};
2.5 shared_ptr的循环引用
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; //1
cout << node2.use_count() << endl; //1
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl; //2
cout << node2.use_count() << endl; //2
return 0;
}
刚开始都是1个好理解,node1->_next = node2 后,指向node2的多了一个_next,所以_count变成2;node2->_prev = node1 后,指向node1的多了一个_prev,所以_count变成2。
当程序结束时,node1和node2析构了,他们各自的引用计数--变成了1,因为node1上还有一个_prev,node2上还有一个_next。也就是说只有这个_next 和 _prev 析构了,node1和node2的引用计数才会变0,node1和node2才会析构,但是_next是属于node的成员,node1释放了,_next才会析构,而node1又由_prev管理,这个_prev属于node2,直接循环引用,谁都不释放。
node2的_prev什么时候析构?-------
node2析构时它就析构 |
| |
| |
node2什么时候析构? |
node1的_next析构,node2就析构 |
| |
| |
node1的_next什么时候析构? |
node1析构他就析构 |
| |
| |
node1什么时候析构? |
node2的_prev析构,node1就析构----
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
weak_ptr 下的next和prev不会增加node的引用计数。
2.6 删除器
我们shared_ptr的底层是写死的,用的是delete,如果是new[] 出来的数据怎么办呢?用delete会崩溃,库里引入了定制删除器。
-
不同资源可能需要不同的释放方式(例如
delete
、delete[]
、free()
、文件句柄关闭等)。 -
直接硬编码
delete
会导致无法处理new[]
、malloc
或其他自定义分配的资源。
在自定义 shared_ptr
中:
template <class T>
class shared_ptr
{
public:
...
// 允许用户传递自定义删除器
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
...
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; }; // 存储删除器
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
zzy::shared_ptr<int> sp1(new int[10], DeleteArray<int>());
zzy::shared_ptr<int> sp2((int*)malloc(sizeof(int)), [](int* ptr) {free(ptr); });
zzy::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
-
默认删除器:适用于普通的
new
分配的资源。 -
自定义删除器:适用于
new[]
、文件句柄、自定义资源等。 -
灵活性:
shared_ptr
的删除器机制使得它可以管理各种类型的资源,同时确保资源能够被正确释放。
3.C++11 和 boost中智能指针的关系
1.C++98中产生了第一个智能指针auto_ptr
2.Boost 提供了以下智能指针:
-
boost::shared_ptr
:基于引用计数的智能指针,允许多个指针共享同一个对象。 -
boost::weak_ptr
:与boost::shared_ptr
配合使用,解决循环引用问题。 -
boost::scoped_ptr
:独占所有权的智能指针,不能复制或移动。
Boost 的智能指针为 C++ 程序员提供了强大的工具,但需要额外安装和依赖 Boost 库。
3.C++11引入了unique_ptr 和 shared_ptr 和 weak_ptr,unique_ptr 对应 scoped_ptr。
并且是参考实现的。