要正确的理解智能指针,首先必须理解引用计数的技术。
C++新特性15_引用计数
1. 深拷贝、浅拷贝的概念
前面介绍深拷贝与浅拷贝,请参考:C++57个入门知识点_24_ 浅拷贝与深拷贝(浅拷贝:对象1、2均指向一块内存; 深拷贝:重新分配内存,将对象1内容复制给对象2;何时重写拷贝构造函数:对象成员非基本数据类型,为资源时需重写或禁用)
深拷贝:当对象里面的资源,进行复制时需要写拷贝构造函数,拷贝构造函数需要对内容重新进行拷贝
浅拷贝:只是值传递
1.1 深拷贝优缺点:
- 优点:每一个的对象(哪怕是通过拷贝构造函数实例化的对象)的指针都有指向的内存空间,而不是共享,所以在对象析构的时候就不存在重复释放或内存泄露的问题了。
- 缺点:内存开销大
1.2 浅拷贝优缺点:
- 优点:通过拷贝构造函数实例化的对象的指针数据变量指向的共享的内存空间,因此内存开销较小。
- 缺点:对象析构的时候就可能会重复释放或造成内存泄露。
鉴于深拷贝和浅拷贝的优缺点,可采用引用计数技术,既减小了内存开销,又避免了堆的重复释放或内存泄露问题。
1.3 举例说明
- 在深拷贝的情况下,通过拷贝构造或者赋值构造的对象均各自包含自己的指针指向“Hello”。
- 浅拷贝
2. 举例查看浅拷贝的问题
以下通过实例查看浅拷贝会存在的问题
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
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);
}
//main中采用stu1 = stu2;这种形式时才会调用=运算符重载,这里使用是为防止此种形式的拷贝
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::CStudent(CStudent& obj)
//此种写法参考C++57个入门知识点_20_ 构造函数的调用中构造函数只有一个参数的隐式转换表达的方式
//CStudent stu3是一个没有构造的对象,首先需要调用构造函数,通过上面隐式转换的形式,调用只有一个参数为CStudent&的构造函数
CStudent stu3 = stu2;
stu1.Show();
stu2.Show();
stu3.Show();
stu2.release();
stu3.Show();
return 0;
}
设置断点查看之后可以看到stu2,stu3浅拷贝,指向同一个内存,在释放时可能会存在重复释放的问题
。
浅拷贝的析构
但是这样同样存在问题,一旦其中一个对象释放了资源,那么所有的其他对象的资源也被释放了。
补充:
上面=运算符重载被调用代码如下:
CStudent stu3("li si");
stu3 = stu2;
因为stu3已经被构造,已经是一个对象,stu3 = stu2
时不会再调用构造函数,而是调用=的重载类型
3. 浅拷贝重复释放资源解决方法:增加一个变量,记录资源使用的次数
打个比方:教室里5名同学,空调为5名同学共用,用一个计数器,当1个同学走了就计数1,当所有同学走了之后才关闭空调。
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
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;
//资源计数器,当资源计数器减为0时,表示该资源可以被释放,从而避免重复被释放的问题
int * m_pCount; //资源与计数器绑定,必须用指针写
};
CStudent::CStudent(const char* pszName)
{
m_pszName = new char[256];
m_pCount = new int;
strcpy(m_pszName, pszName);
*m_pCount = 1;
}
CStudent::CStudent(CStudent& obj)
{
//首先浅拷贝
m_pszName = obj.m_pszName;
//让使用该资源的人均使用同一个计数器
m_pCount = obj.m_pCount;
(*m_pCount)++;
}
CStudent::~CStudent()
{
release();
}
//main中采用stu1 = stu2;这种形式时才会调用=运算符重载,这里使用是为防止此种形式的拷贝
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::CStudent(CStudent& obj),拷贝构造,参考C++57个入门知识点_23_ 拷贝构造函数(利用一个对象创建另一个对象,调用的构造函数即拷贝构造函数)
CStudent stu3 = stu2;
stu1.Show();
stu2.Show();
stu3.Show();
stu2.release();
stu3.release();
stu3.Show();
return 0;
}
运行结果:
4. 引用计数的类封装
最后,我们将该引用计数做一个简易的封装,也就是把引用计数作为一个新的类来使用:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
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;
}
5. 引用计数存在的问题
- 上面的做法能在一定程度上解决资源多次重复申请的浪费,但是仍然存在两个核心的问题。
- 如果对其中某一个类对象中的资源进行了修改,那么所有引用该资源的对象全部会被修改,这显然是错误的。
- 当前的计数器作用于Student类,在使用时候,需要强行加上引用计数类,这样复用性不好,使用不方便。
后面需要对上面两个问题进行修正