智能指针使用及详细解析

文章目录

  • 智能指针
    • 概念
    • 为什么使用智能指针
    • 智能指针使用
      • 智能指针的常用函数
        • get() 获取智能指针托管的指针地址.
        • reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
      • auto_ptr
      • unique_ptr
      • shared_ptr
        • **shared_ptr的原理**
        • 引用计数的使用
        • 构造
        • 初始化
        • 主动释放对象
        • 重置
        • 交换
        • shared_ptr使用陷阱
          • 1. 互相引用的时候,就会出现循环引用的现象 造成无法释放资源
          • 2.不要把一个原生指针给多个智能指针管理;
          • 3.禁止delete 智能指针get 函数返回的指针;
      • weak_ptr
    • shared_from_this
      • 使用原因
      • shared_from_this 使用注意事项
          • 不能再构造函数中调用shared_from_this()
        • 继承问题
    • 参考

智能指针

概念

在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。

所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间

其实是RALL的思想。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。
智能指针使用及详细解析_第1张图片

为什么使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏!

如下例子就是内存泄露的例子:

#include 
#include 
#include 

using namespace std;


// 动态分配内存,没有释放就return
void memoryLeak1() {
	string *str = new string("动态分配内存!");
	return;
}

// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
	string *str = new string("内存泄露!");

	// ...此处省略一万行代码

	// 发生某些异常,需要结束函数
	if (1) {
		return -1;
	}
	/
	// 另外,使用try、catch结束函数,也会造成内存泄漏!
	/

	delete str;	// 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
	return 1;
}


int main(void) {

	memoryLeak1();

	memoryLeak2();

	return 0;
} 

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!

memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

所以我们要把分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存。

智能指针就是通过这个原理来解决指针自动释放的问题!

智能指针使用

智能指针的使用跟普通指针类似,可以使用运算符“ * " 和 ” -> "去获得指向的对象,

为什么智能指针可以像普通指针那样使用???

因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象。

智能指针使用及详细解析_第2张图片

C++ 提供了多种智能指针:

C++98 提供了 auto_ptr 模板的解决方案
C++11 增加unique_ptr、shared_ptr 和weak_ptr

智能指针的三个常用函数:

智能指针的常用函数

get() 获取智能指针托管的指针地址.

// 定义智能指针
auto_ptr<Test> test(new Test);

Test *tmp = test.get();		// 获取指针返回
cout << "tmp->debug:" << tmp->getDebug() << endl;

但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
函数原型:

_NODISCARD _Ty * get() const noexcept
{	// return wrapped pointer
	return (_Myptr);
}

reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

// 定义智能指针
auto_ptr<Test> test(new Test);

test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL

test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之

reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。

函数原型:

void reset(_Ty * _Ptr = nullptr)
{	// destroy designated object and store new pointer
	if (_Ptr != _Myptr)
		delete _Myptr;
	_Myptr = _Ptr;
}

auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

它采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
智能指针使用及详细解析_第3张图片

智能指针使用及详细解析_第4张图片
智能指针使用及详细解析_第5张图片
如果auto_ptr调用拷贝构造函数或者赋值重载函数后,如果再去使用原来的对象的话,那么整个程序就会崩溃掉(因为原来的对象被设置为nullptr),这对程序是有很大的伤害的.所以很多公司会禁用auto_ptr智能指针。

C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。

auto_ptr 被C++11抛弃的主要原因

  1. 复制或者赋值都会改变资源的所有权
  2. 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
  3. 不支持对象数组的内存管理

unique_ptr

C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。

unique_ptr特性

1、基于排他所有权模式:两个指针不能指向同一个资源
智能指针使用及详细解析_第6张图片
2、在 STL 容器中使用unique_ptr,不允许直接赋值
3、支持对象数组的内存管理

// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]);	// 支持这样定义

shared_ptr

shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃掉。

shared_ptr的原理

shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:

  • shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
  • 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
  • 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
  • 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
    智能指针使用及详细解析_第7张图片
    销毁过程:
    智能指针使用及详细解析_第8张图片
    举例:
class Person {
public:
	Person(int v) {
		this->no = v;
		cout << "构造函数 \t no = " << this->no << endl;
	}

