C++之智能指针auto_ptr

当你在读这篇文章的时候,应该都有这样一个疑问?那就是为什么要使用智能指针。
我们先看这样一个示例:

#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):
定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

实现机制:利用类的构造和析构函数(释放资源)是由编译器自动调用的。

在C++标准库中主要有下面4个智能指针:

(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,剩下的三个会在后面的篇章中进行一一介绍。

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注释(没有使用智能指针的情况下),运行结果如下:C++之智能指针auto_ptr_第1张图片
结果显示只执行了构造函数,并没有自动执行析构函数

(2)如果将代码1注释,而放出来代码2,使用代码2替代代码1,结果如下:
C++之智能指针auto_ptr_第2张图片
结果显示既执行了构造,又自动执行了析构函数

这是怎么一回事呢,使用vs2015(其他的也可以),代码2处auto_ptr按F12进入源码。如下:
C++之智能指针auto_ptr_第3张图片
是实话这里的很多代码我自己也看不懂,但并不影响我们了解实现原理。看红色箭头的地方,此处是构造函数的地方。注意_Myptr(_Ptr),这里应该还挺好理解的,将我们外面的auto_ptr test(new Test());这个代码定义的对象,赋值到它内部。当看到源码实现最底下这里时:
C++之智能指针auto_ptr_第4张图片
是不是突然间感觉容易理解了很多。这不就是将它的类对象替换成前面我们在外面定义的对象吗?当它内部执行完成后,就会调用析构函数释放,这样不就能实现对象的自动释放了吗。

不知道在看auto_ptr源代码的时候有没有注意到里面这几个类方法
C++之智能指针auto_ptr_第5张图片
(3)当上面示例1.1在调用代码3的时候,其实auto_ptr源码里面重构了->,返回了一个get()方法,而内部get()的实现则返回了内部类对象,这样当外部示例1.1调用test->getNum()的时候,其他变相的还是使用传进去的test对象再调用这个函数。至于示例1.1中的代码4实现原理也是一样的。
注:以后可以使用对象调用.get()来判断智能指针是否为空,是否创建成功。
在示例1.1代码2之后可以调用test.get()来判断是否生成成功了。

(4)C++之智能指针auto_ptr_第6张图片既然(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())时,此时新对象会析构并替换掉原来的对象。(此时结果会调用两次构造和析构)
C++之智能指针auto_ptr_第7张图片

说到这里,对于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的补充

这里忘记说明了一个重要的知识点,或许这才是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;
}

运行结果如下:
C++之智能指针auto_ptr_第8张图片
这里可以清晰的看到在执行完代码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

C++之智能指针auto_ptr_第9张图片
再去源代码764查看:
C++之智能指针auto_ptr_第10张图片
归根结底,还是在于执行代码6后,再去访问代码8时,它已经是一个空值了,它的地址被赋值到了va[0]了,而空值是不允许被访问使用的。
(2)总结弊端2:在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable),STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权,而弊端1中明确的说到了赋值和复制都会改变资源的所有权。所以一般情况下尽量不要在STL容器中使用auto_ptr。

当将弊端1和2的代码注释,运行弊端3下面的代码时,看起来并没有错误才是,但运行的结果却是失败的。如下
C++之智能指针auto_ptr_第11张图片

那造成这样的原因是什么呢?我们注意一下弊端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++11的三种智能指针介绍,觉得写的不错的可以继续阅读:

C++之智能指针unique_ptr
C++之智能指针shared_ptr
C++之智能指针weak_ptr

你可能感兴趣的:(《C/C++成长笔记》,《c++高级架构成长笔记》,c++)