目录
一.智能指针的背景概念与发展历史
1.为何需要有智能指针
2.RAII思想
3.智能指针实现框架
4.智能指针的发展历史
二.智能指针的拷贝/赋值问题
三.定制删除器(仿函数)
1.new/new[]与delete/delete[]匹配问题
2.定制删除器的简单实现
0.前言
1.C++98: auto_ptr
2.C++11: unique_ptr
五.循环引用问题,weak_ptr登场
1.循环引用问题
2.如何解决(使用weak_ptr)
回顾异常安全问题: 如果申请了一个需要手动释放的资源, 而在手动释放代码执行之前, 有异常被抛出, 很可能因为抛出异常而导致进程执行流的跳跃, 导致资源没有被释放, 引发内存泄漏问题
例如以下伪代码
还没有释放pa, 就因为抛出异常跳跃执行流, 内存泄漏, 更严重的情况new本身抛出异常, 如果外面有对该代码段捕获异常的行为, 再加上多次new堆区空间, 也是很麻烦的内存泄漏问题
int* pa = new int;
throw ...;
delete pa;
以下为实际情况中的代码
#include
#include
#include
using namespace std;
//以下程序有很多种情况会发生内存泄漏问题
//1.两次new成功了, 而在delete之前抛出异常, 执行流直接跳到catch, pa与pb内存泄漏
//2.第二次new失败, new本身抛出异常, 被外部捕获, pa内存泄漏
void func()
{
int* pa = new int;
int* pb = new int;
int x = rand() % 5 + 1;
if (x == 1 || x == 5)
{
throw x;
}
delete pa;
delete pb;
}
int main()
{
srand((unsigned int)time(nullptr));
try
{
func();
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
基于以上问题, 如果new出的空间不再需要我们手动释放, 而是出函数栈帧就自动释放, 就不再会发生内存泄漏问题了
那么如何才能"自动化"管理堆区资源呢
智能指针很好的解决了"自动化管理资源"这一问题, 智能指针主要采用的RAII思想
RAII - Resource Acquistion Is Initialization
利用对象生命周期控制进程资源
1.在对象构造时获取资源
2.在对象析构时释放资源
本质上是一种将资源托管给一个对象来管理的行为
由于将资源托管给了对象, 自然地, 在对象创建时就要调用构造函数, 在对象销毁时就要调用析构函数, 完成了所谓的自动释放资源这一过程
这里只是一般智能指针的简单框架的模拟实现, 真正的各种智能指针的实现在第三大点详细介绍
//智能指针框架
//主要实现3点
//1.资源的初始化与释放
//2.像指针一样的行为
//3.拷贝与赋值(且必须是浅拷贝,采用计数法调用析构)
template
class MySmartPtr
{
public:
MySmartPtr(const T* ptr)
:_ptr(ptr)
,_pCount(new int(1))
{}
MySmartPtr(MySmartPtr& sp)
{
//...
}
MySmartPtr& operator=(MySmartPtr& sp)
{
//...
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~MySmartPtr()
{
//...
}
private:
T* _ptr;
int* _pCount;
};
--->C++98 auto_ptr, 拷贝有很严重的问题, 基本没人用
在C++98支持了auto_ptr智能指针, 但是auto_ptr在拷贝时相当于一种资源控制权的转移
如果std::auto_ptr p1= new int;
std::auto_ptr p2(p1);
这种行为会使得之前由p1管理的资源转交到了p2手上, p1中指针置为空
当在对p1进行访问的时候, 就会对空指针访问导致进程崩溃
--->后来boost库, 为C++标准库探索, 发表scoped_ptr/shared_ptr/weak_ptr三种智能指针
三种智能指针各有用途, 并且解决了auto_ptr拷贝的缺陷
scoped_ptr禁止拷贝或赋值
shared_ptr通过计数的方式控制资源释放, 从根源上解决智能指针拷贝与赋值的问题
weak_ptr专门为了解决shared_ptr中的循环引用问题
--->再后来C++ TR1, 引入了shared_ptr等。不过注意的是TR1并不是标准版
--->C++11用了boost库中的智能指针, 并将scoped_ptr重命名为unique_ptr
现在我们的C++程序在使用智能指针时, 通常是使用unique_ptr/shared_ptr/weak_ptr
三者的使用需要包头文件memory
以拷贝举例, 使用编译器默认生成的拷贝构造, 只是值拷贝, 属于浅拷贝, 所以在对象销毁调用析构时, 由于多个对象同时释放同一块资源, 就导致一块资源被多次释放, 而delete释放资源后又不会将指针置为nullptr, 对一个非空指针delete, 进程就会报错
通常情况下我们使用深拷贝来解决这样的问题, 所谓深拷贝, 就是不仅仅进行值拷贝, 而是连资源也一起拷贝过来, 但是这与智能指针的用途相违背, 意思就是, 智能指针的RAII思想, 是一种资源托管给对象的行为, 如果拷贝了对象的同时也对资源进行了拷贝, 那么这两个智能指针就分别管控两个值相同但空间不同的资源, 拷贝者的愿意应该是用两个智能指针管理同一个资源才发生的拷贝, 所以, 既然不可以用深拷贝来解决这种问题, 我们就要另辟蹊径. 采用计数的方式来对资源进行释放
计数法释放资源的原理:
每拷贝一个智能指针, 就让计数++一次, 当计数减为0时, 说明不再有指针管理这个资源, 就可以delete掉了
但是, 这个计数应该如何定义呢
思考1: 不可以使用普通成员变量来定义, 因为每个类对象都有属于自己的成员变量, 无法与其他对象产生关联
思考2: 不可以使用静态成员变量来定义, 因为所有对象都靠一个静态成员变量来管理, 这是不合理的, 也无法管理, 因为智能指针是类模板, 可能会有多种类型
综合以上思考, 得出结论
让这个计数变量也变为一种资源, 既然浅拷贝是值拷贝, 让多个智能指针管理同一块资源, 那么自然也就能让管理该资源的所有智能指针都管理计数资源, 这样就将管理同一块资源的所有指针都与计数资源产生关联, 就能进行加减操作了
new与delete[]不匹配, new[]与delete不匹配
其报错的本质原因是:
所以为了避免程序崩溃, 避免内存泄漏等严重问题, 在实际使用中必须将new delete与new[] delete[]严格匹配
所以在实现智能指针析构函数时, 需要用到定制删除器, 对应制定的new, 调用指定的删除器进行析构
定制删除器本质就是由仿函数实现的
在使用时如果需要传入的是模板参数则必须用仿函数实现(标准库unique_ptr就是这样)
如果有支持构造的重载, 可以将仿函数匿名对象传入构造函数参数中(标准库shared_ptr就是这样), 可以使用lambda表达式
template
struct Delete
{
public:
void operator()(T* ptr)
{
cout << "call: delete ptr\n";
delete ptr;
}
};
template
struct DeleteArray
{
public:
void operator()(T* ptr)
{
cout << "call: delete[] ptr\n";
delete[] ptr;
}
};
template
struct Free
{
public:
void operator()(T* ptr)
{
cout << "call: free(ptr)\n";
free(ptr);
}
};
template
struct DocumentClose
{
public:
void operator()(T* ptr)
{
cout << "call: fclose(ptr)\n";
fclose(ptr);
}
};
auto_ptr如今已没人使用, 所以学习auto_ptr的意义就是认识到它的缺点, 不要犯相同的错误
在C++标准库中, unique_ptr的定制删除器采用模板参数, 在实例化模板显式写入仿函数类型来控制
shared_ptr的定制删除器采用构造重载, 在调用构造函数时, 以传入仿函数对象来控制
template
struct DeleteArray
{
public:
void operator()(T* ptr)
{
cout << "call: delete[] ptr\n";
delete[] ptr;
}
};
int main()
{
std::unique_ptr> up(new int[5]);
std::shared_ptr sp(new int[5], DeleteArray());
return 0;
}
对于auto_ptr只用程序验证它的缺陷, 不做实现, 因为实际中没人用这个, 实现起来也没有意义
unique_ptr禁止拷贝与赋值, 其余与shared_ptr相同
//C++98实现方式
private:
MyUniquePtr(MyUniquePtr& sp);
MyUnipuePtr& operator=(MyUniquePtr& sp);
//C++11实现方式
MyUniquePtr(MyUniquePtr& sp) = delete;
MyUnipuePtr& operator=(MyUniquePtr& sp) = delete
template
struct Delete
{
public:
void operator()(T* ptr)
{
cout << "call: delete ptr\n";
delete ptr;
}
};
template
struct DeleteArray
{
public:
void operator()(T* ptr)
{
cout << "call: delete[] ptr\n";
delete[] ptr;
}
};
template
struct Free
{
public:
void operator()(T* ptr)
{
cout << "call: free(ptr)\n";
free(ptr);
}
};
template
struct DocumentClose
{
public:
void operator()(T* ptr)
{
cout << "call: fclose(ptr)\n";
fclose(ptr);
}
};
template>
class MySharedPtr
{
public:
MySharedPtr(T* ptr)
:_ptr(ptr)
, _pCount(new int(1))
{}
MySharedPtr(MySharedPtr& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;
}
MySharedPtr& operator=(MySharedPtr& sp)
{
if (_ptr == sp._ptr)
{
return *this;
}
//1.检查原计数
if (--(*_pCount) == 0)
{
DeleteSource();
}
//2.赋值
_ptr = sp._ptr;
_pCount = sp._pCount;
//3.++新计数
(*_pCount)++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](int pos)
{
return *(_ptr + pos);
}
~MySharedPtr()
{
if (--(*_pCount) == 0)
{
DeleteSource();
}
}
void DeleteSource()
{
D()(_ptr);
delete _pCount;
}
private:
T* _ptr;
int* _pCount;
};
void test1()
{
cout << "-------------test1-------------" << endl;
MySharedPtr sp1(new int(5));
MySharedPtr sp2(new int(10));
MySharedPtr sp3(sp1);
cout << *sp1 << endl;
cout << *sp2 << endl;
cout << *sp3 << endl;
++(*sp1);
++(*sp2);
sp3 = sp2;
cout << *sp1 << endl;
cout << *sp2 << endl;
cout << *sp3 << endl;
}
void test2()
{
cout << "-------------test2-------------" << endl;
MySharedPtr> sp4(new int[10]);
for (int i = 0; i < 10; ++i)
{
//*(&(*sp4) + i) = i;
sp4[i] = i;
}
for (int i = 0; i < 10; ++i)
{
cout << ++sp4[i] << ' ';
}
cout << endl;
}
void test3()
{
cout << "-------------test3-------------" << endl;
MySharedPtr> sp5(fopen("log.txt", "w"));
MySharedPtr sp6(new int(1));
MySharedPtr> sp7(new int[15]);
MySharedPtr> sp8((int*)malloc(sizeof(8)));
}
int main()
{
test1();
test2();
test3();
return 0;
}
#include
struct A
{
int _val = 5;
std::shared_ptr _prev;
std::shared_ptr _next;
};
int main()
{
std::shared_ptr sp1(new A);
std::shared_ptr sp2(new A);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
weak_ptr抛弃了RAII思想, 主要为了解决shared_ptr的循环引用的问题
weak_ptr就像是shared_ptr的跟班, 独立使用没有价值, 要与shared_ptr配合使用
weak_ptr用在从shared_ptr的拷贝与赋值的情况, shared_ptr对象是可以拷贝或者赋值过来的
拷贝/赋值期间只将指针成员指向资源, 不参与资源控制计数, 但是也会将计数记录下来, 以防指向对象被析构之后的野指针问题
使用weak_ptr解决shared_ptr循环引用问题:
#include
struct A
{
int _val;
std::weak_ptr _prev;
std::weak_ptr _next;
};
int main()
{
std::shared_ptr sp1(new A);
std::shared_ptr sp2(new A);
sp1->_next = sp2;
sp2->_prev = sp1;
sp1.~shared_ptr();
sp2.~shared_ptr();
return 0;
}