当你在读这篇文章的时候,应该都有这样一个疑问?那就是为什么要使用智能指针。
我们先看这样一个示例:
#include
#include
#include
using std::string;
void memory_demo1() {
string* str = new string("今天又找了一天的bug,太累了!!!");
std::cout << *str << std::endl;
//并没有调用delete str,进行释放
return;
}
int memory_demo2() {
string* str = new string("这个世界到处是坑啊,异常处理都要坑我!!!");
/*
* 程序这里省略执行了一段复杂的逻辑并会执行到throw这个函数
*/
{
throw exception("文件不存在");
}
std::cout << *str << std::endl;
delete str; //虽然此处调用了,但执行到throw时就结束了,并不会执行到这一布
return 0;
}
int main(){
memory_demo1();
try {
memory_demo2();
}
catch (exception e) {
std::cout<<"catch exception: "<<e.what()<<endl;
}
system("pause");
return 0;
}
这段程序如果乍一看是不是没有什么问题,也可以正常编译和运行。但这只是少量代码,体现不出来问题在哪。如果一涉及到工程级代码,那就很容易造成内存泄漏问题(真的是程序员最头疼的问题之一)。
对于上面问题,最简单的一个办法就是把string定义为auto变量。那在这个函数周期结束时就会自动的进行释放(局部变量)。
void memory_demo1() {
//string* str = new string("今天又找了一天的bug,太累了!!!")
string str("今天又找了一天的bug,太累了!!!");
std::cout << str << std::endl;
return;
}
int memory_demo2() {
//string* str = new string("这个世界到处是坑啊,异常处理都要坑我!!!");
string str("这个世界到处是坑啊,异常处理都要坑我!!!");
...
return 0;
}
现在我们定义的是一个string(局部)对象,那在函数结束的时候,string这个函数内部会自己调用析构函数就行释放。
看到这,那是不是可以这样做:如果我们分配的动态内存都交给有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个非常好的方案?
或许正是由于这样的想法,智能指针一词被那些C++的大牛们所实践出来。
智能指针也是通过这个原理来解决指针智能释放问题。
直接点说就是资源分配即初始化RAII(Resource Acquisition Is Initialization):
定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
实现机制:利用类的构造和析构函数(释放资源)是由编译器自动调用的。
(1)C++98提供了auto_ptr的模板
(2)C++11 增加了unique_ptr、shared_ptr 和weak_ptr三种智能指针
其中:shared_ptr是引用计数的智能指针,被奉为裸指针的完美替身,因此被广泛使用。也可能正是这个原因,scope_ptr 和 weak_ptr似乎被大家遗忘了。
1.auto_ptr
管理权转移
带有缺陷的设计 ----->c++98
在任何情况下都不要使用;
2.scoped_ptr(boost)
unique_ptr(c++11)
防拷贝—>简单粗暴设计—>功能不全。
3、shared_ptr(boost/c++11)
引用计数—>功能强大(支持拷贝、支持定制删除器)
缺陷---->循环引用(使用weak_ptr配合解决)。
下面来介绍这篇文章的主角:auto_ptr,剩下的三个会在后面的篇章中进行一一介绍。
看到这,也许有人会问,既然上面都表明了任何情况下都不要使用,那为什么还要介绍,还要放到C++标准库里面呢?个人感觉或许是因为C++标准库里面的内容一经规定后就不允许修改了吧。这里就不对这个问题就行纠结了,对于auto_ptr,这里权当学习了解一下就好了。
auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
auto_ptr<类型> 变量名(new 类型)
注意:
(1)std::auto_ptr 属于 STL,也在namespace std中,使用的时候要加上#include 头文件。
(2)由于源代码中构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显式调用构造函数。
auto_ptr<string> pstr(new string("abcd")); // success
auto_ptr<string> pstr = new string("abcd"); // error
例如:
auto_ptr str(new string(“今天又找了一天的bug,太累了!!!”));
//对比刚开始的示例使用string* str = new string(“今天又找了一天的bug,太累了!!!”);会造成内存泄漏问题。
讲解示例1.1如下:
#include
#include
#include
#include
using std::string;
class Test
{
public:
Test() { cout << "test is construct" << endl; num = 1; }
~Test() { cout << "test is destruct" << endl; }
int getNum() { return num; }
private:
int num;
};
//用 法: auto_ptr<类型> 变量名(new 类型)
void memory_demo1() {
//Test *test = new Test; //代码1
auto_ptr<Test> test(new Test());//代码2 //创建对象
cout<<test->getNum()<<endl; //代码3
//(*test).getNum(); //代码4
//Test *temp = test.get(); //代码5
//temp->getNum();
//代码6
//Test *tmp1 = test.release(); //取消智能指针对动态内存的托管,之前分配的内存必须手动释放
//tmp1->getNum();
//delete tmp1;
//代码7
//test.reset(); //括号内可以传递一个参数,默认参数是一个空指针
//test.reset(new Test());
return;
}
int memory_demo2()
{
Test *t = new Test;
{
throw exception("文件不存在");
}
delete t;
return 0;
}
int main()
{
memory_demo1();
ty{
memory_demo2();
}catch (exception e) {
std::cout << "catch exception: " << e.what() << endl;
}
system("pause");
return 0;
}
(1)如果将代码1放出来,代码2注释(没有使用智能指针的情况下),运行结果如下:
结果显示只执行了构造函数,并没有自动执行析构函数
(2)如果将代码1注释,而放出来代码2,使用代码2替代代码1,结果如下:
结果显示既执行了构造,又自动执行了析构函数
这是怎么一回事呢,使用vs2015(其他的也可以),代码2处auto_ptr按F12进入源码。如下:
是实话这里的很多代码我自己也看不懂,但并不影响我们了解实现原理。看红色箭头的地方,此处是构造函数的地方。注意_Myptr(_Ptr),这里应该还挺好理解的,将我们外面的auto_ptr test(new Test());这个代码定义的对象,赋值到它内部。当看到源码实现最底下这里时:
是不是突然间感觉容易理解了很多。这不就是将它的类对象替换成前面我们在外面定义的对象吗?当它内部执行完成后,就会调用析构函数释放,这样不就能实现对象的自动释放了吗。
不知道在看auto_ptr源代码的时候有没有注意到里面这几个类方法
(3)当上面示例1.1在调用代码3的时候,其实auto_ptr源码里面重构了->,返回了一个get()方法,而内部get()的实现则返回了内部类对象,这样当外部示例1.1调用test->getNum()的时候,其他变相的还是使用传进去的test对象再调用这个函数。至于示例1.1中的代码4实现原理也是一样的。
注:以后可以使用对象调用.get()来判断智能指针是否为空,是否创建成功。
在示例1.1代码2之后可以调用test.get()来判断是否生成成功了。
(4)既然(2)中说了get()返回的是内部类对象,那示例1.1中调用代码5肯定是可以行得通的。将代码5放出来后运行发现也是可以正常运行的。但这样做的意义并不大,不建议去使用。
(5)当示例1.1代码中放出来代码6,调用release时,通过源码可以看到:
原理就是定义一个此类型的临时指针变量,指向之前保存的指针的地址,然后将它(之前)的属性置为空指针,并将临时的返回了。但是它并没有帮我们去释放之前的地址。所以在代码6调用之后,我还加上了一个delete tmp1操作。即调用release时就相当于取消了智能指针对动态内存的托管,而且之前分配的内存必须手动释放。(个人感觉有点多此一举,不推荐)
realease实现的功能就是将原智能指针的控制权取消。
(6)当调用reset时(释放代码7).通过源码,可以看到还可以传递一个参数。当不传参数或者传递NULL时默认参数是一个空指针。此函数功能就是重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉。也就是说当传递一个空值时,会将原来的析构掉,将空值赋值给原来的值。通俗点就是:无参时智能指针会释放当前管理的内存取消对原对象的控制权。如果传递一个对象,则智能指针会释放当前原对象,来管理新传入的对象。
当调用代码7中test.reset(new Test())时,此时新对象会析构并替换掉原来的对象。(此时结果会调用两次构造和析构)
说到这里,对于auto_ptr的个人认识也差不多结束了。虽然auto_ptr叫智能智能,如果说它智能的话,那它真智能吗。在我看来无非就是利用对象析构的时候要调用析构函数,然后再利用析构函数帮我们做这个事情(释放内存)。
哦。对了。这里做一下用法的总结:(虽然没有什么用,因为它几乎被禁用了,而且shared_ptr也完美的替代了它。这里就当做一个知识的了解吧)
(1)尽可能不要将auto_ptr 变量定义为全局变量或指针
当设定为全局变量的时候,比如全局定义这样的一个对象:auto_ptr t(new Test()),那t这个对象只有在程序执行结束的时候才会被释放,就达不到我们使用它的目的和初衷。使用智能指针就没有意义了
(2)除非自己知道后果,否则不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
auto_ptr< Test> t(new Test());
auto_ptr< Test> t1;
t1 = t;
(3)不要定义指向智能指针对象的指针变量
auto_ptr<Test>* tp = new auto_ptr<Test>(new Test());
当这样使用的时候,运行结束时,C++根本不会去析构tp这个对象。因为后面new auto_ptr(new Test())也是动态内存分配的。那它也就成为了一个普通对象,也就自然不会去自动再去析构它。那它跟文章最开始写的new string也就没有区别了。
(4)想使用 std::auto_ptr 的限制感觉还真多,还不能用来管理堆内存数组,就算使用了还怕哪天一个不小心,就导致问题了。或许由于 std::auto_ptr 引发了诸多问题,而且一些设计并不是非常符合 C++ 编程思想。所以C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代
话说这点才是最重要的,说了这么多是不是等于白说了。话虽如此,但看到这里说明前面知识点已经看完了,就当学习知识了,对自己又没有坏处。
这里忘记说明了一个重要的知识点,或许这才是auto_ptr被unique_ptr取代的主要原因:
为了更直观的说明,以示例代码2.1讲解:
#include
#include
#include
#include
#include
using std::string;
using std::vector;
using std::auto_ptr;
int main()
{
//弊端1:复制或赋值都会改变资源的所有权
auto_ptr<string> p1(new string("I 'm string1."));
auto_ptr<string> p2(new string("I 'm string2."));
printf("p1: %p\n", p1.get());
printf("p2: %p\n", p2.get());
p1 = p2; //代码1
printf("after p1 = p2\n");
printf("p1: %p\n", p1.get());
printf("p2: %p\n", p2.get());
//弊端2
vector<auto_ptr<string>> va;
auto_ptr<string> p3(new string("I 'm p3."));
auto_ptr<string> p4(new string("I 'm p4."));
va.push_back(p3); //代码2
va.push_back(p4); //代码3
//va.push_back(std::move(p3)); //代码4 //右值化后才能转换赋值
//va.push_back(std::move(p4)); //代码5
std::cout << "va[0]: " << *va[0] << std::endl;
std::cout << "va[1]: " << *va[1] << std::endl;
//弊端3
{
auto_ptr<string> p5;
string* str = new string("智能指针的内存管理陷阱");
p5.reset(str); //代码9
{
auto_ptr<string> p6;
p6.reset(str); //代码10
}
std::cout <<"str: " << *p5 << std::endl;//代码11
}
system("pause");
return 0;
}
运行结果如下:
这里可以清晰的看到在执行完代码1:p1 = p2后,p1的地址变为p2了,而p2的地址为空了。而这个时候如果再去使用p2进行操作时肯定就会报错的,至于报什么类型的错应该都能猜到。
(1)总结弊端1:当进行 p1= p2,复制或赋值都会改变资源的所有权,这估计是auto_ptr在C++11之后被抛弃的主要原因之一。
当将弊端1和3的代码全部注释,运行出来弊端2的代码。
在示例2.1上,代码2跟代码3按平时的写法貌似没有问题,但编译的时候却会报错。原因在于不支持左值操作,只有进行右值化后才能转换赋值。也就是上面会有代码4跟代码5的原因,它们是为了替代代码2和3而写上的。当使用代码4和5时,运行结果如下:
很正常的数据。还记得刚刚提到的弊端1吗?当添加上如下代码时,再去编译运行查看一下结果:
va[0] = va[1]; //代码6
std::cout << "va[0]: " << *va[0] << std::endl; //代码7
std::cout << "va[1]: " << *va[1] << std::endl; //代码8
再去源代码764查看:
归根结底,还是在于执行代码6后,再去访问代码8时,它已经是一个空值了,它的地址被赋值到了va[0]了,而空值是不允许被访问使用的。
(2)总结弊端2:在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable),STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权,而弊端1中明确的说到了赋值和复制都会改变资源的所有权。所以一般情况下尽量不要在STL容器中使用auto_ptr。
当将弊端1和2的代码注释,运行弊端3下面的代码时,看起来并没有错误才是,但运行的结果却是失败的。如下
那造成这样的原因是什么呢?我们注意一下弊端3下面的代码9,代码10,代码11。当执行完代码9时,相当于初始化p5,将str这个对象返回给p5这个对象,相当于替换吧。(reset内部实现就是将原来的对象delete,并将新传入的对象交给原来的对象)。但当执行完代码10是,就是将str这个对象又交给了p6,由p6去管理。再执行代码11之前,由于加了括号是局部对象,当出了括号p6就会自动执行析构,即同时会delete掉str这个对象。所以当执行代码11时,由于p5内部的地址被替换为了str(它已经被p6的析构释放掉了),所以会报错。
(3)弊端总结3:auto_ptr不能共享所有权,不能把同一段内存交给多个auto_ptr 变量去管理。
(4)auto_ptr不能指向数组,不支持对象数组的内存管理,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]。
auto_ptr<int[]> ai(new int[5]);
当添加这种类似的代码也是会报错的。不能这样用。
所以,C++11用更严谨的unique_ptr 取代了auto_ptr!
好了,对于auto_ptr的用法就到这里了,其他的我自己也不知道了。
C++之智能指针unique_ptr
C++之智能指针shared_ptr
C++之智能指针weak_ptr