【C++】智能指针

目录

前言

一、智能指针引入

一、auto_ptr、

二、unique_ptr

三、shared_ptr

四、weak_ptr


前言

内存管理是C++中的一个常见的错误和bug来源。在大部分情形中,这些bug来自动态分配内存和指针的使用:当多次释放动态分配的内存时,可能会导致内存损坏或者致命的运行时错误;当忘记释放动态分配的内存时,会导致内存泄露。所以,我们需要智能指针来帮助我们管理动态分配的内存。其来源于一个事实:栈比堆要安全的多,因为栈上的变量离开作用域后,会自动销毁并清理。智能指针结合了栈上变量的安全性和堆上变量的灵活性。


一、智能指针引入

void someFunction()
{
    Resource* ptr = new Resource; // Resource是一个类或者结构

    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        return;  // 函数终止,无法释放ptr
    if (x < 0)
        throw;   // 出现异常,函数终止,无法释放ptr
    // 使用ptr处理
    // ...
    delete ptr;
}

上述代码:申请了一份动态内存,可能由于过早的return语句以及异常的抛出,ptr将得不到正确释放,从而出现内存泄露。可见普通指针并没有一个内在机制来自动管理与释放。

【思考】那如何可以避免动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

【思路】你可能想到了类:类内部存储指针,然后在析构函数中销毁该指针。类可以实现资源的自动管理。其好处是,只要类局部变量(分配在栈上)超出其作用域(不论其是如何离开的),其析构函数一定会被执行,那么管理的内存也将一定得到销毁。

【解决方案】C++引入了智能指针概念。使用智能指针可以很大程度上的避免申请的空间在函数结束时忘记释放,造成内存泄漏。因为智能指针是行为类似于指针的类对象,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

C++里面的提供了四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用。

shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。weak_ptr是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。


一、auto_ptr、

接下来看一下auto_ptr和普通指针有什么区别:

auto_ptr智能指针的使用语法:(获得一个X类型的auto_ptr)

(new double是new返回的指针,指向新分配的内存块。它是构造函数auto_ptr<double>的参数,即对应于原型中形参p的实参)

auto_ptr是C++98的解决方案,C++11已经抛弃,采用所有权模式

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.")); 
auto_ptr<string> p2; 
p2 = p1; //auto_ptr不会报错.

【思考】上述赋值语句完成什么工作?

如果p1和p2是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是p1结束,一次是p2结束。要避免这种问题,方法有多种:

  1. 定义赋值运算符,使之进行深拷贝。这样两个指针将指向不同的对象,其中一个对象是另一个对象的副本。
  2. 建立所有权概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数才会删除该对象。然后,让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但是unique_ptr的策略更严格。
  3. 创建智能更高的指针,跟踪引用特定对象的智能指针。这称为引用计数。例如:赋值时,计数加一,而指针过期时,计数减一。仅当最后一个指针过期时,才调用delete。这是share_ptr采用的策略。

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题


二、unique_ptr

用来替换auto_ptr,同样采用所有权模式unique_ptr实现独占式拥有严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露特别有用。

常见操作:
 unique_ptr<T> u1 空unique_ptr可以指向类型为T的对象,u1会使用delete来释放它的指针
unique_ptr<T,D>u2 u2会使用一个类型为D的可调用对象来释放它的指针
 unique_ptr<T,D>u(d) 空unique_ptr指向类型为T的对象,用类型为D的对象d来代替delete释放它的指针
u.release() u放弃对指针的控制权,返回指针,并将u置为空
 u.reset() 释放u指向的对象

【思考】为什么unique_ptr优于auto_ptr?

由上一个实例可知p2接管string对象的所有权后,p1的所有权将被剥夺,这样可以防止p1和p2的析构函数试图删除同一个对象;但如果程序随后试图使用p1,这就会出现错误,因为p1不再指向有效的数据。

接下来看使用unique_ptr的情况:

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!   //#6

此时编译器p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段错误比潜在的程序崩溃更安全)。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world")); 
unique_ptr<string> pu2; 
pu2 = pu1;                                      // #1 不允许
unique_ptr<string> pu3; 
pu3 = unique_ptr<string>(new string ("You"));   // #2 允许

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

【注意】如果确实想执行类似与#1的操作,要安全的使用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后 还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;  //输出结果hello alexia

额外:boost库的boost::scoped_ptr也是一个独占性智能指针,但是它不允许转移所有权,从始而终都只对一个资源负责,它更安全谨慎,但是应用的范围也更狭窄。) 

【优点2】unique_ptr有一个可用于数组的变体,auto_ptr使用delete与new一起使用,不能与new[]一起使用,而unique_ptr有使用new[]和delete[]的版本。

std::unique_ptr<double [] >p (new double[5]);//  will use delete[];

