C++ day36 智能指针模板类:让管理动态内存分配更容易


本文讲解三个智能指针模板类,他们的对象 类似于指针,即可以把new获得的地址赋给他们的对象;而智能指针过期时,他们的 析构函数就会自动释放内存,而无需程序员自己记住释放内存

  • auto_ptr,C++98提供,C++11已经摈弃,但是已经有很多代码用了它,所以还是要看得懂。并且,后面两个新增的类的行为,其实和它相同。
  • unique_ptr,C++11提供
  • shared_ptr,C++11提供

其实还有一个智能指针模板类(即一共有4种),weak_ptr,但是C++ primer这本书不讨论它,不知道为啥。

智能指针:类似指针的类对象

智能指针的行为类似于指针,但是本质是类的对象,所以他还有其他功能,不仅仅是充当指针的角色。

从对象指针和常规指针的对比引入智能指针

内存管理上最可怕的就是内存泄漏,智能指针就可以缓解这一问题。

前面咱们在讲异常的栈解退部分时,曾经说过异常可能使得内存泄漏,当时对比了string类对象用new分配内存和int用new分配内存,发现string类对象分配的内存一定不会受到异常的影响,一定可以调用到析构函数以释放new分配的内存,从而绝对不会内存泄漏;但是int等类型的指针却没有类的后台庇护,一旦遇到异常,函数异常终止,无法执行发生异常的代码后面的delete代码,于是仍然会造成内存泄漏。

把示例代码拿出来再看看:

string类的对象,就算异常导致程序提前终止,也不会内存泄漏

void remodel(std::string & str)
{
	std::string * ps = new std::string(str);//对象指针
	···
	if(weird_thing)
		throw exception();
	str = *ps;
	delete ps;//一旦发生异常,这句代码根本不被执行,但ps变量从栈内存中删除时,会调用string类的析构,于是ps指向的内存会被释放,并不会导致内存泄漏
	return;
}

不是类的数据类型的普通指针,没有后台,就会导致内存泄漏

void remodel(int & i)
{
	int * ps = new int;
	···
	if(weird_thing)
		throw exception();
	i = *ps;
	delete ps;//一旦发生异常,这句代码根本不被执行,导致内存泄漏
	return;
}

虽然当时讲栈解退提到了解决方法,但是还是不够好,现在智能指针提供了一个很好的解决办法。

智能指针模板类就是借鉴了string类对象使用析构函数帮自己释放内存这一点,智能指针背后的思想就是基于把常规指针包装为一个有析构函数后台的类对象

哪些行为类似于常规指针

  • 可以对智能指针对象解引用, *ps
  • 可以用它访问结构成员,ps->data
  • 可以把他赋给同类型的常规指针
  • 可以把它赋给同类型的智能指针对象

怎么用

首先包含头文件memory

这三个模板类的声明都在memory头文件中。

#include <memory>

智能指针模板也位于名称空间std中

再实例化所需类型的指针

用三个模板类的构造函数

template <class X> class auto_ptr{
public:
	explicit auto_ptr(X * p = 0) throw();
	//throw(),参数列表为空,表示该函数不会抛出任何异常
	//这是异常规范,C++11也将其摒弃了。
}

比如,用double和string类型去实例化,创建指向double和string的智能指针:

auto_ptr<double> pd(new double);//代替double * pd
auto_ptr<string> ps(new string);//代替string * ps

仔细挖掘的话,我们应该想到,上述两句代码中,圆括号里的new double返回的地址,是auto_ptr<double>类的构造函数的实际参数,即相当于是上面auto_ptr构造函数中的形参p的实参。

另外两种智能指针的实例化也一样:

unique_ptr<double> pdu(new double);
shared_ptr<double> pds(new double);
注意:不可以把常规指针隐式转换为智能指针,只可以显式转换
double * p_reg = new double;//常规指针
shared_ptr<double> pd;//智能指针
shared_ptr<double> pshared = p_reg;//不允许,隐式转换,复制构造函数
shared_ptr<double> pshared(p_reg);//允许

第二个允许是因为所有三个智能指针模板类都有一个explicit构造函数,这个构造函数的参数是常规指针,这个构造函数的用途是把常规指针显式转换为智能指针

