C++ 智能指针


概述

   为什么要使用智能指针
   动态内存管理newdelete)经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

   智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
   下文中内容大多参考自《C++ Primer Plus》第六版。

   C++里面的四个智能指针:
   <1>. 需包含头文件#include <memory>
   <2>. C++ 标准模板库 STL(Standard Template Library) 一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,其中 auto_ptr 是 C++98 提出的,C++11 已将其摒弃,并提出了 unique_ptr 替代 auto_ptr。虽然 auto_ptr 已被摒弃,但在实际项目中仍可使用,但建议使用更加安全的 unique_ptr。
    <3>. shared_ptrweak_ptr 是 C+11 从准标准库 Boost 中引入的两种智能指针。
    <4>. auto_ptr, unique_ptr,shared_ptr三种智能指针模板都可以将new获得的地址赋给指针对象,当智能指针过期时,其析构函数将使用delete释放对应内存。


1、auto_ptr

1.1 auto_ptr

   auto_ptr是C++98提供的解决方案,C++11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。

auto_ptr的类模板原型为:

templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
};
  • 重点 由于其所有的智能指针类都有一个explicit构造函数,以指针作为参数。因此不能自动将指针转换为智能指针对象,必须显式调用:
// 来源 c++ primer plus
shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)

shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion) 隐式转换
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion) 显式转换
  • 对全部三种智能指针都应避免的一点:
  • 重点 下述代码中,pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No! 

1.2 有关智能指针的注意事项

  • 先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

   上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次: 一次是ps过期时,另一次是vocation过期时。
   要避免这种问题,方法有多种:

  • a. 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • b. 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptrunique_ptr 的策略,但unique_ptr的策略更严格。
  • c. 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略

举例说明:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
    auto_ptr<string> films[5] = {
        auto_ptr<string> (new string("Fowl Balls")),
		auto_ptr<string> (new string("Duck Walks")),
		auto_ptr<string> (new string("Chicken Runs")),
		auto_ptr<string> (new string("Turkey Errors")),
		auto_ptr<string> (new string("Goose Eggs"))
		};
    auto_ptr<string> pwin;
 
    // 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串,films[2]从而变成空指针
    pwin = films[2]; // films[2] loses ownership. 

    cout << "The nominees for best avian baseballl film are\n";
    for(int i = 0; i < 5; ++i)
        cout << *films[i] << endl;
        
    cout << "The winner is " << *pwin << endl;
    cin.get();

    return 0;
}

   运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:

  • 使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwinfilms[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
  • 使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因代码(pwin = films[2];)出现错误。

2、unique_ptr

    unique_ptr 由 C++11 引入,旨在替代不安全的 auto_ptr。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

  • unique_ptr 独享所有权,两个 unique_ptr 不能指向同一个对象。
  • unique_ptr 还未指向对象时,状态被为 nullptr。
  • unique_ptr 对象会在它们自身被销毁时使用删除器自动删除它们管理的对象。
  • 对象所有权可以由一个 unique_ptr 转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源。
  • 可以使用 std::make_unique 创建 unique_ptr 对象,std::make_unique<>() 是C++ 14 引入的新函数。
    在这里插入图片描述

2.1 unique_ptr 为何优于 auto_ptr。

  • 请看下面的代码:
auto_ptr<string> p1(new string ("auto")//#1
auto_ptr<string> p2;                       		//#2
p2 = p1;     

   在语句#3中,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

   编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptrauto_ptr更安全。

2.2 unique_ptr 转移操作

   重点1: 当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 左值 not allowed 

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


   重点2: C++有一个标准库函数std::move(),它的作用是无论你传给它的是左值还是右值,通过std::move之后都变成了右值。
   使用std::move()后,原来的指针仍转让所有权后变成空指针nullptr,可以对其重新赋值。

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s));
    return temp;
}

int main()
{
    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);

    bool flag = ps1 == nullptr;
    cout << "ps2:" <<*ps2 << ", ps1:" << flag << endl;
	
	// 输出结果
	// ps2:hello, ps1:1
}

2.3 unique_ptr 常用方法