【注意】使用new分配内存时,才能使用auto_ptr和share_ptr,使用new[]分配内存时,不能使用它们。不适用new分配内存时,不能使用auto_ptr或share_ptr;不使用new[]或new,不能使用unique.


三、shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, nique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

  • use_count 返回引用计数的个数
  • unique 返回是否是独占所有权( use_count 为 1)
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.
shared_ptr<int> sp(new int(1)); //sp 与 sp.get()是等价的。

share_ptr的实例:

int main()
{
	string *s1 = new string("s1");

	shared_ptr<string> ps1(s1);
	shared_ptr<string> ps2;
	ps2 = ps1;

	cout << ps1.use_count()<<endl;	//2
	cout<<ps2.use_count()<<endl;	//2
	cout << ps1.unique()<<endl;	//0

	string *s3 = new string("s3");
	shared_ptr<string> ps3(s3);

	cout << (ps1.get()) << endl;	//033AEB48
	cout << ps3.get() << endl;	//033B2C50
	swap(ps1, ps3);	//交换所拥有的对象
	cout << (ps1.get())<<endl;	//033B2C50
	cout << ps3.get() << endl;	//033AEB48

	cout << ps1.use_count()<<endl;	//1
	cout << ps2.use_count() << endl;	//2
	ps2 = ps1;
	cout << ps1.use_count()<<endl;	//2
	cout << ps2.use_count() << endl;	//2
	ps1.reset();	//放弃ps1的拥有权,引用计数的减少
	cout << ps1.use_count()<<endl;	//0
	cout << ps2.use_count()<<endl;	//1
}

【问题】shared_ptrunique_ptr的主要区别

前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。还有一个区别是shared_ptr不能用于管理C语言风格的动态数组。

shared_ptrunique_ptr内部实现机理有区别,前者内部使用两个指针,一个指针用于管理实际的指针,另外一个指针指向一个”控制块“,其中记录了哪些对象共同管理同一个指针。这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。share_ptr使用复制构造函数还有赋值运算时,“控制块”会同步更新的,这样就达到了引用计数的目的。

shared_ptr底层实现原理 

一个 shared_ptr 对象的内存开销要比裸指针和 unique_ptr 对象略大。

 std::unique_ptr<int> p1(new int(10));
 std::cout << sizeof(p1) << std::endl;//8

 std::shared_ptr<int> p2(new int(10));
 std::cout << sizeof(p2) << std::endl;//16

shared_ptr 需要维护的信息有两部分:

  1. 指向共享资源(对象)的指针。
  2. 指向控制信息的指针,例如:引用计数等共享资源的控制信息。

所以,shared_ptr 对象需要保存两个指针。

 

参考链接:现代 C++:一文读懂智能指针 - 知乎 (zhihu.com)

shared_ptr缺陷——循环引用问题 


四、weak_ptr

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象,进行该对象的内存管理的是那个强引用的shared_ptrweak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr

强引用和弱引用的区别
强引用是指当被引用的对象仍然活着,这个引用也存在(即只有至少有一个强引用,这个对象就不会也不能被释放) shared_ptr是一个强引用
弱引用当引用的对象活着的时候不一定存在,仅仅当它自身存在时的一个引用。

弱引用并不修改对象的引用计数,这意味着弱引用并不会对对象的内存进行管理
在功能上类似于普通指针,然而最大的区别在于弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存

class B;	//声明
class A
{
public:
	shared_ptr<B> pb_;
	~A()
	{
		cout << "A delete\n";
	}
};

class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout << "B delete\n";
	}
};

void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;	//2
	cout << pa.use_count() << endl;	//2
}

int main()
{
	fun();
	return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,运行结果如下:

class B;	//声明
class A
{
public:
	weak_ptr<B> pb_;
	~A()
	{
		cout << "A delete\n";
	}
};

class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout << "B delete\n";
	}
};

void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//2
}

int main()
{
	fun();
	return 0;
}

输出结果:

1
1
1
2
B delete
A delete

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。

【注意】我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:

shared_ptr<B> p = pa->pb_.lock();
p->print();

weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象.

【注意】weak_ptr 在使用前需要检查合法性.

  • expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
  • lock 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
  • use_count 返回与 shared_ptr 共享的对象的引用计数.
  • reset 将 weak_ptr 置空.
  • weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.
#include <iostream>
#include <memory>

std::weak_ptr<int> gw;

void observe()
{
    std::cout << "gw.use_count() == " << gw.use_count() << "; ";
    // we have to make a copy of shared pointer before usage:
    if (std::shared_ptr<int> spt = gw.lock()) {
        std::cout << "*spt == " << *spt << '\n';
    }
    else {
        std::cout << "gw is expired\n";
    }
}

int main()
{
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;

        observe();
    }

    observe();
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值