目录
一、智能指针的概念和意义
对于下列代码,如果int* arr1 = new int[10]抛异常,arr1和arr2没有成功分配到内存,异常处理时无需释放arr1和arr2的内存;如果int* arr2 = new int[10]抛异常,异常处理时要释放arr1的内存;如果Div(a,b)抛异常,异常处理时要释放arr1和arr2的内存。如果有许多这样的情况,那么异常捕获就需要多层嵌套try-catch语句,代码冗余。如果不给他们进行异常捕获处理操作,就会导致内存泄漏。
为了解决上述情况,C++引入了智能指针。智能指针是一个用于自动管理动态分配的内存的类模板,能够保证对象不需要时自动释放内存,从而避免内存泄露。
cpp
double Div(double x, double y)
{
if (y == 0)
throw "Dividing by 0";//抛出异常
else
return x / y;
}
void Func()
{
int* arr1 = new int[10];
int* arr2 = new int[10];
try {
int a, b;
std::cin >> a >> b;
std::cout << Div(a, b) << std::endl;
}
catch (...)
{
std::cout << "delete []" << arr1 << std::endl;
delete[] arr1;
std::cout << "delete []" << arr2 << std::endl;
delete[] arr2;
throw;//重新抛出异常
}
std::cout << "delete []" << arr1 << std::endl;
delete[] arr1;
std::cout << "delete []" << arr2 << std::endl;
delete[] arr2;
}
int main()
{
try {
Func();
}
catch (const char* error_msg)
{
std::cout << error_msg << std::endl;
}
catch (int error_msg)
{
std::cout << error_msg << std::endl;
}
catch (...)
{
std::cout << "unknown exception" << std::endl;
}
return 0;
}
二、简单模拟智能指针
智能指针是利用RAII技术实现的。RAII技术是一种利用对象生命周期控制对象资源的技术,C++线程库中的lock_guard就是利用RAII技术实现的。RAII本质就是将资源封装起来,构造成一个对象,利用构造函数和析构函数来分配内存和释放内存,当对象行名周期结束时,对象也会自动释放。
cpp
template<class T>
class Smart_Ptr {
private:
T* _ptr;
public:
//构造函数
Smart_Ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
//析构函数
~Smart_Ptr()
{
if (_ptr)
delete _ptr;
}
//*
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
};
有了智能指针后,当Div(a,b)抛异常进行异常处理时,就不需要手动释放arr1和arr2的内存了
cpp
double Div(double x, double y)
{
if (y == 0)
throw "Dividing by 0";//抛出异常
else
return x / y;
}
void Func()
{
Smart_Ptr<int> sp1(new int);
Smart_Ptr<int> sp2(new int);
int a, b;
std::cin >> a >> b;
std::cout << Div(a, b) << std::endl;
}
int main()
{
try {
Func();
}
catch (const char* error_msg)
{
std::cout << error_msg << std::endl;
}
catch (int error_msg)
{
std::cout << error_msg << std::endl;
}
catch (...)
{
std::cout << "unknown exception" << std::endl;
}
return 0;
}
代码示例:
cpp
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
Smart_Ptr<Date> sp1(new Date);
//本来应该是sp1.operator->()->_month=1或者sp1->->_month
//这里语法上为了可读性,省略了一个->
sp1.operator->()->_year = 2025;
sp1->_month = 1;
sp1->_day = 26;
return 0;
}
三、智能指针
智能指针有很多种,C++98中的智能指针是auto_ptr,C++11中的智能指针是unique_ptr和shared_ptr。
1.auto_ptr
auto_ptr是C++98中的智能指针,但是该智能指针的的拷贝构造函数是管理权转移。如下列代码所示,管理权转移会将sp1指针的内容拷贝给sp2,再将sp1指针置空,即悬空指针。如果再次解引用sp1程序就会报错。之所以要使用悬空指针,是为了防止析构函数多次释放同一份资源。
因此auto_ptr是一个失败的设计,许多公司明确要求禁止使用auto_ptr。
cpp
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
cpp
int main()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
2.unique_ptr
C++11中提供了更靠谱的unique_ptr智能指针,unique_ptr禁用拷贝构造,这样就能避免悬空指针问题。
unique_ptr文档:unique_ptr - C++ Reference
3.shared_ptr
shared_ptr也是C++11提供的智能指针,shared_ptr通过引用计数的方法支持拷贝构造。原理是给内存资源计数,计数=指针指向数,智能指针销毁调用析构时引用计数减一,当引用计数值减为0时才真正销毁释放内存资源。
shared_ptr文档:shared_ptr - C++ Reference
四、模拟实现shared_ptr
- 引用计数使用指针类型,因为当一个智能指针拷贝给另一个智能指针时,都是值拷贝,两个对象的引用计数指针指向的是同一个计数资源。如果使用值类型,当两个智能指针指向同一份资源时,引用计数是属于各自对象的,一个智能指针销毁时,引用计数--,而另一个智能指针的引用计数不会变化。如果使用静态的值类型,那么所有同类型的智能指针都使用同一个引用计数,更加不可能。
- 实现赋值运算符重载时,需要先显式实现析构函数。因为在调用赋值重载时,例如sp1=sp2,首先要"释放"sp1的资源,但是不能直接delete,因为可能还有其他智能指针指向sp1的资源,又因为析构函数不能显示调用,因此必须实现一个和析构函数功能相同的函数release,用于帮助"释放"sp1的资源。同时在实现赋值重载时,必须防止自己赋值给自己,所以实现赋值重载时要先通过指向的资源是否相同来判断是否是同一个对象。(sp1和sp2指向同一份资源,sp1=sp2也算作是自己赋值给自己)。
- 智能指针线程安全问题:多线程对智能指针进行拷贝时,拷贝构造中的引用计数++,智能指针销毁时调用析构函数引用计数--,引用计数的++和--操作不是线程安全的,因此多线程中智能指针的拷贝和析构会存在问题。有两种解决方式:一是为引用计数的++和--操作加锁保护,二是将引用计数类型改为原子类型。此处采用第二种解决方式,更简单。
cpp
namespace bit {
//shared_ptr
template<class T>
class shared_ptr {
private:
T* _ptr;
std::atomic_int* _pcount;//引用计数
//int* _pcount;//引用计数
public:
//构造函数
shared_ptr(T* ptr)
:_ptr(ptr),
_pcount(new std::atomic_int(1))
{}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount)
{
(*_pcount)++;
}
//析构函数
~shared_ptr()
{
--*(_pcount);
if (*(_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//引用计数值
std::atomic_int use_count() const
{
return *_pcount;
}
//显式实现析构函数
void release()
{
--(*_pcount);
if (*(_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
this->release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
//*
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
};
}
class A {
private:
int _a1;
int _a2;
public:
//构造函数
A(int a1, int a2)
:_a1(a1),
_a2(a2)
{}
//析构函数
~A()
{
std::cout << "~A()" << std::endl;
}
};
int main()
{
bit::shared_ptr<A> sp1(new A(1,1));
bit::shared_ptr<A> sp2(new A(2, 2));
bit::shared_ptr<A> sp3(sp1);
std::cout << sp1.use_count() << std::endl;
std::cout << sp2.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
bit::shared_ptr<A> sp4(new A(4,4));
sp3 = sp4;
std::cout << "-----------------" << std::endl;
std::cout << sp1.use_count() << std::endl;
std::cout << sp2.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
std::cout << sp4.use_count() << std::endl;
//std::cout << "##############################" << std::endl;
//std::shared_ptr<A> sp11(new A(1, 1));
//std::shared_ptr<A> sp22(new A(2, 2));
//std::shared_ptr<A> sp33(sp11);
//std::cout << sp11.use_count() << std::endl;
//std::cout << sp22.use_count() << std::endl;
//std::cout << sp33.use_count() << std::endl;
//std::shared_ptr<A> sp44(new A(4, 4));
//sp33 = sp44;
//std::cout << "-----------------" << std::endl;
//std::cout << sp11.use_count() << std::endl;
//std::cout << sp22.use_count() << std::endl;
//std::cout << sp33.use_count() << std::endl;
//std::cout << sp44.use_count() << std::endl;
return 0;
}
五、智能指针shared_ptr的循环引用问题
常规情况下,如果没有node1->_next = node2、node2->_prev = node1代码,程序结束时调用shared_ptr的析构函数,释放Node节点。
但是如果有了node1->_next = node2、node2->_prev = node1代码,就会出现循环引用问题。因为node1->_next = node2、node2->_prev = node1,node1指针指向资源的引用计数是2,node2指针指向资源的引用计数也是2。根据析构函数顺序,智能指针node2先调用析构函数,--引用计数,由于引用计数还不等于0,所以node2智能指针不会销毁;智能指针node1再调用析构函数,--引用计数,由于引用计数还不等于0,所以node1智能指针也不会销毁。node2智能指针不销毁,其中的_prve智能指针也不会销毁,导致node1的引用计数永远都是1;同理对于node2也是一样,因此它们不会销毁,这就是循环引用导致的问题。
cpp
struct Node {
std::shared_ptr<Node> _prev = nullptr;
std::shared_ptr<Node> _next = nullptr;
int _val;
//析构函数
~Node()
{
std::cout << "~Node()" << std::endl;
}
};
int main()
{
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
如何解决循环引用问题?
C++引入了智能指针weak_ptr,用于辅助shared_ptr解决循环引用问题。weak_ptr与shared_ptr共享同一份资源,但是weak_ptr不会修改引用计数,对资源没有所有权,仅仅作为观察者存在。当出现循环引用问题时,将Node节点中的智能指针类型改为weak_ptr即可。
cpp
struct Node {
std::weak_ptr<Node> _prev;
std::weak_ptr<Node> _next;
//std::shared_ptr<Node> _prev;
//std::shared_ptr<Node> _next;
int _val;
//析构函数
~Node()
{
std::cout << "~Node()" << std::endl;
}
};
int main()
{
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
六、定制删除器
智能指针unique_ptr和shared_ptr都有定制删除器,本质是智能指针析构函数中清理对象资源的具体方法,默认的定制删除器是delete,只能清理new出来的资源,但是并非所有的智能指针所指向的资源都是new出来的,因此就需要定制删除器来删除非new分配的资源。
cpp
template <class U, class D> shared_ptr (U* p, D del);
对于下述代码,智能指针sp2和sp3分别是malloc和fopen分配的资源,不能使用delete清理,因此必须给他们提供对应的定制删除器清理相应的资源。malloc分配的资源要用free清理,fopen分配的资源要用fclose清理。
cpp
std::shared_ptr<int> sp1(new int);
std::shared_ptr<int> sp2((int*)malloc(4));
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"));
定制删除器需要传递的是一个可调用函数对象,可以是函数指针、lambda表达式、函数对象
cpp
//仿函数定制删除器
template<class T>
struct FreeFunc {
void operator()(T* p)
{
free(p);
}
};
void test(FILE* p)
{
fclose(p);
}
int main()
{
std::shared_ptr<int> sp1(new int);//定制删除器------默认delete删除器
std::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());//定制删除器------函数对象
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* p) {fclose(p); });//定制删除器------lambda表达式
std::shared_ptr<FILE> sp4(fopen("test2.txt", "w"), test);//定制删除器------函数指针
return 0;
}
七、模拟实现shared_ptr(带上定制删除器)
- 要新增构造函数的模板重载,因为不传递定制删除器默认使用delete删除,传递定制删除器则使用传递的定制删除器。
- 要新增包装器包装的定制删除器成员变量,因为传递的定制删除器只能在函数模板内部使用,而不能在析构函数中调用它。同时还要使用包装器将定制删除器重新包装为可调用对象,因为定制删除器类型D也是只有在当前构造函数模板中存在,不能在其他地方使用。使用包装器包装成可调用对象后即可在析构函数中调用。
- 包装器包装的定制删除器成员变量提供缺省值,这一趟不传递定制删除器的智能指针就直接调用缺省值------delete删除
cpp
namespace bit {
//shared_ptr
template<class T>
class shared_ptr {
private:
T* _ptr;
std::atomic_int* _pcount;//引用计数
//int* _pcount;//引用计数
std::function<void(T*)> _del = [](T* p) {delete p; };//包装器包装的定制删除器
public:
//构造函数
shared_ptr(T* ptr)
:_ptr(ptr),
_pcount(new std::atomic_int(1))
{}
//构造函数模板重载
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr),
_pcount(new std::atomic_int(1)),
_del(del)
{}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount)
{
(*_pcount)++;
}
//析构函数
~shared_ptr()
{
--*(_pcount);
if (*(_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
//引用计数值
std::atomic_int use_count() const
{
return *_pcount;
}
//显式实现析构函数
void release()
{
--(*_pcount);
if (*(_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
this->release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
//*
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
};
}
class A {
private:
int a;
public:
~A()
{
std::cout << "~A()" << std::endl;
}
};
int main()
{
bit::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
return 0;
}