智能指针的由来
早期的c++
中是没有任何内存回收机制的,只能通过程序员手动的在适当的地方
写delete
语句回收,对于早期的语言来说本身也是合理的。但是难就难在适当的地方
太难把控,导致c++
程序后期出现各种内存回收问题。
内存回收的目的就是为了程序中不使用哪一块堆内存时,系统可以及时的回收,如果不使用智能指针,单单使用c++11
以前的语法也是可以实现的,依靠的就是栈对象在离开作用域时会自动回收空间,如果是类或者结构体的实例还会调用析构函数,那我们是不是可以将堆内存的对象封装在栈对象中,运用栈内存自动回收空间的性质回收堆内存。
template <typename T>
class SmartPtr {
public:
SmartPtr() {}
SmartPtr(T *ptr) : _ptr(ptr) {}
SmartPtr(const SmartPtr &smart_ptr) { _ptr = smart_ptr._ptr; }
SmartPtr &operator=(const SmartPtr &form) { _ptr = form._ptr; }
T *operator->() { return _ptr; }
void print() { cout << *_ptr << endl; }
~SmartPtr() { delete _ptr; }
private:
T *_ptr;
};
如上代码所示实现了一个非常基本的智能指针的例子,虽然很简陋,但是可以在局部变量作用域结束时释放空间,这个是没有什么卵用,但就是在这个简单的思想上衍生中各种智能指针。
智能指针的类型
类代码
class shape {
public:
shape() = default;
shape(const string &name) : _name(name) {}
virtual void print() { cout << _name << endl; }
virtual double perimeter() { return 0; };
virtual double area() { return 0; };
~shape() {}
private:
string _name;
};
class cricle : public shape {
public:
cricle() = default;
cricle(const string &name, const float &radius) : shape(name), _r(radius) {}
double perimeter() override { return _r * 2 * PI; }
double area() override { return _r * PI * PI; }
~cricle() {}
private:
float _r;
};
auto_ptr
auto_ptr
很多问题,目前在c++17
中已经被废弃了,即使是c++17
之前的版本也不推荐使用,主要的问题是auto_ptr
会出让所有权,导致出让所有权之前的智能指针存在但是使用时会导致程序段错误。
auto_ptr<cricle> sa(new cricle("圆形", 10));
auto_ptr<cricle> sa1 = sa; //成功
sa1->print(); //打印 圆形
cout << sa1->area() << endl; //打印 98.596
sa->print(); //段错误
cout << sa->area() << endl;
执行结果
圆形
98.596
[1] 52190 segmentation fault
我们可以看到,拷贝构造成立的情况下,sa指向指针的所有权会被出让给sa1,导致原来的智能指针悬空,导致段错误。
auto_ptr
可以看做不允许多引用的指针,同一时刻只能有一个auto_ptr
封装并使用,但是赋值语义和拷贝构造函数应该是不可以被调用,但是通过我们的结果可以看到,并不是这样。这依然违背了c++的编程思想,并且极易出错。
auto_ptr
可以通过move
进行所有权转移,move
之后语义明确。
unique_ptr
由于auto_ptr
的种种问题,我们应当使用unique_ptr
来替代auto_ptr
,它们两个唯一的区别是unique_ptr
不支持拷贝构造和赋值的语义,只支持move
,目的依然是同一时刻只能有一个智能指针指向一个堆对象地址。但是比auto_ptr
语义更加明确。
unique_ptr<cricle> sa(new cricle("圆形", 10));
sa->print();
cout << sa->area() << endl;
auto sa1 = sa;
执行结果
smart_ptr.cpp:51:8: error: call to implicitly-deleted copy constructor of 'std::__1::unique_ptr<cricle, std::__1::default_delete<cricle> >'
auto sa1 = sa;
使用move语义
unique_ptr<cricle> sa(new cricle("圆形", 10));
sa->print();
cout << sa->area() << endl;
auto sa1 = move(sa);
sa1->print();
cout << sa1->area() << endl;
scoped_ptr
scoped_ptr
并非标准库的中指针类型,而是boost库中的。放在这里讲述是为了和以上两个指针做对比分析。scoped_ptr
和以上两个智能指针最大的不同是不允许任何形式的出让所有权
,上面两种智能指针可以通过复值或者move的方式出让所有权,导致他们的声明周期是根据当前有没有智能指针引用来决定的,而scoped_ptr
不允许任何形式的所有权出让,这就意味着智能指针只能在当前作用域内起作用,离开作用域后就会自动回收分配的堆空间。
boost::scoped_ptr<cricle> sa(new cricle("圆形", 10));
sa->print();
cout << sa->area() << endl;
auto sa1 = move(sa);/auto sa1 = sa;
执行结果
smart_ptr.cpp:52:14: error: calling a private constructor of class 'boost::scoped_ptr<cricle>'
我们可以看到无论是拷贝构造还是move
都会提示编译错误,说明scoped_ptr
只能在一个作用域中起作用。
这里我们看到这里提示的是构造函数是静态函数,不难猜出boost是如何实现构造函数、复制运算符、移动是如何被禁止调用的。如果不显示声明这些函数,编译器会自动声明出默认的函数,所以想要禁止构造函数,只需要将这些函数显示声明成private
,但是不用定义。在编译过程中就会显示调用失败了。如下
class A {
private:
A(const A &a); //拷贝构造函数
A(const A &&a); //移动构造函数
A& operator=(const A &A); //复制函数
};
shared_ptr
上面三个指针有一个共同的特点,就是同一时刻只能有一个指针有效,但是在开发中需要大量的多指针指向同一空间的情况,以上三种指针都不能满足这样的情况,所以说在开发过程中用到的最多的智能指针其实是shared_ptr
,它与以上三种指针最大的不同是可以使用多个指针共享。但是这里要解决一个以上三种智能指针不需要关注的问题,就是一个共享指针调用析构函数时,如何不影响其他的引用者。
stl采用了引用计数的方法,原理很简单,就是每多出一个新的共享指针就将引用计数值加1,而析构一个共享指针,就将这个值减1,如果析构时引用计数为1,则释放内存空间。
shared_ptr<cricle> sa(new cricle("圆形", 10));
cricle *c = sa.get();
auto sa1 = sa;
cout << c->area() << endl;
sa1.reset();
cout << c->area() << endl;
sa.reset();
cout << c->area() << endl;
执行结果
98.596
98.596
[1] 53532 segmentation fault
从以上现象可以看出,共享指针释放后,并不会影响其他的引用和指向内存中内容。只有所有引用都不再指向内存,这块内存才会释放,再次调用该空间,就会引起段错误。
weak_ptr
我认为weak_ptr
并不算常规意义上的智能指针,它应是shared_str
的附属指针,功能是检测一个shared_str
指针的引用数量,并且不占用引用计数,就是即使将shared_str
赋值给weak_ptr
,也不会增加引用计数值,但是不同的是weak_ptr
不能访问指向内存的任何方法或者获得成员值。
shared_ptr<cricle> sa(new cricle("圆形", 10));
cout << sa.use_count() << endl;
shared_ptr<cricle> sa1 = sa;
cout << sa.use_count() << endl;
weak_ptr<cricle> wp = sa1;
cout << sa.use_count() << endl;
执行结果
1
2
2
当使用weak_ptr
调用方法时报如下错误
smart_ptr.cpp:57:6: error: no member named 'print' in 'std::__1::weak_ptr<cricle>'
wp.print();
weak_ptr
也可以调用lock
函数获得一个shared_ptr
,可以临时使用,也可以存储到另一个变量里,这样就可以像shared_ptr
使用了,只是引用计数会加1。
总结:分为两类总结
只允许单个智能指针引用
1.auto_ptr
可以出让所有权给其他auto_ptr
,但是同时支持拷贝构造和赋值运算符,语义不明确。不推荐使用
2.unique_ptr
可以出让所有权给其他auto_ptr
,不支持拷贝构造和赋值运算符,但是支持move语义,当只需要一个引用时使用。
3.scoped_str
不能出让所有权,不支持拷贝、赋值运算符以及move语义,当只需要在局部使用并为了保证不被其他智能指针引用时使用。
允许多个智能指针引用
4.shared_str
共享的智能指针,采用引用计数的方式,只有最后一个引用的变量析构时才会回收堆空间。适合在需要多个地方同时使用时使用。
5.weak_str
可以看做shared_str
的附属指针,可以用来检测shared_str
是否有引用计数。不可以访问指针指向的对象。