C++智能指针
智能指针是一种用于管理动态分配内存的对象,可以在内存不再需要时自动释放。智能指针通过重载了指针操作符的类来实现,以模拟指针的行为,但具有自动资源管理的功能。
RAII思想
RAII
是资源获取即初始化(Resource Acquisition Is Initialization)的缩写,是一种 C++ 编程范式,它通过在对象的构造函数中获取资源,利用对象的生命周期来管理资源的释放,从而确保资源在不再需要时被正确释放。智能指针是RAII
思想的一种产物,在多线程中,守卫锁Guard Lock
也是一种常见的RAII
风格的加锁方式。
RAII
风格的Lock Guard(Linux下原生线程库)
#pragma once
#include <iostream>
#include <pthread.h>
//RAII枷鎖
class Mutex
{
public:
Mutex(pthread_mutex_t* mutex)
:_mutex(mutex)
{
pthread_mutex_init(_mutex,nullptr);
}
void lock()
{
pthread_mutex_lock(_mutex);
}
void unlock()
{
pthread_mutex_unlock(_mutex);
}
~Mutex()
{
pthread_mutex_destroy(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* pmtx)
:_mtx(pmtx)
{
_mtx.lock();
}
~LockGuard()
{
_mtx.unlock();
}
private:
Mutex _mtx;
};
智能指针
C++标准库提供了两种主要的智能指针:std::unique_ptr
和 std::shared_ptr
。此外,C++17还引入了 std::weak_ptr
。
std::auto_ptr
C++在C++98中就引入了auto_ptr
,但是但在 C++11 标准中已经被废弃,并在 C++17 中被完全移除。这是因为 auto_ptr
存在一些严重的缺陷。
问题如下:
int main()
{
std::auto_ptr<int> p1(new int);
std::auto_ptr<int> p2(p1);
*p1 = 10;
*p2 = 10;
std::cout << *p1 << std::endl;
std::cout << *p2 << std::endl;
return 0;
}
上面的代码以指针的角度来看,就是让两个指针维护同一块地址空间,但是上面程序运行会奔溃。这是标准库中的auto_ptr
简单实现一份auto_ptr
然后了解一下auto_ptr
的问题。
#pragma once
#include <iostream>
namespace ding
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T> & sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
//赋值运算符(注意先释放原空间在赋值 以及自己给自己赋值的情况)
auto_ptr<T>& operator=(const auto_ptr<T>& sp)
{
if (&sp != this)
{
if (_ptr != nullptr)
{
delete _ptr;
}
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
//模拟指针行为
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
~auto_ptr()
{
if (_ptr != nullptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
}
使用auto_ptr
#include "auto_ptr.h"
int main()
{
ding::auto_ptr<int> p1(new int);
ding::auto_ptr<int> p2(p1);
*p1 = 10;
*p2 = 10;
std::cout << *p1 << std::endl;
std::cout << *p2 << std::endl;
return 0;
}
当执行完ding::auto_ptr<int> p2(p1);
后,p1对象的指针已经被置空了,在对其解引用,就会出现对空指针解用的问题。空指针是不能解引用的。这种情况编译器应该要出警告的,但是我的编译器还是能运行的,只是退出码不正常。这应该是vs2022的bug(我用的是2022测试版的)。
只能通过监视窗口来看!
C++98提供的auto_ptr
最大的问题就是这个了,称之为管理权转移,将p1的管理权转移给p2然后自己悬空。导致了auto_ptr
直接被禁用,甚至在17中被移除了。
std::unique_ptr
经过十几年后,在C++11中,出现了比auto_ptr
更靠谱的unique_ptr
。unique_ptr
主要解决auto_ptr
带来的问题,在unique_ptr
中直接禁用了拷贝和赋值。
std::unique_ptr
的使用
int main()
{
std::unique_ptr<int> p1(new int);
std::unique_ptr<int> p2(p1);//error
p2 = p1;//error
}
unique_ptr
不能再使用拷贝和赋值了,所以他的使用场景就被限制了,对于有些地址空间需要更多的指针来维护是不能实现的。
std::unique_ptr简易模拟实现
unique_ptr
就很简单了,对比auto_ptr
直接把拷贝构造和赋值用C++11的语法用delete禁用就行了。
namespace ding
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr<T>& sp) = delete;
//赋值运算符
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
//模拟指针行为
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
~unique_ptr()
{
if (_ptr != nullptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
}
std::shared_ptr
C++11还提供了更靠谱的智能指针shared_ptr
。解决了auto_ptr
的悬空问题和unique_ptr
的防拷贝问题。
std::shared_ptr
使用引用计数来跟踪有多少个智能指针指向相同的资源,当最后一个 std::shared_ptr
被销毁时,它所管理的资源也会被释放。
shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr 简易模拟实现
引用计数是shared_ptr
对象的公共资源,也就是说在实现shared_prt
时,不能简单的将引用计数_ref_count
设计为一个普通的成员变量,设计成为普通的成员变量,就意味着每个shared_ptr
对象都有一个引用计数。普通的成员变量导致无法正确记录内存块的使用情况。
图解:
使用static也不能解决,因为static是所有类对象共享的。这就会导致只要使用shared_ptr
不论管理的是哪一块地址空间,用的都是同一个引用计数。
图解:
将引用计数定义成为一个指针,当一块地址空间第一次被shared_ptr对象维护时,在堆区开辟一块空间用于存储其对应得引用计数,如果其他shared_ptr对象也要维护这块地址空间,除了将地址给他还要把引用计数的地址也给他。
图解:
namespace ding
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_ref_count(new size_t(1))
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_ref_count(sp._ref_count)
{
++(*_ref_count);
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (this != &sp)
{
if (--(*_ref_count) == 0)
{
delete _ref_count;
delete _ptr;
}
_ptr = sp._ptr;
_ref_count = sp._ref_count;
++(*_ref_count);
}
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->()const
{
return _ptr;
}
~shared_ptr()
{
if (--(*_ref_count) == 0 && _ptr != nullptr)
{
delete _ptr;
delete _ref_count;
}
}
private:
T* _ptr;
size_t* _ref_count;//引用计数
};
}
通过监视窗口可以看出当前代码得sp1,sp2,sp3都指向得是同一块地址空间。引用计数都是3。没有问题。
执行完sp3 = sp4 后,sp1和sp2得引用计数应该变为2,sp3和sp4应该指向同一块地址空间,引用计数也是同一个。
监视窗口观看也没有问题。
如果是普通得成员变量得话,上面同样得代码,监视窗口结果如下:
引用计数得结果完全不符合要求。
shared_ptr线程安全问题
std::sharer_ptr
本身是线程安全得,当多个线程同时访问同一个 std::shared_ptr
对象时,std::shared_ptr
本身能够确保引用计数的操作是原子的,从而保证了线程安全性。
比如下面得场景:
创建两个线程,让他们疯狂的拷贝当前智能指针对象,引用计数就会一直增加。
线程执行完后,引用计数应该还是1。因为copy对象是一个局部对象,出了作用域就释放了。
void fun(std::shared_ptr<int