【前言】
智能指针和移动语义是迄今为止,最难理解的两个概念。网上查的资料也都解释的要么太高深,要么泛泛为止,反复阅读了好几遍才恍然大悟,领会之后发现其实也没有那么难。
智能指针为什么和移动语义有关系
智能指针的概念出现其实是非常合理,毕竟有的时候会用new
开辟空间,如果这块地址不及时释放,就会造成内存泄漏程序崩溃,所以如果这个指针可以自己在该释放的时候释放,不用人为管理那么多指针,岂不美哉,这就是智能指针。
那么移动语义是什么?它和智能指针是啥关系?
这个问题的关键就在于,指针可能被用于赋值,传递等,一个智能指针A
它把值赋值给另一个指针B
,然后A
的生命结束了并且释放了自己管理的内存区域,那么B
指针的地址岂不是指向了未知区域?
所以智能指针之间的赋值不是一个简单的直接=
赋值完事儿,比如A
赋值完,那么B
其实也指向那块内存,两个指针都指向内存,并且都会“自动”释放,这就是很危险的事情,所以在A
赋值给B
后,A
就不应该在拥有对那块内存的管理权,这就像是A
把内存移动给B
一样,这就是移动语义。下面详细介绍。
什么是智能指针
先看一段代码
void someFunction()
{
Resource *ptr = new Resource(); // Resource is a struct or class
// do stuff with ptr here
delete ptr;
}
发出灵魂只问:如果在delete ptr
这行之前,函数提前返回了,这是不是就内存泄漏了?
那么有什么东西可以实现自动消失呢?局部变量?但是局部变量只能消失自己,没有办法消失第三者比如指针管理的内存。还有什么?对了,就是类。
关于类的最好的事情之一是它们包含析构函数,这些析构函数在类的对象超出范围时自动执行。因此,如果您在构造函数中分配(或获取)内存,则可以在析构函数中对其进行分配,并确保在销毁该类对象时,该内存将被重新分配(无论其是否超出范围,是否被明确删除,等等…)。
这就是智能指针设计的依据、理论基石。智能指针是一个类,也就是说我们把原先new
分配的内存变成一个类的变量,并且把释放内存写到析构函数里,这样就不用自己手动释放内存了,是不是觉得很美妙。当然有封装好的这样的智能指针类,但是为了进一步了解智能指针的工作原理,先自己写一个智能指针,一步步完善它,这样更能理解它为什么这么设计。
按照上面的思路,可以实现下面一个版本的智能指针:
#include <iostream>
template <class T>
class Auto_ptr{
T* m_ptr;
public:
Auto_ptr(T* ptr = nullptr):m_ptr(ptr){}
~Auto_ptr(){delete m_ptr;}
T& operator*() const{return *m_ptr;}
T* operator->() const {return m_ptr;}
};
class Resource{
public:
Resource() {std::cout << "Resource acquired\n";}
~Resource(){std::cout<< Resource destoryed";}
void sayHi() {std::cout << "Hi!\n";}
};
//使用示范
void sample(){
Auto_ptr1<Resource> ptr(new Resource()); //创建智能指针
if (true) return;
}
调用sample()
函数后将会打印:
Resource acquired
Resource destroyed
可以发现,上面简单的一个类,已经实现了智能指针的基本功能!
那么接下来其实很自然的需要完善它,上面的功能有什么缺陷呢?
移动语义
- 如果指针传递的话怎么办?两个智能指针都拥有内存的所有权,一个释放了,另外一个在释放时候怎么办?
指针传递有三种方式可选,按值传递、按地址传递、和引用传递。具体可以复习这篇内容:指针值传递、地址传递和引用传递
逐个进行分析,
- 按值传递的话如上面所说,第二个智能指针将变成悬空指针;
- 按引用传递的话,和按值传递一样,可能会变成悬挂引用;
- 按地址传递,首先我们还得记得手动释放这个地址,这样智能指针就不智能了,其次就是地址里面对应的内容可能已经被提前释放了。
所以针对智能指针的复制问题,跟经典的变量复制问题不一样,他又一个更贴切的表达:**移动,将指针的所有权从源目标转移到目标对象。**这就是为什么移动语义和智能指针一起出现。
移动语义意味着类将转移对象的所有权,而不是进行复制。比如智能指针A将值赋值给智能指针B时,A同时失去对指针管理内容的所有权,也就是A已经指向空了,所以A释放的时候对之前管理的内存毫无影响。就不会存在悬空指针的问题了。
根据上面的设计思想可以知道,需要重写智能指针类的复制构造函数。
#include <iostream>
template <class T>
class Auto_ptr{
T* m_ptr;
public:
Auto_ptr(T* ptr = nullptr):m_ptr(ptr){}
~Auto_ptr(){delete m_ptr;}
//-----------------New Add -----------------
Auto_ptr(Auto_ptr& a){
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
}
Auto_ptr& operator=(Auto_ptr& a){
if(&a == this) return *this;
delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
//-----------------New Add -----------------
T& operator*() const{return *m_ptr;}
T* operator->() const {return m_ptr;}
};
class Resource{
public:
Resource() {std::cout << "Resource acquired\n";}
~Resource(){std::cout<< Resource destoryed";}
void sayHi() {std::cout << "Hi!\n";}
};
int main()
{
Auto_ptr<Resource> res1(new Resource());
Auto_ptr<Resource> res2; // Start as nullptr
std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");
res2 = res1; // res2 assumes ownership, res1 is set to null
std::cout << "Ownership transferred\n";
std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");
return 0;
}
要留意的是上面的复制函数中,形参用的是引用,因为里面要更改实参,如果不使用引用,则无法修改。
本篇基本介绍了一下智能指针的为什么、是什么和怎么样,和移动语义的为什么、是什么。下面会分几篇详细介绍。