重载的赋值运算符也一样,不接受隐式转换

pd = p_reg;//出错,不允许,因为这是隐式转换
pd = shared_ptr<double> (p_reg);//可以

简单示例1

把上面那个函数直接改了

#include <memory>
void remodel(std::string &str)
{
	std::auto_ptr<std::string> ps(new std::string(str));
	···
	if (weird_thing)
		throw exception();
	str = *ps;
	//delete ps;//不需要了,no longer needed
	return;
}

智能指针模板也位于名称空间std中

简单示例2

//hangman.cpp
#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

class Report{
private:
	string str;
public:
	Report(const string s):str(s)
		{cout << "Object created!\n";}
	~Report(){cout << "Object deleted!\n";}
	void comment() const {cout << str << '\n';}
};
int main()
{
	{
		std::auto_ptr<Report> ps(new Report("using auto_ptr"));
		ps->comment();//行为类似于指针
	}
	{
		std::shared_ptr<Report> ps(new Report("using shared_ptr"));
		ps->comment();//行为类似于指针
	}
	{
		std::unique_ptr<Report> ps(new Report("using unique_ptr"));
		ps->comment();//行为类似于指针
	}
	cout << "Bye!\n";
	return 0;
}

把三个类的对象的构造放在三个块,是为了专门看析构函数的执行。这里只是展示了析构会被调用,但是这里析构内部没写释放内存的代码。。。其实应该要写的

Object created!
using auto_ptr
Object deleted!
Object created!
using shared_ptr
Object deleted!
Object created!
using unique_ptr
Object deleted!
Bye!

注意:一定不要给智能指针模板类的构造函数传递非堆内存地址!!!

三种智能指针模板类都必须严格避免这一点,很容易犯错,比如把栈内存作为参数传进去,这就会导致delete作用于栈内存,,,,可是new和delete都只可以作用于堆内存。。。

string s("dont do this!");//s是自动变量
shared_ptr<string> ps(&s);//&s是栈内存

delete作用于栈内存有什么后果:它会无所作为,不释放栈内存,没啥可怕后果

delete作用于栈内存,具体会引起什么后果我不清楚,于是我试了一下。。。不怕搞坏电脑的好奇心

传入堆地址:

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
    string * s = new string("dont do this!");//s是堆地址
    {
        cout << *s << '\n';
        std::shared_ptr<string> ps(s);
    }

    cout << *s << '\n';
	return 0;
}

可以看到,块结束后,析构函数确实释放了s指向的内存,所以再打印s中内容(s这个变量还是在的,它是栈变量,即自动变量),就得到了乱码。
在这里插入图片描述

传入栈内存地址:

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
    string s("dont do this!");//s是自动变量
    {
        cout << s << '\n';
        std::shared_ptr<string> ps(&s);//&s是栈内存
    }

    cout << s << '\n';
	return 0;
}

程序还是打印出来了s的值,说明块结束时,调用析构函数并没有成功释放&s指向的内存,也就是说,delete只会释放堆内存,如果你传入栈地址,他就什么也不做,也不会破坏栈数据,也不会引发异常,就无所作为。但是我们也一定不要这么做,因为对我们来说,这一定会引起错误。

dont do this!
dont do this!
Process returned 0 (0x0)   execution time : 0.328 s
Press any key to continue.

auto_ptr比unique_ptr,shared_ptr差在哪里

为啥auto_ptr会被C++11嫌弃?

还记得学习delete时,有一个注意点是:不可以对一块内存delete两次甚至更多次。这就是auto_ptr被嫌弃的原因了。因为它允许你把一个智能指针直接赋给另一个同类型的智能指针,于是就有两个智能指针指向了同一块内存,那么他俩过期的时候,就都会调用自己的析构去释放这块内存,于是就会对一块内存delete两次!

比如:

auto_ptr<string> ps(new string("I want a job"));
auto_ptr<string> pss;
pss = ps;

ps,pss过期都会释放那块内存,于是出错。

而unique_ptr对这个现象采取了解决措施:它建立了所有权概念,对于一个特定对象,最多只能有一个智能指针可以拥有它,只有拥有它的智能指针才可以删除它的内存。赋值还是可以进行,但是一旦赋值,所有权就会被转让,被赋值那个对象成了新的拥有者。