	~Person() {
		cout << "析构函数 \t no = " << this->no << endl;
	}

private:
	int no;
};

// 仿函数,内存删除
class DestructPerson {
public:
	void operator() (Person *pt) {
		cout << "DestructPerson..." << endl;
		delete pt;
	}
};

引用计数的使用

调用use_count函数可以获得当前托管指针的引用计数。

shared_ptr<Person> sp1;

shared_ptr<Person> sp2(new Person(2));

// 获取智能指针管控的共享指针的数量	use_count():引用计数
cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;

// 共享
sp1 = sp2;

cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;

shared_ptr<Person> sp3(sp1);
cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl;
cout << "sp2	use_count() = " << sp3.use_count() << endl << endl;

如上代码,sp1 = sp2; 和 shared_ptr< Person > sp3(sp1);就是在使用引用计数了。

sp1 = sp2; --> sp1和sp2共同托管同一个指针,所以他们的引用计数为2;

shared_ptr< Person > sp3(sp1); --> sp1和sp2和sp3共同托管同一个指针,所以他们的引用计数为3;

智能指针使用及详细解析_第9张图片

构造

  1. shared_ptr< T > sp1; 空的shared_ptr,可以指向类型为T的对象

    shared_ptr<Person> sp1;
    Person *person1 = new Person(1);
    sp1.reset(person1);	// 托管person1
    
  2. shared_ptr< T > sp2(new T()); 定义shared_ptr,同时指向类型为T的对象

    shared_ptr<Person> sp2(new Person(2));
    shared_ptr<Person> sp3(sp1);
    
  3. shared_ptr sp4; 空的shared_ptr,可以指向类型为T[]的数组对象 C++17后支持

    shared_ptr<Person[]> sp4;
    
  4. shared_ptr sp5(new T[] { … }); 指向类型为T的数组对象 C++17后支持

    shared_ptr<Person[]> sp5(new Person[5] { 3, 4, 5, 6, 7 });
    
  5. shared_ptr< T > sp6(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D释放内存

    shared_ptr<Person> sp6(NULL, DestructPerson());
    
  6. shared_ptr< T > sp7(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存

    shared_ptr<Person> sp7(new Person(8), DestructPerson());
    

初始化

  1. 构造函数

    shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(up1);  // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
    
  2. 使用make_shared 初始化对象,分配内存效率更高(推荐使用)
    make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:

    make_shared<类型>(构造类型对象需要的参数列表);

    shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
    shared_ptr<string> up4 = make_shared<string>("字符串");
    shared_ptr<Person> up5 = make_shared<Person>(9);
    

    赋值:

    shared_ptrr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(new int(11));   // int(11) 的引用计数为1
    up1 = up2;	// int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
    

主动释放对象

	shared_ptrr<int> up1(new int(10));
	up1 = nullptr ;	// int(10) 的引用计数减1,计数归零内存释放 
	// 或
	up1 = NULL; // 作用同上 

重置

p.reset() ; 将p重置为空指针,所管理对象引用计数 减1

p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控

p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器

p1是一个指针!

交换

p1 和 p2 是智能指针

std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2);    // 交换p1 和p2 管理的对象,原对象的引用计数不变

shared_ptr使用陷阱

1. 互相引用的时候,就会出现循环引用的现象 造成无法释放资源

如下代码:
Boy类中有Girl的智能指针;
Girl类中有Boy的智能指针;
当他们交叉互相持有对方的管理对象时…

#include 
#include 
#include 

using namespace std;

class Girl;

class Boy {
public:
	Boy() {
		cout << "Boy 构造函数" << endl;
	}

	~Boy() {
		cout << "~Boy 析构函数" << endl;
	}

	void setGirlFriend(shared_ptr<Girl> _girlFriend) {
		this->girlFriend = _girlFriend;
	}

private:
	shared_ptr<Girl> girlFriend;
};

class Girl {
public:
	Girl() {
		cout << "Girl 构造函数" << endl;
	}

	~Girl() {
		cout << "~Girl 析构函数" << endl;
	}

	void setBoyFriend(shared_ptr<Boy> _boyFriend) {
		this->boyFriend = _boyFriend;
	}

private:
	shared_ptr<Boy> boyFriend;
};


void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());

	// 陷阱用法
	spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);
	// 此时boy和girl的引用计数都是2
}


