一.智能指针及RAII
问题:
C++中最令人头疼的问题是强迫程序员对申请的资源(文件,内存等)进行管理,一不小心就会出现泄露(忘记对申请的资源进行释放)的问题。
C++的解决办法:RAII
在传统 C++ 里我们只好使用 new
和 delete
去『记得』对资源进行释放。而 C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。
解决思路:
利用C++中一个对象出了其作用域会被自动析构,因此我们只需要在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间,这样,就减轻了程序员在编码过程中,考虑资源释放的问题,这就是RAII
RAII,完整的英文是 Resource Acquisition Is Initialization,是 C++ 所特有的资源管理方式。
- 有少量其他语言,如 D、Ada 和 Rust 也采纳了 RAII,但主流的编程语言中, C++是唯一一个依赖RAII来做资源管理的。
- RAII依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理。对 RAII的使用,使得C++不需要类似于Java 那样的垃圾收集方法,也能有效地对内存进行管理。
具体而言,C11的stl中为大家带来了3种智能指针,正确合理的使用可以有效的帮助大家管理资源,当然,在C++的使用智能指针没有像Java,python这种具备垃圾回收机制的语言那么舒适,毕竟,程序员还需要做一些额外的事情,但是,这远比传统的C或者C++更加优雅。
3种智能指针分别是:
std::shared_ptr
强指针std::unique_ptr
std::weak_ptr
弱指针
在早期有一个auto_ptr,这四种指针在使用上有区别:
-
auto_ptr有缺陷是过时的产物。
-
unique_ptr对auto_ptr的问题进行了修正。
-
shared_ptr使用了引用计数,但是会出现循环引用的问题需要配合后面的weak_ptr一起使用。
二.引用计数
引用计数
要正确的理解智能指针,首先必须理解引用计数技术。
深拷贝、浅拷贝的概念
-
深拷贝优缺点:
- 优点:每一个的对象(哪怕是通过拷贝构造函数实例化的对象)的指针都有指向的内存空间,而不是共享,所以在对象析构的时候就不存在重复释放或内存泄露的问题了。
- 缺点:内存开销大
-
浅拷贝优缺点:
- 优点:通过拷贝构造函数实例化的对象的指针数据变量指向的共享的内存空间,因此内存开销较小。
- 缺点:对象析构的时候就可能会重复释放或造成内存泄露。
鉴于深拷贝和浅拷贝的优缺点,可采用引用计数技术,既减小了内存开销,又避免了堆的重复释放或内存泄露问题。
例1:
例1可能在VS2015编译不过,需要在属性管理器页面进行修改
重复释放:一旦其中一个对象释放了资源,那么所有的其他对象的资源也被释放了。
附例1代码:
#include <iostream>
#include <cstring>
using namespace std;
class CStudent
{
public:
CStudent(const char* pszName);
CStudent(CStudent& obj);//拷贝构造
CStudent& operator=(CStudent& obj);//拷贝赋值
void release();
void Show()//打印出变量以及地址
{
cout << hex << (int&)m_pszName << m_pszName <<endl;
}
private:
char* m_pszName;
};
CStudent::CStudent(const char* pszName)
{
m_pszName = new char[256];
strcpy(m_pszName, pszName);
}
CStudent::CStudent(CStudent& obj)//浅拷贝
{
m_pszName = obj.m_pszName;
//strcpy(m_pszName, obj.m_pszName);
}
CStudent& CStudent::operator=(CStudent& obj)//浅拷贝
{
m_pszName = obj.m_pszName;
return *this;
}
void CStudent::release()
{
if (m_pszName != NULL)
{
delete m_pszName;
m_pszName = NULL;
}
}
int main(int argc, char* argv[])
{
CStudent stu1("zhang san");
CStudent stu2("li si");
CStudent stu3 = stu2;//拷贝构造,stu2和stu3使用的是同一个资源
stu1.Show();
stu2.Show();
stu3.Show();
stu2.release();//在释放stu2的时候把stu3也给释放了,造成了重复释放的问题
stu3.Show();
return 0;
}
解决方法:增加一个变量,记录资源使用的次数
例2:添加引用计数器
附上例代码:
#include <iostream>
#include <cstring>
using namespace std;
class CStudent
{
public:
CStudent(const char* pszName);
CStudent(CStudent& obj);
~CStudent();
CStudent& operator=(CStudent& obj);
void release();
void Show()
{
if (*m_pCount > 0)
{
cout << hex << (int&)m_pszName << m_pszName <<endl;
}
}
private:
char* m_pszName;
int* m_pCount;
//资源计数器,当资源计数器减为0时,那么表示该资源可以被释放,从而避免重复释放的问题。
};
CStudent::CStudent(const char* pszName)
{
m_pszName = new char[256];
m_pCount = new int;
strcpy(m_pszName, pszName);
*m_pCount = 1;//创建资源的时候同时创建了资源计数器,初始为1
}
CStudent::CStudent(CStudent& obj)//浅拷贝
{
m_pszName = obj.m_pszName;
m_pCount = obj.m_pCount;
(*m_pCount)++;
}
CStudent::~CStudent()
{
release();
}
CStudent& CStudent::operator=(CStudent& obj)
{
if (obj.m_pszName == m_pszName)
{
return *this;
}
if (--(*m_pCount) == 0)
{
delete m_pszName;
m_pszName = NULL;
delete m_pCount;
}
m_pszName = obj.m_pszName;
m_pCount = obj.m_pCount;
(*m_pCount)++;
return *this;
}
void CStudent::release()
{
if (m_pszName != NULL && --*m_pCount == 0)
{
//通过计数器来避免资源的重复释放的问题
delete m_pszName;
m_pszName = NULL;
delete m_pCount;
}
}
int main(int argc, char* argv[])
{
CStudent stu1("zhang san");
CStudent stu2("li si");
CStudent stu3 = stu2;
stu1.Show();
stu2.Show();
stu3.Show();
stu2.release();
stu3.release();
stu3.Show();
return 0;
}
但上述写法的缺点是:资源计数器和类绑在一起了,如果还有一个新的类要使用引用计数器不方便。
最后,我们将该引用计数做一个简易的封装,也就是把引用计数作为一个新的类来使用:
例3:引用计数器封装
#include <iostream>
#include <cstring>
using namespace std;
struct RefValue
{
RefValue(const char* pszName);
~RefValue();
void AddRef();
void Release();
char* m_pszName;
int m_nCount;
};
RefValue::RefValue(const char* pszName)
{
m_pszName = new char[strlen(pszName)+1];
m_nCount = 1;
}
RefValue::~RefValue()
{
if (m_pszName != NULL)
{
delete m_pszName;
m_pszName = NULL;
}
}
void RefValue::AddRef()
{
m_nCount++;
}
void RefValue::Release()
{
if (--m_nCount == 0)
{
delete this;
}
}
class CStudent
{
public:
CStudent(const char* pszName);
CStudent(CStudent& obj);
~CStudent();
CStudent& operator=(CStudent& obj);
void release();
void Show()
{
if (m_pValue->m_nCount > 0)
{
cout << hex << (int&)m_pValue->m_pszName << m_pValue->m_nCount <<endl;
}
}
private:
RefValue* m_pValue;
};
CStudent::CStudent(const char* pszName)
{
m_pValue = new RefValue(pszName);
}
CStudent::CStudent(CStudent& obj)
{
m_pValue = obj.m_pValue;
m_pValue->AddRef();
}
CStudent::~CStudent()
{
release();
}
CStudent& CStudent::operator=(CStudent& obj)
{
if (obj.m_pValue == m_pValue)
{
return *this;
}
m_pValue->Release();
m_pValue = obj.m_pValue;
m_pValue->AddRef();
return *this;
}
void CStudent::release()
{
m_pValue->Release();
}
int main(int argc, char* argv[])
{
CStudent stu1("zhang san");
CStudent stu2("li si");
CStudent stu3 = stu2;
stu1.Show();
stu2.Show();
stu3.Show();
stu2.release();
//stu3.release();
stu3.Show();
stu3.release();
return 0;
}
上面的做法能在一定程度上解决资源多次重复申请的浪费,但是仍然存在两个核心的问题:
- 如果对其中某一个类对象中的资源进行了修改,那么所有引用该资源的对象全部会被修改,这显然是错误的。
- 当前的计数器作用于Student类,在使用时候,需要强行加上引用计数类,这样复用性不好,使用不方便。
三.写时拷贝
问题:如果共享资源中的值发生了变化,那么其他使用该共享资源的值如何保持不变?
解决思路:使用引用计数时,当发生共享资源值改变的时候,需要对其资源进行重新的拷贝,这样改变的时拷贝的值,而不影响原有的对象中的共享资源。
写时拷贝(COW copy on write),在所有需要改变值的地方,重新分配内存。
例4:
#include <iostream>
#include <cstring>
struct RefValue
{
RefValue(const char* pszName);
~RefValue();
void AddRef();
void Release();
char* m_pszName;
int m_nCount;
};
RefValue::RefValue(const char* pszName)
{
m_pszName = new char[256];
strcpy(m_pszName, pszName);
m_nCount = 1;
}
RefValue::~RefValue()
{
if (m_pszName != NULL)
{
delete m_pszName;
m_pszName = NULL;
}
}
void RefValue::AddRef()
{
m_nCount++;
}
void RefValue::Release()
{
if (--m_nCount == 0)
{
delete this;
}
}
class CStudent
{
public:
CStudent(const char* pszName);
CStudent(CStudent& obj);
void SetName(const char* pszName);
~CStudent();
CStudent& operator=(CStudent& obj);
void release();
void Show()
{
if (m_pValue->m_nCount > 0)
{
cout << hex << (int&)m_pValue->m_pszName << m_pValue->m_pszName <<endl;
}
}
private:
RefValue* m_pValue;
};
void CStudent::SetName(const char* pszName)
{
m_pValue->Release();
m_pValue = new RefValue(pszName);
}
CStudent::CStudent(const char* pszName)
{
m_pValue = new RefValue(pszName);
}
CStudent::CStudent(CStudent& obj)
{
m_pValue = obj.m_pValue;
m_pValue->AddRef();
}
CStudent::~CStudent()
{
release();
}
CStudent& CStudent::operator=(CStudent& obj)
{
if (obj.m_pValue == m_pValue)
{
return *this;
}
m_pValue->Release();
m_pValue = obj.m_pValue;
m_pValue->AddRef();
return *this;
}
void CStudent::release()
{
m_pValue->Release();
}
int main(int argc, char* argv[])
{
CStudent stu1("zhang san");
CStudent stu2("li si");
CStudent stu3 = stu2;
stu2.Show();
stu3.Show();
stu2.SetName("li si2");
stu2.Show();
stu3.Show();
return 0;
}
四.智能指针的原理
前面,我们学会了如何使用引用计数及写时拷贝,这是理解智能指针必不可少的方法。但是,在实际写代码中,我们其实更倾向于让程序员对于资源的管理没有任何的感知,也就是说,最好让程序员只需要考虑资源的何时申请,对于何时释放以及资源内部如何计数等问题,统统交给编译器内部自己处理。
智能指针另外一点就是在使用上要像真正的指针一样可以支持取内容, 指针访问成员->等操作,因此,就需要对这些运算符进行重载。
例5:
一.最原始
#include<iostream>
#include<cstring>
using namespace std;
//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放
class CStudent
{
public:
CStudent()
{
}
void test()
{
cout<<"CStudent"<<endl;
}
private:
char* m_pszBuf;
int m_nSex;
};
int main()
{
CStudent* pStu=new CStudent();
if(pStu!=nullptr)
{
delete pStu;
pStu=nullptr;
}
return 0;
}
二.//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制来解决资源自动释放的问题
#include<iostream>
#include<cstring>
using namespace std;
//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放
class CStudent
{
public:
CStudent()
{
}
void test()
{
cout<<"CStudent"<<endl;
}
private:
char* m_pszBuf;
int m_nSex;
};
//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制
//来解决资源自动释放的问题
//智能指针雏形,需要管理资源
class CSmartPtr
{
public:
//一定要是一个堆对象
CSmartPtr(CStudent* pObj)
{
m_pObj=pObj;
}
~CSmartPtr()
{
if(m_pObj!=nullptr)
{
delete m_pObj;
}
}
private:
CStudent* m_pObj;//将资源放入智能指针类中,管理起来
};
int main()
{
//这里可以完成资源的自动释放
CSmartPtr sp(new CStudent());
//但是,用起来不像是一个指针
return 0;
}
三.
#include<iostream>
#include<cstring>
using namespace std;
//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放
class CStudent
{
public:
CStudent()
{
}
void test()
{
cout<<"CStudent"<<endl;
}
private:
char* m_pszBuf;
int m_nSex;
};
//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制
//来解决资源自动释放的问题
//智能指针雏形,需要管理资源
class CSmartPtr
{
public:
//一定要是一个堆对象
CSmartPtr(CStudent* pObj)
{
m_pObj=pObj;
}
~CSmartPtr()
{
if(m_pObj!=nullptr)
{
delete m_pObj;
}
}
//需要想办法让其对象用起来像是一个指针
//像一个指针的写法
CStudent* operator->()
{
return m_pObj;
}
CStudent& operator*()
{
return *m_pObj;
}
//bool
operator bool()
{
return m_pObj!=nullptr;
}
private:
CStudent* m_pObj;//将资源放入智能指针类中,管理起来
};
int main()
{
//这里可以完成资源的自动释放
CSmartPtr sp(new CStudent());
//但是,用起来不像是一个指针
sp->test();
(*sp).test();
return 0;
}