主要介绍智能指针(auto_ptr、unique_ptr、shared_ptr、weak_ptr)和特殊类的设计(单例模式)。
主要是存在内存泄漏、异常安全等问题。
1)、内存泄漏,丢的是指针,还是地址?
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:我们知道进程结束,会回收释放各种PCB等资源,如此看来内存泄漏也没什么,进程结束了也会被释放。但实际上,长期运行的程序出现内存泄漏影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
1)、C/C++程序中一般有两种方面的内存泄漏
①堆内存泄漏(Heap leak):
说明:堆内存指的是程序执行中依据须要分配通过malloc
/ calloc
/ realloc
/ new
等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak
。
②系统资源泄漏
说明:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
(比如僵尸进程,就属于系统级别的内存泄漏。)
2)、内存泄漏非常常见,解决方案分为两种
1、事前预防型:如智能指针等。
2、事后查错型:如使用泄漏检测工具。
实际上,我们应该养成良好的编码规范,申请的内存空间记着匹配的去释放。
1)、定义介绍
RAII(Resource Acquisition Is Initialization) :是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
功能: 在对象构造时获取资源,接着控制对资源的访问,使之在对象的生命周期内始终保持有效,最后在对象析构时候释放资源。借此,我们把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
①不需要显式地释放资源。
②采用这种方式,对象所需的资源在其生命期内始终保持有效。
1)、基本构架
RAII实则是一种设计思想。这里我们使用它设计一个类,用于资源的自动释放(delete/free)。
相关演示如下:关于这个类,可以是模板,也可以是具体的类型,根据需求来定。
template<class T>
class SmartPtr//智能指针
{
public:
SmartPtr(T* ptr = nullptr)//构造函数
:_ptr(ptr)
{}
~SmartPtr()//析构函数
{
cout << "delete: " << _ptr << endl;
delete _ptr;//在析构时将ptr指向位置资源释放
}
private:
T* _ptr;
};
基于此,针对1中问题引入,于是便有了如下解决:
用于演示的代码:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw invalid_argument("除零错误!");
}
return (double)a / (double)b;
}
void Func()
{
SmartPtr<int>sp1(new int);
SmartPtr<int>sp2(new int);
int a, b;
//a = 3; b = 0;
a =3; b = 2;
printf("Divesion: %d / %d = %lf\n", a, b, Division(a, b));
}
int main()
{
try {
Func();
}
catch(const exception & e)
{
cout << e.what() << endl;
}
return 0;
}
此外,无论是否出现异常,对于动态申请出来的空间,都得到了释放。
2)、完善1.0
说明:上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用*
,也可以通过->
去访问所指空间中的内容,因此,我们需要继续完善该对象:
1. RAII特性
2. 重载operator*和opertaor->,使其具有像指针一样的行为。
以下为相关实现:
template<class T>
class SmartPtr//智能指针
{
public:
SmartPtr(T* ptr = nullptr)//构造函数
:_ptr(ptr)
{}
~SmartPtr()//析构函数
{
cout << "delete: " << _ptr << endl;
delete _ptr;//在析构时将ptr指向位置资源释放
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
虽然上述RAII为我们提供了解决内存泄漏的一种思想设计,但上述的SmartPtr实则存在问题:拷贝。
1)、此处浅拷贝存在什么问题?
SmartPtr中,成员变量为指针类型,其是内置类型。根据之前所学可知,对内置类型会进行浅拷贝,那么调用析构时必然会引起两个指针指向的同一块空间被析构两次。
2)、是否能用深拷贝?
回答:不能,因为这里使用指针的需求就是让指针指向同一块空间共同管理。
上述问题可以如何解决?
实际C++库中有提供几类智能指针,这里我们可以学习了解一下它们的相关实现方案。
C++98版本的库中就提供了auto_ptr
的智能指针,相关参考网站:auto_ptr。
1)、演示auto_ptr
验证:我们以一个自己实现的类来演示auto_ptr
有做到资源释放。以下为我们自己写的类,主要用于观察库中实现的智能指针出了作用域自动调用析构会释放资源(RAII)。
class A
{
public:
~A()
{
cout << "~A() : "<< this << endl;
}
//private:
int _a = 0;//初始化列表
int _b = 0;
};
以下为验证代码:
void Func()
{
auto_ptr<A>ap1(new A);
auto_ptr<A>ap2(new A);
(*ap1)._a = 2;//若成员变量为公有,这里可以演示智能指针内部实现了 *和->
(*ap2)._b = 4;
printf("ap1: %d, %d \n", ap1->_a, ap1->_b);
printf("ap2: %d, %d \n", ap2->_a, ap2->_b);
cout << "---------------------------------" << endl;
auto_ptr<A>ap3(ap2);//验证拷贝
ap3->_a = 333;
printf("ap3: %d, %d \n", ap3->_a, ap3->_b);
ap3 = ap1;//验证赋值
ap3->_a = 444;
printf("ap3: %d, %d \n", ap3->_a, ap3->_b);
}
int main()
{
try {
Func();
}
catch (...)
{
cout << "another" << endl;
}
return 0;
}
说明:auto_ptr的拷贝/赋值运算符重载,会将资源的管理权转移,如此一来会导致被拷贝的对象悬空。此时若错用该对象,进行解引用等操作行为,会带来解引用空指针等问题。(虽然不会释放同一块多次,但这种拷贝方式并不妥当。)
虽然auto_ptr
存在缺陷,但我们仍旧需要学习了解它的实现方式:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "delete: " << _ptr << endl;//用于检测
delete _ptr;
}
auto_ptr(auto_ptr<T>& ap)
{
if(_ptr)//若当前指针存在指向,需先释放当前对象指向资源
{
cout << "delete: " << _ptr << endl;//用于检测
delete _ptr;
}
_ptr = ap._ptr;//浅拷贝
ap._ptr = nullptr;//将原先对象指向置空(注意这里是delete)
}
auto_ptr<T>& operator = (auto_ptr<T>& ap)
{
if (this != &ap)//检测是否自己给自己赋值
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;//用于检测
delete _ptr;
}
_ptr = ap._ptr;//浅拷贝
ap._ptr = nullptr;//将原先对象指向置空(注意这里是delete)
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
C++11中提供unique_ptr,其用法和之前相同,区别在于 unique_ptr
防拷贝,因此不能赋值、拷贝。
相关参考网站:unique_ptr
演示代码如下:
class A
{
public:
~A()
{
cout << "~A() : " << this << endl;
}
//private:
int _a = 0;//初始化列表
int _b = 0;
};
void Func()
{
//验证析构
unique_ptr<A> up1(new A);
unique_ptr<A> up2(new A);
//验证operator*()、operator->()
up1->_a = 2;
(*up1)._b = 3;
printf("up1: %d, %d \n", up1->_a, up1->_b);
printf("up2: %d, %d \n", up2->_a, up2->_b);
//验证是否能拷贝构造/赋值
unique_ptr<A> up3 = up1;//error
unique_ptr<A> up4(up2);//error
}
int main()
{
try {
Func();
}
catch (...)
{
cout << "another" << endl;
}
return 0;
}
演示结果如下:
说明:C++11提供了关键字delete,我们直接对拷贝构造函数和赋值运算符重载函数使用该关键字即可。若是C++98,我们可以只声明不实现,为了防止类外实现,需要将它们设置为私有成员。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete: " << _ptr << endl;//用于检测
delete _ptr;
}
unique_ptr(unique_ptr<T>& up) = delete;//注意这里语句加;
unique_ptr<T>& operator = (unique_ptr<T>& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
虽然unique_ptr相较于auto_ptr更为靠谱,但禁用并不能满足拷贝/赋值的需求,因此有了这里能支持拷贝的shared_ptr
。
相关文档:shared_ptr。
演示代码:
class A
{
public:
~A()
{
cout << "~A() : " << this << endl;
}
//private:
int _a = 0;//初始化列表
int _b = 0;
};
void Func()
{
//验证析构
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2(new A);
//验证operator*()、operator->()
sp1->_a = 2;
(*sp1)._b = 3;
printf("sp1: %d, %d \n", sp1->_a, sp1->_b);
printf("sp2: %d, %d \n", sp2->_a, sp2->_b);
//验证是否能拷贝构造/赋值
shared_ptr<A> sp3 = sp1;
sp3->_a++;
sp3->_b++;
shared_ptr<A> sp4(sp2);
sp4->_a--;
sp4->_b--;
printf("sp1: %d, %d \n", sp1->_a, sp1->_b);
printf("sp2: %d, %d \n", sp2->_a, sp2->_b);
}
int main()
{
try {
Func();
}
catch (...)
{
cout << "another" << endl;
}
return 0;
}
说明:shared_ptr是通过引用计数的方式来实现多个shared_ptr对象之间共享资源的。
1、在shared_ptr对象内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2、出了作用域调用析构函数,对象销毁,此时引用计数会减一(--
)。
3、若引用计数减到0,说明当前shared_ptr是最后一个使用该资源的对象,此时必须释放资源;若引用计数自减后未达到0,说明除了当前shared_ptr外还有其他shared_ptr在使用该份资源,此时不能释放资源,否则其他对象就成了野指针。
1)、如何设计引用计数?
问题一:考虑到引用计数要被多个对象共享,是否可以将其设置为静态的成员变量?
template<class T>
class shared_ptr
{
public:
//……
private:
T* _ptr;//指针
static int count;//定义一个静态成员变量:用于表示引用计数
};
如下:在析构、赋值、拷贝时,对引用计数count进行++
、--
,从而控制多个指针管理同一份资源。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
if(_ptr)
++_count;
}
void release()
{
if (_ptr && (--_count) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
{
release();
_ptr = sp._ptr;
_count = sp._count;
_count++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
release();
_ptr = sp._ptr;
_count = sp._count;
_count++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
static int getCount()
{
return _count;
}
private:
T* _ptr;
static int _count;
};
template<class T>
int shared_ptr<T>::_count = 0;//静态成员:类中声明,类外定义
回答:上述使用静态成员变量,看似没有问题,实则存在缺陷。要知道静态成员是被整个类共享的,假如有多个不同的资源空间,分别为其申请多个指针进行资源管理,那么静态的count就乱套了。
基于上述,引用计数可以使用一个指针来表示,_pcount(new int(1))
,构造时在堆上申请。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
,_pcount(new int(1))
{}
//……
private:
T* _ptr;
int* _pcount;
};
}
以下为其它实现细节:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
,_pcount(new int(1))
{}
void release()
{
//当count减到0时才释放资源,其余情况减计数器
if (--(*_pcount) == 0 && _ptr)//短路设计:不用分别判断两种情况,count都会自减一次
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//存在原本就指向同一份资源的情况。
//这里不使用this!= &sp的原因:除了sp1=sp1,还可以是sp1=sp2,虽然对象不同,但指向的资源相同.
if (_ptr != sp._ptr)
{
release();//赋值时,若当前指针原先有指向,要先进行处理。
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int getCount()//返回引用计数
{
return *_pcount;
}
T* get()const//返回指针
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
演示结果如下:
相关验证代码:
class A
{
public:
~A()
{
cout << "~A() : " << this << endl;
}
//private:
int _a = 0;//初始化列表
int _b = 0;
};
void Func()
{
//operator*()、operato->()
mySAII::shared_ptr<A> sp1(new A);
cout << "count: " << sp1.getCount() << endl;
printf("sp1: %d, %d \n", sp1->_a, sp1->_b);
验证是否能拷贝构造/赋值
mySAII::shared_ptr<A> sp3 = sp1;
cout << "count: " << sp1.shared_ptr<A>::getCount() << endl;
sp3->_a++;sp3->_b++;
printf("sp1: %d, %d \n", sp1->_a, sp1->_b);
cout << "--------------------------" << endl;
//另一个动态空间
mySAII::shared_ptr<A> sp2(new A);
cout << "count: " <<sp2.getCount() << endl;
sp2->_a = 2; sp3->_b = 2;
printf("sp2: %d, %d \n", sp2->_a, sp2->_b);
mySAII::shared_ptr<A> sp4(sp2);
cout << "count: " << sp4.getCount() << endl;
printf("sp2: %d, %d \n", sp2->_a, sp2->_b);
cout << "--------------------------" << endl;
mySAII::shared_ptr<int> sp5(new int);
cout << "count: " << sp5.getCount() << endl;
}
version3.0见下述删除器版本(注意那里的实现方案和库中不同,主要在于了解并学习删除器问题。
1)、上述shared_ptr面临的线程安全问题举例
演示代码如下:
namespace mySAII_3
{
template<class T>
struct Delete
{
void operator()(T* ptr)
{
cout << "缺省参数delete:" << ptr << endl;
delete ptr;//默认情况下,new单个,若new多个,则传入对应的删除器
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
, _pcount(new int(1))
{}
void release()
{
//当count减到0时才释放资源,其余情况减计数器
if (--(*_pcount) == 0 && _ptr)//短路设计:不用分别判断两种情况,count都会自减一次
{
D()(_ptr);//释放指针:使用一个匿名对象
delete _pcount;//释放引用计数
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int getCount()
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
int main()
{
mySAII_3::shared_ptr<int> sp1 = new int(1);
mySAII_3::shared_ptr<int> sp2(sp1);
vector<thread> vt(2);//创建两个线程
int n = 100;
for (auto& t : vt)
{
t = thread([&]() {
for (size_t i = 0; i < n; ++i)
{
mySAII_3::shared_ptr<int> sp(sp1);//在两个线程中:分别创建新的shared_ptr指针,指向sp1
}
});
}
for (auto& t : vt)
{
t.join();//一一捕捉创建出来的线程。
}
//检验:看看count计数是否正确
cout << "count: " << sp1.getCount() << endl;
cout << *sp1 << endl;
return 0;
}
说明: 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的。最终会导致资源未释放或者程序崩溃的问题。所以需要对智能指针中引用计数++、–的操作进行加锁,以保障线程安全。
2)、version4.0(加锁版shared_ptr)
相关实现如下:实际只用修改构造、析构对引用计数++、–的部分。(需要注意,这里我们新增了一个成员变量,并将其在构造时new出来,那么析构时不要忘记释放)。
namespace mySAII_3
{
template<class T>
struct Delete
{
void operator()(T* ptr)
{
cout << "缺省参数delete:" << ptr << endl;
delete ptr;//默认情况下,new单个,若new多个,则传入对应的删除器
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
, _pcount(new int(1))
,_pmutex(new mutex)
{}
void release()
{
bool isdelete = false;
_pmutex->lock();//加锁
//当count减到0时才释放资源,其余情况减计数器
if (--(*_pcount) == 0 && _ptr)//短路设计:不用分别判断两种情况,count都会自减一次
{
D()(_ptr);//释放指针:使用一个匿名对象
delete _pcount;//释放引用计数
isdelete = true;
}
_pmutex->unlock();//解锁
if (isdelete)//判断是否需要释放锁:为了保证引用计数的线程安全问题,同时也为了能够释放锁,引入flags做为判断
delete _pmutex;
}
~shared_ptr()
{
release();
}
//拷贝构造:指向对象相同、引用计数相同、持有/竞争的锁相同
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmutex(sp._pmutex)
{
_pmutex->lock();//加锁
(*_pcount)++;//引用计数一次只能允许一个线程操作
_pmutex->unlock();//解锁
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();//赋值:该智能指针可能原本就指向某一个对象,需要先做处理
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmutex = sp._pmutex;
_pmutex->lock();//加锁
(*_pcount)++;//引用计数一次只能允许一个线程操作
_pmutex->unlock();//解锁
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int getCount()
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;//智能指针
int* _pcount;//引用计数
mutex* _pmutex;//锁
};
}
3)、其它问题说明
说明:加锁只是解决了智能指针内部赋值、拷贝时,引用计数存在的线程安全问题。但不能保障该智能指针指向的对象,在多线程中使用时存在的线程安全问题。
例如:智能指针管理的对象存放在堆上,两个线程中同时去访问,仍旧存在线程安全,但这属于外部实现时,需要我们自己手动保障。
对于shared_ptr,即使是库中版本,它也只是解决自身引用计数的线程安全问题。
1)、场景引入
说明:在shared_ptr
中,当两个对象相互引用时,即使其他对象不再引用它们,它们的引用计数会一直保持非零状态,导致这些对象所占用的内存无法被释放,造成内存泄漏。
以下为演示代码:
struct Node
{
int _val;//当前节点变量
std::shared_ptr<Node> _next;//指向前一个节点和后一个节点的指针
std::shared_ptr<Node> _prev;
~Node()//析构:这里不用做处理,因为_val为内置类型,而_next、_prev会去调用它们自己的析构函数
{
cout << "~Node" << endl;
}
};
void Func()
{
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
cout << "n1:" << n1.use_count() << endl;
cout << "n2:" << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << "n1:" << n1.use_count() << endl;
cout << "n2:" << n2.use_count() << endl;
}
问题分析:
如上图, 虽然n1和n2析构使得引用计数减到1,但此时_next还指向右侧节点,_prev还指向左侧节点。
也就是说,只有当左侧的_next析构了,使得引用计数减到0,右侧节点才会被delete(释放资源)。可是此时因为n1指针已近释放,能对左侧_next释放的,只有右侧还在指向它的_prev指针。但对于右侧的_prev指针来说,其面临的情况和_next相同。(由于二者彼此卡住,此时谁也不会释放)
2)、解决方案
为了解决这个问题,一种常用的方法是使用弱引用(weak reference)
,将在后续讲解。实际上循环引用的一个关键点在于,要识别到当前场景发生了循环引用。
struct Node
{
int _val;//当前节点变量
std::weak_ptr<Node> _next;//指向前一个节点和后一个节点的指针
std::weak_ptr<Node> _prev;//这里使用了weak_ptr
~Node()
{
cout << "~Node" << endl;
}
};
void Func()
{
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
cout << "n1:" << n1.use_count() << endl;
cout << "n2:" << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << "n1:" << n1.use_count() << endl;
cout << "n2:" << n2.use_count() << endl;
}
相关文档:weak_ptr。通过查阅可知,weak_ptr
支持使用shared_ptr
进行构造。
核心原理:不增加引用计数,不参与资源释放管理(无需delete
指针)。
template<class T>
class weak_ptr
{
public:
weak_ptr()//无参构造
:_ptr(nullptr)
{}
weak_ptr(const weak_ptr<T>& wp)//拷贝构造
:_ptr(wp._ptr)
{}
weak_ptr(const shared_ptr<T>& sp)//shared_ptr进行构造
:_ptr(sp.get())
{}
weak_ptr<T>& operator = (const weak_ptr<T>& wp)
{
if (_ptr != wp._ptr)
{
_ptr = wp.get();
}
return *this;
}
//赋值运算符重载中,不用考虑delete释放问题,因为weak_ptr不参与资源释放管理。
//析构函数~weak_ptr()同理。不需要在内部 delete _ptr。
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp.get())
{
_ptr = sp.get();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
使用演示如下:这里只是用于演示weak_ptr解决了循环引用。而实际实现中,其功能更为复杂。
1)、关于new和new[ ]类型不匹配(delete、delete[ ])
说明:关于这里演示的自定义类型和内置类型new/delete不匹配时进程崩溃的区别,其中一个因素与平台有关。但不管是什么类型,建议new/delete匹配使用。
原因解释:实则是delete释放时访问位置不对。(与new的底层实现有关)
在上述场景中,对于这类new[ ]一次多个的对象,和那些不是new出来的对象,如何通过智能指针进行管理?
为此,我们引入了删除器。
shared_ptr中设计了一个删除器来解决这个问题。实际官方库中也提供了一个删除器::default_delete。
在unique_ptr和shared_ptr的构造函数中,会提供带删除器参数的构造:(可自行查阅文档查看。)
实际上我们可以自己写一个删除器,它可以是仿函数对象、也可以是lambda表达式。
1)、代码演示一
演示代码:
struct Node
{
int _val;//当前节点变量
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)//该删除器就是作用于一次申请多个资源的指针delete[]
{
cout << "delete[]" << ptr << endl;//用于检测观察
delete[] ptr;
}
};
template<class T>
struct FreeArray
{
void operator()(T* ptr)
{
cout << "free()" << ptr << endl;//用于检测观察
free(ptr);
}
};
void Func()
{
//std::shared_ptr n1(new Node[5]);
std::shared_ptr<Node> n1(new Node[5],DeleteArray<Node>());
//shared_ptr中,该删除器是作为构造函数的参数出现的,因此这里传递的是一个对象(我们使用了匿名对象)
std::shared_ptr<Node> n2(new Node);
//std::shared_ptr n3(new int[5]);
std::shared_ptr<int> n3(new int[5],DeleteArray<int>());
//std::shared_ptr n4((int*)malloc(sizeof(int) * 5));
std::shared_ptr<int> n4((int*)malloc(sizeof(int) * 5), FreeArray<int>());
}
使用lambda表达式的情况如下:(注意这里是针对shared_ptr而已,对于unique_ptr,由于其删除器设置在模板参数中,因此只能传递类类型。)
void Func()
{
//std::shared_ptr n1(new Node[5]);
std::shared_ptr<Node> n1(new Node[5], [](Node* ptr) {delete[] ptr; });
std::shared_ptr<Node> n2(new Node);
//std::shared_ptr n3(new int[5]);
std::shared_ptr<int> n3(new int[5], [](int* ptr) {delete[] ptr; });
//std::shared_ptr n4((int*)malloc(sizeof(int) * 5));
std::shared_ptr<int> n4((int*)malloc(sizeof(int) * 5), [](int* ptr) {free(ptr); });
}
2)、代码演示二
这里我们也可以实现一个带删除器版本的shared_ptr,只不过此处借用了unique_ptr的模板参数方式。
只改变了release()
函数中delete指针的部分。
template<class T>
struct Delete
{
void operator()(T* ptr)
{
cout << "缺省参数delete:" << ptr << endl;
delete ptr;//默认情况下,new单个,若new多个,则传入对应的删除器
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
, _pcount(new int(1))
{}
void release()
{
//当count减到0时才释放资源,其余情况减计数器
if (--(*_pcount) == 0 && _ptr)//短路设计:不用分别判断两种情况,count都会自减一次
{
D()(_ptr);//使用一个匿名对象
delete _pcount;
}
}
整体情况
namespace mySAII_2
{
template<class T>
struct Delete
{
void operator()(T* ptr)
{
cout << "缺省参数delete:" << ptr << endl;
delete ptr;//默认情况下,new单个,若new多个,则传入对应的删除器
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
, _pcount(new int(1))
{}
void release()
{
//当count减到0时才释放资源,其余情况减计数器
if (--(*_pcount) == 0 && _ptr)//短路设计:不用分别判断两种情况,count都会自减一次
{
D()(_ptr);//使用一个匿名对象
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int getCount()
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
演示结果:
注意这里传参,和库中的不同,我们实现时,将删除器作为模板参数使用,那么传参时不是传递函数对象,而是类类型。
void Func()
{
//std::shared_ptr n1(new Node[5]);
mySAII_2::shared_ptr<Node, DeleteArray<Node>> n1(new Node[5]);
mySAII_2::shared_ptr<Node> n2(new Node);
//std::shared_ptr n3(new int[5]);
mySAII_2::shared_ptr<int, DeleteArray<int>> n3(new int[5]);
//std::shared_ptr n4((int*)malloc(sizeof(int) * 5));
mySAII_2::shared_ptr<int, FreeArray<int>> n4((int*)malloc(sizeof(int) * 5));
}
1)、问题分析
两个涉及拷贝的场景:赋值运算符重载、拷贝构造。
因此,想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。结合之前所学,在C++98中,我们可以对这两个默认函数只声明不实现,并将其设置为私有;在C++11中我们可以使用delete关键字。
2)、相关写法
C++98: ①只声明不定义,该函数就不会被调用。②设置成私有,就禁止了在类外定义实现这两个函数的可能。
class Bancopy
{
//……
private:
Bancopy(const Bancopy& copy);
Bancopy& operator=(const Bancopy& copy);
//……
};
C++11:C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class Bancopy
{
Bancopy(const Bancopy& copy) = delete;
Bancopy& operator=(const Bancopy& copy) = delete;
//……
};
1)、基础认知:一个类可以在地址空间的哪些区域被创建?
int main()
{
HeapOnly hp1;//在栈区:函数栈帧
static HeapOnly hp2;//在静态区:静态变量
HeapOnly* hp3 = new HeapOnly;//在堆区:new/malloc等
return 0;
}
1)、思路分析:析构函数私有化
说明: 由于对象创建出来,其声明周期结束后都需要调用析构函数。若将析构函数私有,在函数中创建的类对象(main函数也是函数栈帧、不论该对象是静态与否)会因为无法调用析构而不能创建。因此最终只剩下堆上申请空间的方式。
int main()
{
HeapOnly* hp3 = new HeapOnly;//在堆区:new/malloc等
return 0;
}
回答: 这里hp3
并不是实际的对象本身,而是指向该对象的指针,那么其生命周期结束后,hp3
指针本身不需要析构(析构函数作用于其指向的对象),但这样一来也会造成一个问题,我们无法释放该指针指向空间。
问题: 针对上述问题,如何解决?
回答: ①private私有,虽然不能在类外访问,但可以在类内访问。因此我们可以提供一个用于delete的函数。②delete底层为operator delete()。
2)、相关实现
①提供函数的情况:
②使用operator delete()
的情况演示:
class HeapOnly
{
public:
void Destroy()//写法一
{
delete this;
}
static void Destroy(HeapOnly* ptr)//写法二:静态可以使用类名调用
{
delete ptr;
}
private:
~HeapOnly()
{
cout << "~HeapOnly()" << endl;//用于观察
}
int _a = 111;//用于观察
};
int main()
{
HeapOnly* hp3 = new HeapOnly;
hp3->Destroy();//写法一
HeapOnly::Destroy(hp3);//写法二
operator delete (hp3);//写法三:operator delete()
return 0;
}
说明: 关于构造函数私有的写法,我们在之前 类和对象(四)中演示讲解过。
上述类还存在一定缺陷: 可以被拷贝和赋值。因此我们可以将其禁用。
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
HeapOnly(const HeapOnly& hp) = delete;//禁止拷贝
HeapOnly& operator =(const HeapOnly& hp) = delete;//禁止赋值
private:
HeapOnly()//构造私有
{
cout << "HeapOnly()" << endl;//用于观察
}
int _a = 111;//用于观察
};
int main()
{
HeapOnly* hp1 = HeapOnly::CreateObj();
cout << endl;
return 0;
}
说明: 这里的实现方法和上述相同,只是此处由于传值返回,不能使用delete禁止拷贝构造函数。
相关实现:
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
StackOnly()//构造函数私有化
{
cout << "StackOnly()" << endl;
}
int _b = 222;
};
int main()
{
StackOnly s1 = StackOnly::CreateObj();
cout << endl;
return 0;
}
C++98: 基类构造函数私有化。
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11: 使用final
关键字。被final修饰类,表示该类不能被继承。
class NonInherit final
{
public:
private:
int _parent;
};
class child:public NonInherit
{
private:
int _child;
};
int main()
{
child ch;
return 0;
}
1)、设计模式
设计模式(Design Pattern):是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
使用设计模式的目的: 为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化,是软件工程的基石脉络,如同大厦的结构一样。
2)、单例模式
单例模式: 一个类只能实例化一个对象,其是进程内的唯一对象,即单例模式。该模式可以保证系统中该类只有一个实例,被所有程序模块共享,访问时会提供一个全局访问点。
场景举例: 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式的两种实现模式: 饿汉模式、懒汉模式。(实际可以有好几种模式说法,这里统一划分为两类)
饿汉模式: 一开始(在main函数前)就创建出对象。即,不管将来用不用到,程序启动时就创建一个唯一的实例对象。
方法: ①构造函数私有化:防止在类外任意创建多个对象。②提供一个静态类成员变量,其类型可以是类本身,也可以是指向该类的指针。③提供一个静态成员函数,用于返回该静态成员变量。
以下为框架搭建:
class Singleton
{
public:
static Singleton* getInstance()
{
return psg;//方式一
return &sg;//方式二
}
private:
Singleton()//构造函数私有化
{}
static Singleton* psg;//提供一个静态成员·方式一
static Singleton sg;//方式二
};
Singleton* Singleton::psg = new Singleton;//方式一
Singleton Singleton::sg;//方式二:在程序入口之前就完成单例对象的初始化
int main()
{
return 0;
}
简单演示:单例,为了确保唯一性防止拷贝,需要将拷贝构造、赋值都禁止。
class Singleton
{
public:
static Singleton* getInstance()
{
return _psg;
}
Singleton(const Singleton& sn) = delete;// 防拷贝
Singleton& operator=(const Singleton& sn) = delete;// 防拷贝
void Debug()//测试
{
cout << "Debug: " << _psg << endl;
}
private:
Singleton()//构造函数私有化
{
cout << "Singleton() " << _psg << endl;
}
static Singleton* _psg;
};
Singleton* Singleton::_psg = new Singleton;//在程序入口之前就完成单例对象的初始化
int main()
{
Singleton* ps = Singleton::getInstance();
ps->Debug();
Singleton::getInstance()->Debug();
return 0;
}
优点说明: 简单、没有线程安全问题
缺陷说明:
①一个程序中,多个单例,若有先后创建初始化顺序要求时,饿汉无法控制。(例如:程序两个单例类A 和 B,假设要求A先创建初始化,B再创建初始化。静态成员无法控制谁先被初始化创建,尤其是一个项目多个文件中。)
②饿汉模式的单例类,若初始化时任务多,会影响程序启动速度。
PS:
①如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么使用饿汉模式来避免资源竞争,提高响应速度更好。
②如果单例对象构造十分耗时或者占用很多资源,如需要加载插件、 初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
懒汉模式: 第一次实际使用时,对象才创建。
优点: ①不影响启动速度;②多个单例实例启动顺序自由控制(只用在调用时按照需求的顺序初始化即可)。
缺点: 相对复杂,需要处理线程安全的问题。
基本框架如下:(此处没有引入线程安全问题,后续可继续完善。)
class Singleton
{
public:
static Singleton* getInstance()
{
if (_psg == nullptr)
_psg = new Singleton;
return _psg;
}
Singleton(const Singleton& sn) = delete;// 防拷贝
Singleton& operator=(const Singleton& sn) = delete;// 防拷贝
void Debug()//测试
{
cout << "Debug: " << _psg << endl;
}
private:
Singleton()//构造函数私有化
{
cout << "Singleton() " << _psg << endl;
}
static Singleton* _psg;
};
Singleton* Singleton::_psg = nullptr;
int main()
{
Singleton* ps = Singleton::getInstance();
ps->Debug();
Singleton::getInstance()->Debug();
return 0;
}
问题说明: 懒汉模式中,多线程存在的问题主要体现在这里的首次实例化对象。单例对象第一次使用时,_psg = new Singleton;
,对于多线程而言,若不加锁保护,有可能new多次,造成内存泄漏。
static Singleton* getInstance()
{
if (_psg == nullptr)
_psg = new Singleton;//多线程同时访问,首次时new对象,那么此处检验时在多个线程中同时满足条件,_psg被多次赋值,而先前new出来的空间无指针指向,造成内存泄漏问题。
return _psg;
}
解决方案: ①最好RAII,因为new会抛异常。②加锁设置为双检查。
static Singleton* getInstance()
{
//双检查模式设计
if (_psg == nullptr)//2、这里再加一层条件判断,是因为只有首次才会存在现象安全问题, 后续使用时,反复加锁判断又解锁属于无意义行为,影响效率。
{
lock_guard<mutex> lck(_mutex);//首次:加锁
//unique_lock lck(_mutex);//其它写法
if (_psg == nullptr)//1、这里是加锁是为了解决单例首次实例化时,多线程造成的线程安全问题
_psg = new Singleton;
}
return _psg;
}
class Singleton
{
public:
static Singleton* getInstance()
{
//双检查模式设计
if (_psg == nullptr)//2、这里再加一层条件判断,是因为只有首次才会存在现象安全问题, 后续使用时,反复加锁判断又解锁属于无意义行为,影响效率。
{
lock_guard<mutex> lck(_mutex);//首次:加锁
//unique_lock lck(_mutex);//其它写法
if (_psg == nullptr)//1、这里是加锁是为了解决单例首次实例化时,多线程造成的线程安全问题
_psg = new Singleton;
}
return _psg;
}
Singleton(const Singleton& sn) = delete;// 防拷贝
Singleton& operator=(const Singleton& sn) = delete;// 防拷贝
void Debug()//测试
{
cout << "Debug: " << _psg << endl;
}
//实现一个内嵌垃圾回收类 :内部类是外部类的友元
class CGarbo {
public:
~CGarbo() {
///……
///处理其它事务
///……
cout << "~CGarbo() " << _psg << endl;//测试
if (_psg)
delete _psg;
}
};
private:
Singleton()//构造函数私有化
{
cout << "Singleton() " << _psg << endl;
}
static Singleton* _psg;//单例对象(声明)
static mutex _mutex;//锁
};
Singleton* Singleton::_psg = nullptr;
mutex Singleton::_mutex;
// 全局的回收对象,main函数结束后,会调用它的析构函数。析构函数内对单例做了处理,会释放单例对象。
static Singleton::CGarbo gc;
说明:如下,是其它懒汉的写法,但这种写法存在的限制。
在C++11之前,这种写法不能保证线程安全,因为C++11之前,对于局部静态对象,构造函数调用初始化时并不能保证线程安全的原子性。
C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用。
class Singleton
{
public:
static Singleton* GetInstance()
{
// 局部的静态对象,在第一次调用时初始化
static Singleton _s;
return &_s;
}
private:
// 构造函数私有
Singleton(){};
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
};
如上述,对于new出来的空间对象,我们似乎没有进行释放。
① 一般情况下,单例对象不需要释放。通常整个程序运行期间都可能会用它,那么在进程正常结束后,单例模式也会被资源释放。
②在一些特殊场景需要下,比如单例对象析构时,要进行一些持久化(往文件、数据库写)操作。此时需要我们释放,可以实现一个内嵌垃圾回收类来解决。(这里以懒汉模式演示)
class Singleton
{
public:
static Singleton* getInstance()
{
if (_psg == nullptr)
_psg = new Singleton;
return _psg;
}
Singleton(const Singleton& sn) = delete;// 防拷贝
Singleton& operator=(const Singleton& sn) = delete;// 防拷贝
void Debug()//测试
{
cout << "Debug: " << _psg << endl;
}
//实现一个内嵌垃圾回收类 :内部类是外部类的友元
class CGarbo {
public:
~CGarbo() {
///……
///处理其它事务
///……
cout << "~CGarbo() " << _psg << endl;//测试
if (_psg)
delete _psg;
}
};
private:
Singleton()//构造函数私有化
{
cout << "Singleton() " << _psg << endl;
}
static Singleton* _psg;
};
Singleton* Singleton::_psg = nullptr;
// 全局的回收对象,main函数结束后,会调用它的析构函数。析构函数内对单例做了处理,会释放单例对象。
static Singleton::CGarbo gc;
int main()
{
Singleton* ps = Singleton::getInstance();
ps->Debug();
Singleton::getInstance()->Debug();
return 0;
}