unique_ptr成员函数:

  • <1>. 构造方法
  • <2>. 析构方法 ptr = nullptr 释放up指向的对象,将ptr置为空。
  • <3>. get() 返回内部对象地址(指针)。
  • <4>. release() 放弃对它所指对象的控制权,并返回保存的指针,将ptr置为空,不会释放内存。
  • <5> swap() 交换两unique_ptr所指向的对象。
  • <6-1> reset() 释放指针指向的对象,并使unique_ptr 对象为空。
  • <6-2> reset(new ) 释放指针指向的对象,并使unique_ptr 指向新创建的对象
  • <7> make_unique,使用 std::make_unique 创建 unique_ptr 对象。C++14

  • <1>. 构造方法 std::unique_ptr::unique_ptr
//unique_ptr constructor example
#include <iostream>
#include <memory>
 
int main () {
  std::default_delete<int> d;
  std::unique_ptr<int> u1;
  std::unique_ptr<int> u2 (nullptr);
  std::unique_ptr<int> u3 (new int);
  std::unique_ptr<int> u4 (new int, d);
  std::unique_ptr<int> u5 (new int, std::default_delete<int>());
  std::unique_ptr<int> u6 (std::move(u5));
  std::unique_ptr<int> u7 (std::move(u6));
  std::unique_ptr<int> u8 (std::auto_ptr<int>(new int));
 
  std::cout << "u1: " << (u1?"not null":"null") << '\n';
  std::cout << "u2: " << (u2?"not null":"null") << '\n';
  std::cout << "u3: " << (u3?"not null":"null") << '\n';
  std::cout << "u4: " << (u4?"not null":"null") << '\n';
  std::cout << "u5: " << (u5?"not null":"null") << '\n';
  std::cout << "u6: " << (u6?"not null":"null") << '\n';
  std::cout << "u7: " << (u7?"not null":"null") << '\n';
  std::cout << "u8: " << (u8?"not null":"null") << '\n';
 
  return 0;
}

输出结果

u1: null
u2: null
u3: not null
u4: not null
u5: null
u6: null
u7: not null
u8: not null

  • <2>. 析构方法 std::unique_ptr::~unique_ptr
int main () {
  // user-defined deleter
  auto deleter = [](int*p){
    delete p;
    std::cout << "[deleter called]\n";
  };
  
  std::unique_ptr<int,decltype(deleter)> foo (new int,deleter);
  std::cout << "foo " << (foo?"is not":"is") << " empty\n";
  return 0; // [deleter called]
}

执行结果为:

foo is not empty
[deleter called]

  • <3>. 方法 get()
    std::unique_ptr<int> ptr_(new int(10));
    
    auto ad = ptr_.get();
    cout << ad << endl;			// 打印地址 0x15f7c20
	
	int* p = ptr_.get();
	cout << "int*: " << *p << " ###  unique_ptr: " << *ptr_ << endl; 
    
    *p = 20;
    cout << "int*: " << *p << " ###  unique_ptr: " << *ptr_ << endl;

    p = nullptr;
    cout << "unique_ptr: " << *ptr_ << endl;

重点get()返回内部对象地址,普通指针(int*)pptr_共同管理一个对象,普通指针生命周期结束并不会释放对象,执行结果为:

0x922c20
int*: 10 ###  unique_ptr: 10
int*: 20 ###  unique_ptr: 20
unique_ptr: 20

  • <4>. 释放方法 std::unique_ptr::release
std::unique_ptr<int> ptr_1 (new int(10));
cout << "ptr_1:" << ptr_1.get() << endl; 		// 打印 unique_ptr 地址

int* ptr_2 = ptr_1.release();
cout << "ptr_2:" << ptr_2 << endl;				// 打印 int* 地址

delete ptr_2;

执行结果为:

ptr_1:0xbd7c20
ptr_2:0xbd7c20

  • <5>. 方法 swap()
    std::unique_ptr<int> foo (new int(10));
    std::unique_ptr<int> bar (new int(20));

    // org adress
    cout << "foo adress" << foo.get() << '\n';
    cout << "bar adress" << bar.get() << '\n';

    foo.swap(bar);
    std::cout << "foo: " << *foo << ", adress" << foo.get() << '\n';
    std::cout << "bar: " << *bar << ", adress" << bar.get() << '\n';