shared_ptr的策略也大概差不多,但是又要更加严格一些。shared_ptr的智能程度更高,它会去跟踪每个对象的智能指针数目,即引用计数,reference counting,赋值会使得计数加1,而一个指针过期则会使得计数减1,只有最后一个指针过期,才调用delete离开释放内存

其实如果抛开智能指针的概念,单纯思考怎么避免一块内存被释放两次,就只有一个办法,那就是重载赋值运算符时,内部使用深复制,即创建的是一个副本,根本不是同一块地址。两个智能指针实际指向的是不同的对象
显然,这也不算什么好办法,还是unique_ptr,shared_ptr比较好。

示例:auto_ptr和unique_ptr的所有权模型会造成的问题,前者运行时崩溃,后者编译时就报错

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
	std::auto_ptr<string> films[5] =
	    {
		    std::auto_ptr<string> (new string("Fowl Balls")),
			std::auto_ptr<string> (new string("Duck Walks")),
			std::auto_ptr<string> (new string("Chicken Runs")),
			std::auto_ptr<string> (new string("Turky Brrors")),
			std::auto_ptr<string> (new string("Goose Eggs"))
		};
	std::auto_ptr<string> pwin;
	pwin = films[2];//films[2]失去了所有权
	cout << "The nominees for best baseball film are \n";
	for (int i=0;i<5;++i)
		cout << *films[i] << endl;
	cout << "The winner is " << *pwin << "!\n";
	cin.get();
	cin.get();

	return 0;
}

出现了这个熟悉的错误代码!!!!!!!!运行阶段程序崩溃

华为软挑我好几天困在这个错误里!!!

The nominees for best baseball film are
Fowl Balls
Duck Walks

Process returned -1073741819 (0xC0000005)   execution time : 0.466 s
Press any key to continue.

因为pwin = films[2];把这个string对象的所有权转让给了pwin,films[2]就不再是这个string对象的指针了,而是成了一个空指针!!!于是for循环就在试图对空指针解引用,所以出错。

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
	std::unique_ptr<string> films[5] =
	    {
		    std::unique_ptr<string> (new string("Fowl Balls")),
			std::unique_ptr<string> (new string("Duck Walks")),
			std::unique_ptr<string> (new string("Chicken Runs")),
			std::unique_ptr<string> (new string("Turky Brrors")),
			std::unique_ptr<string> (new string("Goose Eggs"))
		};
	std::unique_ptr<string> pwin;
	pwin = films[2];//films[2]失去了所有权
	cout << "The nominees for best baseball film are \n";
	for (int i=0;i<5;++i)
		cout << *films[i] << endl;
	cout << "The winner is " << *pwin << "!\n";
	cin.get();
	cin.get();

	return 0;
}

unique_ptr和auto_ptr一样,也是采用所有权模型,但是前者会在编译时就不允许赋值,这种赋值就会报错,而不会像后者那样等到运行时才崩溃
在这里插入图片描述
把auto_ptr换为shared_ptr,就对了。因为计数,确保只delete一次。

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
	std::shared_ptr<string> films[5] =
	    {
		    std::shared_ptr<string> (new string("Fowl Balls")),
			std::shared_ptr<string> (new string("Duck Walks")),
			std::shared_ptr<string> (new string("Chicken Runs")),
			std::shared_ptr<string> (new string("Turky Brrors")),
			std::shared_ptr<string> (new string("Goose Eggs"))
		};
	std::shared_ptr<string> pwin;
	pwin = films[2];//films[2]失去了所有权
	cout << "The nominees for best baseball film are \n";
	for (int i=0;i<5;++i)
		cout << *films[i] << endl;
	cout << "The winner is " << *pwin << "!\n";
	cin.get();
	cin.get();

	return 0;
}
The nominees for best baseball film are
Fowl Balls
Duck Walks
Chicken Runs
Turky Brrors
Goose Eggs
The winner is Chicken Runs!

unique_ptr为什么比auto_ptr优秀

刚才的示例看到,这俩货都是采用所有权的方式,只不过unique_ptr要严格一些,它直接不允许你把一个智能指针赋给另一个同类型的智能指针。编译阶段出错比运行时出错更安全

