Smart Pointer
内存回收
C++ 的垃圾回收:仍然可以作为学术论文的主题,说明没解决
主要问题:1. 没有单根结构 2. 类和对象的联系很弱
OCaml:学术语言
智能指针的出现整合了以下内容:
- 模板
- 继承、虚函数
- 引用计数
引用计数
定义
引用计数的定义:一个对象被几个引用共享,这些引用的个数。
- 需要指针来修改引用计数
- 一般称为
UCObject
(Use-Counted Object),其智能指针称为UCPointer
。- 智能指针由另一个类定义
- 使用模板编程
- 必须重载
operator->
和operator*
运行时引用计数
以自定义的 String
类为例。
String s1("abcdef");
String s2 = s1;// shadow copy
s1 = "Hello World";// copy on write
在创建对象 s1
时,字符串空间需要一个空间来保存计数,表示该字符串被多少个指针引用。初始值为 $0$。
拷贝构造时不复制字符串本身,而是对共享的引用计数自增。
指针指向新的对象时,旧对象的引用计数要自减,新对象的引用计数自增。
如果一个对象的引用计数归零,这个对象就可以被销毁了(不可能再被寻址)。
代码实现
一个智能指针的实现,需要四个类:
UCObject
:维护引用计数String Rep
:is-aUCObject
,维护字符串内容,是可共享的UCPointer
:has-aString Rep
,是UCObject
的智能指针,类模板编程String
:has-aUCObject
,维护 public 接口
UCObject
只维护引用计数,所以构造时总是赋 $0$。
在引用计数归零时销毁自己。
#include <assert.h>
class UCObject{
public:
UCObject();
virtual ~UCObject();
UCObject(const UCObject& rhs);
void incr();
void decr();
int reference();
private:
int m_refcount;
};
UCObject::UCObject(): m_refcount(0){}
UCObject::UCObject(const UCObject& rhs): m_refcount(0){}
virtual UCObject::~UCObject(){
assert(m_refcount == 0);
}
void UCObject::incr(){
m_refcount++;
}
void UCObject::decr(){
m_refcount--;
if(!m_refcount){
delete this;// legal, but don't use it afterwards
}
}
int UCObject::reference(){
return m_refcount;
}
UCPointer
维护引用计数的操作(自增自减)。
template<typename T>
class UCPointer{
public:
UCPointer(T* ref = nullptr): m_ptr(ref){this->increment();}
~UCPointer(){this->decrement();}
UCPointer(const UCPointer<T>& rhs);
UCPointer<T>& operator =(const UCPointer<T>& rhs);
T* operator ->()const;
T& operator *()const;
private:
T* m_ptr;
void increment(){if(m_ptr) m_ptr->incr();}
void decrement(){if(m_ptr) m_ptr->decr();}
};
template<typename T>
UCPointer<T>::UCPointer(const UCPointer<T>& rhs){// a new reference
m_ptr = rhs.m_ptr;
this->increment();
}
template<typename T>
UCPointer<T>& UCPointer<T>::operator =(const UCPointer<T>& rhs){
if(m_ptr != rhs.m_ptr){
this->decrement();
rhs.increment();
m_ptr = rhs.m_ptr;
}
return *this;
}
template<typename T>
T& UCPointer<T>::operator *()const{
return *m_ptr;
}
template<typename T>
T* UCPointer<T>::operator ->()const{
return m_ptr;
}
opearter->()
是一元运算符,其结果应该也支持 ->
运算。
Ellipse elly(200F, 300F);
UCPointer<Shape> ptr(&elly);
p->render();// Ellipse::render(this = &elly)
其他一些 C++ 可以重载的运算符:
[]
:下标寻址()
:调用函数->()
:指针调用*()
:一元的指针解引用
String Rep
继承 UCObject
,维护字符串内容的空间。
class StringRep: public UCObject{
public:
StringRep(const char* str);
~StringRep();
StringRep(const StringRep& rhs);
int length()const{return strlen(m_str);}
int equal(const StringRep& s)const;
operator const char*()const;
private:
char *m_str;
void operator =(const StringRep& rhs){}// forbidden assignment operator
};
StringRep::StringRep(const char* str){
if(str != nullptr){
int len = strlen(str) + 1;
m_str = new char[len];
strcpy(m_str, str);
}
else{
m_str = new char[1];
*m_str = '\0';
}
}
StringRep::~StringRep(){
delete[] m_str;
}
StringRep::StringRep(const StringRep& rhs){
int len = rhs.length();
m_str = new char[len + 1];
strcpy(m_str, rhs.m_str);
}
int StringRep::equal(const StringRep& s)const{
return (strcmp(m_str, s.m_str) == 0);
}
Stringrep::operator const char*()const{
return m_str;
}
通过将一个运算符重载为 private
,可以达到在外部禁用此运算符的效果。
String
类可以直接利用智能指针的特性管理内存,不需要自己管理字符串空间。
class String{
public:
String(const char* str);
~String();
String(const String& rhs);
String& operator =(const String& rhs);
int operator ==(const String& s)const;
String operator +(const String& s)const;
int length()const;
operator const char*()const;
private:
UCPointer<StringRep> m_rep;
};
String::String(const char* str): m_rep(nullptr){
m_rep = new StringRep(str);
}
String::~String(){}
String::String(const String& s): m_rep(s.m_rep){}
String& String::operator =(const String& s){
m_rep = s.m_rep;
return *this;
}
int String::operator ==(cons String& s)const{
return m_rep->equal(*s.m_rep);
}
int String::length()const{
return m_rep->length();
}
小结
UCPointer
维护了引用计数UCObject
包装了引用计数修改的内容,使得String
类不需要再维护,非常简洁StringRep
只负责字符串内容的维护,而不关心其他内容UCObject
和UCPointer
可以重新利用- 但如果出现
UCPointer
的循环指向,则环上的内容都会不能释放(引用计数不能归零)
- 但如果出现
拓展:STL 中的 auto_ptr
语法
在 <memory>
库中,模板:
template<typename _Tp>
class auto_ptr{
public:
typedef _Tp element_type;
// constructors & destructors
explicit auto_ptr(element_type* __p = 0) throw(): _M_ptr(__p){}
auto_ptr(auto_ptr& __a) throw(): _M_ptr(__a.release()){}
template<typename _Tp1>
auto_ptr(auto_ptr<_Tp1>& __a)throw(): _M_ptr(__a.release()){}
auto_ptr(auto_ptr_ref<element_type> __ref) throw(): _M_ptr(__ref._M_ptr){}
~auto_ptr(){delete _M_ptr;}
// assignment operations
auto_ptr& operator=(auto_ptr& __a) throw();
template<typename _Tp1>
auto_ptr& operator=(auto_ptr<_Tp1>& __a) throw();
auto_ptr& operator=(auto_ptr_ref<element_type> __ref) throw();
// dereference operation
element_type& operator*() const throw();
element_type* operator->() const throw();
element_type* get() const throw();// get the pointer
element_type* release() throw();// discard the pointer and return the content
void reset(element_type* __p = 0) throw();// reset the pointer
template<typename _Tp1>
operator auto_ptr_ref<_Tp1>() throw(){return auto_ptr_ref<_Tp1>(this->release());}
template<typename _Tp1>
operator auto_ptr<_Tp1>() throw(){return auto_ptr<_Tp1>(this->release());}
private:
_Tp* _M_ptr;
} _GLIBCXX11_DEPRECATED_SUGGEST("std::unique_ptr");// suggest use std::unique_pt
测试代码
#include <iostream>
#include <memory>
using namespace std;
class Test{
public:
int id;
string info;
Test(int pid = 0): id(pid), info(){
cout << "construct id:" << id << endl;
}
~Test(){
cout << "destruct id:" << id << endl;
}
void print(){
cout << "print infomation:" << info << endl;
}
private:
};
signed main(int argc, char **argv){
auto_ptr<Test> ptr(new Test(131));
if(ptr.get() != nullptr){
ptr->print();
ptr.get()->info = "Hello World!";
ptr->print();
(*ptr).print();
ptr->print();
}
return 0;
}
运行结果:
construct id:131
print infomation:
print infomation:Hello World!
print infomation:Hello World!
print infomation:Hello World!
destruct id:131
可以看到 new Test(131)
的对象最后被释放了。
弊端
内存泄漏风险
auto_ptr
的 release()
和 reset()
实现:
element_type* release() throw(){
element_type* __tmp = _M_ptr;
_M_ptr = 0;
return __tmp;
}
void reset(element_type* __p = 0) throw(){
if (__p != _M_ptr){
delete _M_ptr;
_M_ptr = __p;
}
}
在我们想要主动销毁一个智能指针时,如果仅调用 release()
而不使用别的指针接受返回值,就会产生内存泄漏。
而 reset()
的参数在不阅读源码的情况下不易理解。
所有权的转移
auto_ptr
赋值运算符和拷贝构造的实现:
template<typename _Tp1>
auto_ptr& operator=(auto_ptr<_Tp1>& __a) throw(){
reset(__a.release());
return *this;
}
auto_ptr(auto_ptr& __a) throw(): _M_ptr(__a.release()){}
如果一个 auto_ptr
被赋给了另一个,系统上认为指向的对象的所有权转移了,原指针变为空,有时候这并不是我们想要的。
同样地,如果在函数参数中未使用引用传递,那么进入函数后调用拷贝构造,原来的智能指针就变为空了。
如果要使用 STL 的智能指针,建议学习:std::unique_ptr - cppreference.com,std::shared_ptr - cppreference.com