输出结果:

foo adress0x25f8c20
bar adress0x25f8c40
foo: 20, adress0x25f8c40
bar: 10, adress0x25f8c20

  • <6>. 方法 reset()
struct Test {
    int ID;
    Test(int id) :ID(id) {
        std::cout << "Task::Constructor" << std::endl;
    }
    ~Test() {
        std::cout << "Task::Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<Test> TestPtr(new Test(23));

    TestPtr.reset();
    if (!TestPtr) cout << "TestPtr empty! \n";


    cout << " ------------- ";
    TestPtr.reset(new Test(100));
    TestPtr == nullptr; 		
}

打印结果:

Task::Constructor
Task::Destructor
TestPtr empty! 
 ------------- 
Task::Constructor
Task::Destructor

  • <7>. 方法 make_unique<>()
// 使用<6>中定义的类方法
int main(){
	std::unique_ptr<Test> TestPtr = std::make_unique<Test>(1000);
}

// 输出
Task::Constructor
Task::Destructor

2.4 unique_ptr 自定义删除器


3、shared_ptr

    shared_ptr 是一个标准的共享所有权的智能指针,使用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。
    shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。成员函数有:

  • use_count() 返回引用计数的个数。
  • unique() 返回是否独占所有权。
  • swap() 交换指向的对象。
  • reset() 放弃指向对象的所有权。
  • get() 返回内部对象地址(指针)。
  • operator* 返回对存储指针指向的对象的引用。
  • operator->返回指向存储指针所指向的对象的指针

       此外,我们可以使用std::make_shared创建 shared_ptr 对象,std::make_shared是C++11的一部分。
    shared_ptr<string> p = make_shared<string>(new ***);。

试列代码:

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

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

    // use_count、 unique
	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);

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

    // reset
	cout << ps2.use_count() << endl;	// 2
	cout << ps3.use_count() << endl;	// 2: ps2 ps3 管理同一对象
	ps2.reset();	                    // ps2放弃管理权,引用计数的减少
	cout << ps2.use_count() << endl;	// 0
	cout << ps3.use_count() << endl;	//1
}

4、weak_ptr

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

    weak_ptr是一种不控制对象生命周期的智能指针,它指向由一个shared_ptr管理的智能指针。将一个 weak_ptr 绑定到一个shared_ptr对象,不会改变 shared_ptr 的引用计数。
    重点: 一旦最后一个所指向对象的shared_ptr被销毁,所指向的对象就会被释放,即使此时有weak_ptr指向该对象,所指向的对象依然被释放。
    重点: 它是对 对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。


成员函数

  • 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 内部对象的计数.
int main()
{
    //default consstructor
    weak_ptr<string> wp;
    {
        shared_ptr<string> p = make_shared<string>("hello world!\n");
        wp = p;   
        cout << "use_count: " <<wp.use_count() << endl;				// use_count:1
    }
    cout << "use_count: " << wp.use_count() << endl;				// use_count:0
}
  • 使用weak_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;
}

// 运行结果
// 1
// 1
// 2
// 2

    可以看到fun()papb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针papb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。

    将class A 中的 shared_ptr pb_;,改为weak_ptr pb_; ,修改情况及运行结果如下:

class A
{
public:
	weak_ptr<B> pb_; // shared_ptr 改为 weak_ptr
	~A(){ cout << "A delete\n"; }
};
...
// 运行结果
// 1
// 1
// 1
// 2
// B delete
// A delete
  • 由于weak_ptr 没有重载*->(所以我们不能通过weak_ptr直接访问对象的方法。),但可以使用 lock 获得一个可用的 shared_ptr 对象。
shared_ptr<B> p = pa->pb_.lock();
p->func();			// 假设func() 为 class_B 中方法

5、如何选择智能指针

<1>. 如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:

  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
  • 两个对象包含都指向第三个对象的指针;
  • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

<2>. unique_ptr

  • 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr
  • 如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。
  • 可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。

参考文章

[1] http://www.cplusplus.com/reference/memory/unique_ptr/
[2] https://blog.csdn.net/K346K346/article/details/81478223
[3] https://www.cnblogs.com/WindSun/p/11444429.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值