在这里插入图片描述

但是其实这么说(unique_ptr不允许把一个智能指针赋给另一个同类型的智能指针)是不对的,unique_ptr是允许同类型指针之间赋值的,但有个重要前提,那就是源指针是一个临时右值或者使用移动构造函数move(),因为这两种情况可以保证赋值后他们就会失效,这个对象仍然只有一个指针。

源指针是一个临时右值

只要源指针不是临时右值,编译器就不允许。

这种临时右值一般是函数返回的值,普通函数和构造函数都可以。

unique_ptr<string> demo(const char * s)
{
	unique_ptr<string> temp(new string(s));
	return temp;
}
unique_ptr<string> ps;
ps = demo("Uniquely spaecial");//demo函数返回一个临时的unique_ptr指针,然后通过赋值,它的string对象的所有权就转让给了ps,而被返回的那个指针丧失了所有权,但是完全没有任何危险,因为他是一个临时右值,我们没机会再次使用它去访问数据,所以这种赋值就根本不需要编译阶段报错

示例

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;
std::unique_ptr<string> demo(const char * s)
{
	std::unique_ptr<string> temp(new string(s));
	return temp;
}
int main()
{
    std::unique_ptr<string> ps;
    ps = demo("Uniquely special");//编译器不会报错了!
    cout << *ps << endl;
	return 0;
}
Uniquely special

还比如

unique_ptr<string> p1(new string("Hi you!"));
unique_ptr<string> p2;
p2 = p1;//编译出错,因为p1不是临时右值
p2 = unique_ptr<string> (new string("Yo!"));允许,因为unique_ptr<string>的构造函数创建的临时指针把所有权转让给p2以后就无法再被访问到了

可以看到,这两种类型的赋值实际上都是源指针为临时右值的情况,这时候unique_ptr会允许赋值,且不会出现问题,是安全的。

正是考虑到unique_ptr比auto_ptr更安全,所以容器对象可以使用unique_ptr,但是禁止使用auto_ptr

如果unique_ptr指针是右值,则可以赋值给shared_ptr

这个条件和俩unique_ptr指针赋值一样。

这是因为shared_ptr类有一个explicit显式构造函数,专门用于把右值unique_ptr转为shared_ptr。让shared_ptr接管源指针(unique_ptr)指向的对象。

unique_ptr<int> pi(make_int(rand() % 1000));
shared_ptr<int> ps(pi);//报错,因为pi是lvalue, 左值
shared_ptr<int> pss(make_int(rand() % 1000));//可以,因为括号里是一个rvalue,右值
如果实在要给同类型两个unique_ptr指针赋值,源指针又不是临时右值,则使用move()函数,可以给源指针重新赋值

但是注意,move()方法只是让这个赋值可以实现,可以通过编译,但是源指针仍然会把所有权转让给目标指针,所以源指针会成为空指针,还是不可以在后续代码中解引用源指针,如果解引用,就会出现内存错误。

为了保证后面对源指针解引用不出错,可以重新给它赋值。

#include <iostream>
#include <string>
#include <memory>
using std::string;
using std::cout;
using std::cin;
using std::endl;

int main()
{
	std::unique_ptr<string> films[5] =
	    {
		    std::unique_ptr<string> (new string("Fowl Balls")),
			std::unique_ptr<string> (new string("Duck Walks")),
			std::unique_ptr<string> (new string("Chicken Runs")),
			std::unique_ptr<string> (new string("Turky Brrors")),
			std::unique_ptr<string> (new string("Goose Eggs"))
		};
	std::unique_ptr<string> pwin;
	//pwin = films[2];//编译错误
	pwin = move(films[2]);//用move方法替代赋值运算符,即使源指针不是临时右值,也可以通过编译
	films[2] = std::unique_ptr<string> (new string("HULA huLA"));//为了保证后面对源指针films[2]解引用不出错,重新给它赋值
	cout << "The nominees for best baseball film are \n";
	for (int i=0;i<5;++i)
		cout << *films[i] << endl;
	cout << "The winner is " << *pwin << "!\n";
	cin.get();
	cin.get();

	return 0;
}
The nominees for best baseball film are
Fowl Balls
Duck Walks
HULA huLA
Turky Brrors
Goose Eggs
The winner is Chicken Runs!
unique_ptr怎么区别安全还是不安全的赋值呢:C++11新增的移动构造函数和右值引用