int main(void) {
	useTrap();

	system("pause");
	return 0;
}

运行截图:
智能指针使用及详细解析_第10张图片
可以看出,程序结束了,但是并没有释放内存,这是为什么呢???

如下图:
当我们执行useTrap函数时,注意,是没有结束此函数,boy和girl指针其实是被两个智能指针托管的,所以他们的引用计数是2

智能指针使用及详细解析_第11张图片
seTrap函数结束后,函数中定义的智能指针被清掉,boy和girl指针的引用计数减1,还剩下1,对象中的智能指针还是托管他们的,所以函数结束后没有将boy和gilr指针释放的原因就是于此。
智能指针使用及详细解析_第12张图片
所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!

当然,这也是有办法解决的,那就是使用weak_ptr弱指针。

针对上面的情况,还讲一下另一种情况。如果是单方获得管理对方的共享指针,那么这样着是可以正常释放掉的!
例如:

void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());

	// 单方获得管理
	//spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);	
}

智能指针使用及详细解析_第13张图片

反过来也是一样的!

这是什么原理呢?

首先释放spBoy,但是因为girl对象里面的智能指针还托管着boy,boy的引用计数为2,所以释放spBoy时,引用计数减1,boy的引用计数为1;

在释放spGirl,girl的引用计数减1,为零,开始释放girl的内存,因为girl里面还包含有托管boy的智能指针对象,所以也会进行boyFriend的内存释放,boy的引用计数减1,为零,接着开始释放boy的内存。最终所有的内存都释放了。

2.不要把一个原生指针给多个智能指针管理;
int *x = new int(10);

unique_ptr< int > up1(x);

unique_ptr< int > up2(x);

// 警告! 以上代码使up1 up2指向同一个内存,非常危险

或以下形式:

up1.reset(x);

up2.reset(x);
3.禁止delete 智能指针get 函数返回的指针;

如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!

shared_ptr< int > sp1(new int(10));

// 一个典型的错误用法 shared_ptr< int > sp4(sp1.get());

weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少同时weak_ptr 没有重载*和-> .但可以使用 lock 获得一个可用的 shared_ptr 对象。
智能指针使用及详细解析_第14张图片

shared_from_this

enable_shared_from_this是一个模板类,定义于头文件,其原型为:

template< class T > class enable_shared_from_this;

    std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权。

    若一个类 T 继承 std::enable_shared_from_this ,则会为该类 T 提供成员函数:shared_from_this 。 当 T 类型对象 t 被一个为名为 pt std::shared_ptr 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr 对象,它与 pt 共享 t 的所有权。

使用原因

  1. 把当前类对象作为参数传给其他函数时,为什么要传递share_ptr呢?直接传递this指针不可以吗?一个裸指针传递给调用者,谁也不知道调用者会干什么?假如调用者delete了该对象,而share_tr此时还指向该对象。

  2. 这样传递share_ptr可以吗?share_ptr 这样会造成2个非共享的share_ptr指向一个对象,最后造成2次析构该对象。

举例:

      当你想使用一个类的某个成员函数返回指向这个类的指针的时候,可以这样写

class A
{
public:
	A(int y=0):x(y){ }
	A* getthis()
	{
		return this;
	}
	int x;
};
int main (void) 
{
	A a;
	cout<<a.x<<endl;
	A*p=a.getthis();
	p->x=1;
	cout<<a.x<<endl;
}

      通过在成员函数中返回this指针,我们得到了指向这个对象本身的指针,并且可以通过它来改变对象。

      但是在很多情况中,我们更想用智能指针去控制对象的生存周期,比如这样。

int main (void) 
{
	shared_ptr<A>sp1(new A());
	shared_ptr<A>sp2(sp1->getthis());
	cout<<sp1.use_count()<<endl;
	cout<<sp2.use_count()<<endl;
}

      两个智能指针的引用计数都是1,可想而知在函数退出的时候会发生什么,同一块内存被释放了两次,程序崩溃是免不了的。原因是this是裸指针,我们这么操作和用同一个裸指针给两个智能指针赋值是一个意思。如果我们能返回一个智能指针,那么问题就解决了。这时我们可以使用shared_from_this这个模版类。

