1. 为什么需要智能指针?
在C++中,动态内存的管理是通过一对运算符来完成的:new 在动态内存中为对象分配空间并返回一个指向该对象的指针;delete 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出问题,比如new了一个变量,忘记delete,就会产生内存泄漏问题;有2个指针指向同一块内存,第一个指针成功delete与之关联的内存,那剩下那么指针再次delelte同一块内存,就会产生错误。
为了更容易地使用动态内存,C++11标准库提供了两种智能指针类型来管理动态对象。shared_ptr允许多个指针指向同一个对象,也称共享指针;unique_ptr则独占所指向的指针,也称独占指针。标准库还定义了一个名为wek_ptr的伴随类,它是一种弱定义,指向shared_ptr所管理的对象,它是为了解决共享指针循环引用的问题,该问题不是此文的重点。
2. shared_ptr指针的使用
智能指针也是模板,当创建智能指针时,必须提供指向的类型。
语法 | 功能 |
---|---|
shared_ptr p | 可以指向T类型的智能指针 |
p | 将将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
3.shared_ptr指针的引用计数
当进行拷贝和赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。在每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值吗,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁,计数器就会递减。当shared_ptr的计数器变为0,它就会自动释放自己管理的对象。
到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
4. 为什么不能用static计数?
static int count;
有些同学可能想要在智能指针类中添加一个static成员变量,每新增一个智能指针类,count就加1 。在只有一个对象的时候,该方法是合适的。但是static变量是属于类的,如下图所示,当出现第二个对象,此时新增一个shared_ptr指针,此时内部的count就变成了4,既不满足指向对象1的指针数量,也不满足指向对象2的指针数量。因此我们需要指向对象1的shared_ptr共享一个内部计数器,指向对象2的shared_ptr共享另外一个内部计数器,实现途径见下一节。
5. 实现简易版内部技术器
5.1 自定义类声明
定义一个myclass类,里面包含一个num成员变量和一个getNum函数。
class myclass {
private:
int num;
public:
myclass(int n) :num(n) { cout << "成功构成一个myclass类" << endl; }
int getNum() const{
return num;
}
~myclass(){ cout << "成功析构一个myclass类" << endl; }
};
5.2 辅助类
定义一个辅助类,负责实现计数功能,里面的变量和函数均为私有类型,不能被其他类所访问。为了让智能指针类可以访问该类,要声明SmartPtr类为友元类。这个类含有一个计数器count和一个指向myclass的指针。
类可以允许其他类或函数访问它的非公有成员,方法是其他类或者函数成为它的友元,前面加上关键字friend。如果一个类指定了友元类,则友元类的成员函数可以访问此类包含非公有成员在内的所有成员。
class countClass {
private:
friend class SmartPtr;
countClass(myclass* ptr) :p(ptr), count(1) { cout << "成功构成一个countClass类,此时count等于1" << endl; }
~countClass() {
cout << "进入countClass析构函数" << endl;
delete p;
cout << "成功释放p" << endl;
}
int count;
myclass* p;
};
5.3 智能指针类
智能指针类SmartPtr只在初始化构造时new一个countClass类负责计数,此时count等于1 。当调用拷贝构造函数时,相当于又多了一个智能指针指向该对象,因此计数器加1 。 使用拷贝运算符时,等于将右边指向的对象拷贝给左边,因此左边的计数器得减1,因为它不指向原先的对象了,右边对应的技术器得加1,因为新增了一个智能指针指向该对象。
在析构函数中,要先判断计数器是否为0,只有为0,才去释放内存。
class SmartPtr {
private:
countClass* countPtr;
public:
//初始化构造函数
SmartPtr(myclass* ptr): countPtr(new countClass(ptr)){ cout << "成功构成一个SmartPtr类" << endl; };
//拷贝构造函数
SmartPtr(const SmartPtr& sp) :countPtr(sp.countPtr) { ++countPtr->count; cout << "调用拷贝构造函数,计数器加1" << endl;
}
//拷贝赋值运算符
SmartPtr& operator=(const SmartPtr& rhs) {
++rhs.countPtr->count;
cout << "调用拷贝运算符,右边ptr计数器加1,左边ptr计数器减1" << endl;
if (--countPtr->count == 0) {
cout << "左边ptr计数器为0,释放countPtr" << endl;
delete countPtr;
}
countPtr = rhs.countPtr;
return *this;
}
//析构函数
~SmartPtr() {
if (--countPtr->count == 0) {
delete countPtr;
cout << "此时计数为0,成功释放countPtr" << endl;
} else
cout << "计数器减1,还有" << countPtr->count << "个指针指向对象,不能释放" << endl;
}
};
6. 测试
myclass* mp = new myclass(10);
{
SmartPtr ptr1(mp);
cout << mp->getNum() << endl;
}
cout << mp->getNum() << endl;
分析一下输出,在构造智能指针类之前必须先构造一个辅助类作为计数器,所以可以看到countClass类是比SmartPtr类更早构造出来的。因为ptr1处于一个局部函数体,所以当函数退出大括号时,该ptr1会去检查count是否等于0,是的话就去析构mp。等出了大括号,再去访问mp的变量,可以发现已经是随机值,代表mp已经被成功稀释了。
myclass* mp = new myclass(10);
{
cout << "进入第一个括号" << endl;
SmartPtr ptr1(mp);
{
cout << "进入第二个括号" << endl;
SmartPtr ptr2(ptr1);
cout << "退出第二个括号" << endl;
}
cout << "退出第一个括号" << endl;
}
可以看到进入第二个括号后,调用了拷贝构造函数,计数器加1,此时就算退出第二个括号,因此还有一个指针指向对象,所以对象并不会释放。
、
《C++ Primer》
C++ 引用计数技术及智能指针的简单实现