它的秘诀是:C++11新增的移动构造函数和右值引用。

unique_ptr有一个可用于数组的变体

auto_ptr就没有,它只可以使用new,不能用new[],由于new必须和delete配对,所以他也只能使用delete。

而unique_ptr可以用new和delete,还可以使用new[]和delete[]。

std::unique_ptr<double []> p(new double(5));//析构函数用delete[]

只有unique_ptr提供了数组变体,auto_ptr和shared_ptr都没有提供。

  • 使用new分配内存,则可以使用auto_ptr, shared_ptr
  • 使用new[]而不是new分配内存, 则不可以使用auto_ptr, shared_ptr
  • 既不用new也不用new[]分配内存,则不可以使用unique_ptr

怎么选择使用哪一种智能指针

这里说选择只是在unique_ptr和shared_ptr里面选,没auto_ptr什么事儿。

如果要多个指针指向同一个对象,用shared_ptr

  • STL容器的指针,因为很多STL算法都支持赋值和复制操作(就会导致多个指针指向相同对象),所以只可以用shared_ptr,而绝对不能用auto_ptr(运行时崩溃)和unique_ptr(编译出错)
  • 用辅助指针标识最大值和最小值,有可能两个辅助指针指向同一个元素。
  • 两个对象都包含指向第三个对象的指针。

如果编译器没提供shared_ptr,就可以使用boost库提供的shared_ptr。
如果编译器没提供unique_ptr,就可以使用boost库提供的scoped_ptr,是类似的。

如果不需要多个指针指向同一对象,那就用unique_ptr

示例

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <algorithm> //for_each函数
using std::string;
using std::cout;
using std::cin;
using std::endl;
std::unique_ptr<int> make_int(int n)
{
	return std::unique_ptr<int> (new int(n));//返回int的智能指针,用传入的参数n初始化
}
void show(std::unique_ptr<int>  & a)//这里必须按引用传递,因为如果按值传递,那么就要用一个非临时右值作为a的实参,对于unique_ptr类是不允许的
{
	cout << *a << ' ';
}
int main()
{
	int size = 5;
	std::vector<std::unique_ptr<int>> vp(size);
	for (int i=0;i<size;++i)
		vp[i] = make_int(rand() % 1000);
	vp.push_back(make_int(rand() % 1000));//可以调用成功是因为参数是一个临时右值
	for_each(vp.begin(), vp.end(), show);
	return 0;
}
41 467 334 500 169 724

总结

学下来发现,原来unique_ptr和shared_ptr的名字是很恰当的,很符合他们的特征。

  • unique_ptr严格限制了两个同类型指针的赋值,只有在源指针是临时右值时才允许,他强调只能有一个所有权拥有者,就算是赋值成功(源指针为临时右值),那源指针也会立刻丧失所有权,成为空指针,不再指向那片数据,可以对源指针重新赋值。
  • shared_ptr利用计数器,它允许多个指针指向同一片内存,同一个数据,所以是共享的,分享的,它根据计数器的值判断是否delete,如果计数器的值不是1,则对象过期就过期,并不释放内存。

对于智能指针的选择上,我个人觉得:

  • auto_ptr远差于unique_ptr, 毕竟C++11都给摈弃了,而且还会导致程序崩溃,真的害怕。。
  • 而unique_ptr虽然安全,但是限制好多,需要很小心判断是不是满足源指针为右值的条件,有的时候不好判断,比如上面show方法只能按引用传参,一旦按值传参就会不满足右值条件。。。很难判断,需要多积累经验。
  • 但是shared_ptr就很安全又很简单,不需要担心赋值条件,也不需要担心多次delete同一片内存导致程序崩溃。

auto_ptr << unique_ptr < shared_ptr

我现在觉得华为软挑就是多次delete同一内存导致的了…

  • 使用这些智能指针保存new返回的地址,则之后不需要自己用delete删除内存了,对象过期就会自动删除内存,再也不用担心内存泄漏,还不担心delete一片内存两次。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值