class A:public enable_shared_from_this<A>
{
public:
	A(int y=0):x(y){ }
	shared_ptr<A> getthis()
	{
		return shared_from_this();
	}
	int x;
};

这样上面那段程序的输出是两个2,两个智能指针都意识到自己指向的是同一片内存,引用计数正确的发挥作用,同一片内存只会被析构一次。

shared_from_this 使用注意事项

不能再构造函数中调用shared_from_this()

除此之外,shared_from_this还有一个需要注意的地方。你可以点开shared_from_this的源码看看,他返回了enable_shared_from_this中唯一的一个成员变量

mutable weak_ptr<_Ty> _Wptr;

而当我们再看这个类的构造函数,并没有对这个成员变量赋值。可想而知,如果这个变量始终没有被赋值,那么我们无法使用shared_from_this这个函数。

为了弄清楚这个变量什么时候被赋值,我一步一步的调试了下面这行代码,虽说只有一行,但是他真的做了很多事情。

shared_ptr sp1(new A());

来看这条语句,它做了三件事情,第一件事情是构造enable_shared_from_this这个类,毕竟想要构造A就要先构造他的父类。

此时wptr没有被赋值。

第二件事情是构造A,此时wptr也没有被赋值。

第三件事情,构造sp1,令人想不到的是,构造sp1之后,属于类A的父类的wptr被赋值了

所以可想而知,如果没有这样一个sp1的出现,我们是无法使用shared_from_this的。所以才有了所说的“不能再构造函数中调用shared_from_this()”的说法,这个构造函数指的是A的构造函数,因为那个时候wptr还没有值,当然不能调用shared_from_this。

当然了,这样也是不行的。

int main (void) 
{
	A a;
	shared_ptr<A>sp(a.shared_from_this());
}

继承问题

在同一个继承体系中,不能同时出现多个enable_shared_from_this类。父类继承了enable之后,子类只能对shared_from_this()的返回值进行转型。也就是说把shared_ptr转换成shared_ptr,想做到这一点,你只能这么写

 return dynamic_pointer_cast<B>(shared_from_this());

如下代码验证:

namespace test_enable_shared_from_this{

    class Derived : public std::enable_shared_from_this<Derived>
    {
    public:
        void SetValue(int a)
        {
            _a = a;
        }
        int GetValue() const
        {
            return _a;
        }
    private:
        int _a;
    };

    class Derived1 : public Derived
    {
    private:
        int _b;
    };

}

调用代码如下:

int main()
{
    using namespace test_enable_shared_from_this;
    auto d1 = std::make_shared<Derived1>();
    auto obj = d1->shared_from_this();

    return 0;
}

智能指针使用及详细解析_第15张图片
发现问题没有?虽然d1和obj对象地址一样,但是对象内存包含的成员属性不同。d1有自身的属性成员变量,但是obj没有,只含有基类的属性成员变量。是不是shared_from_this返回的对象中只包含那些直接继承

std::enable_shared_from_this呢?我们用多继承验证下:

namespace test_enable_shared_from_this{

    class Derived : public std::enable_shared_from_this<Derived>
    {
    public:
        void SetValue(int a)
        {
            _a = a;
        }
        int GetValue() const
        {
            return _a;
        }
    private:
        int _a;
    };

    class Derived1 : public std::enable_shared_from_this<Derived1>
    {
    private:
        int _b;
    };

    class Derived2 : public Derived, public Derived1
    {
    private:
        int _c;
    };

}

int main()
{
    using namespace test_enable_shared_from_this;
    auto d2 = std::make_shared<Derived2>();
    auto obj = d2->shared_from_this();

    return 0;
}

智能指针使用及详细解析_第16张图片
直接提示执行不明确,这个很好理解,它不知道你究竟返回哪个子类std::shared_ptr。所以,继承类中不能存在多次继承std::enable_shared_from_this。哪个类继承的这个类,就返回这个类实例化的对象

参考

  • C++ 智能指针 - 全部用法详解
  • 智能指针详细解析
  • C/C++编程:理解make_shared
  • std::enable_shared_from_this使用
  • C++ 笔记 shared_from_this()的原理与使用
  • shared_from_this的使用

你可能感兴趣的:(C++,c++)