C++面试难点系列-智能指针

简介

智能指针作为C++管理内存的重要工具,在现代C++中被广泛使用。在智能指针未出现之前,指针的管理一直存在许多的问题,很容易出现野指针、指针悬挂,踩内存等问题。野指针指的是指针指向的内存被释放,但是指针未置空,导致代码再次调用的时候指向一块非法内存。指针悬挂指的是多个指针指向一块内存,当其中一个指针将内存释放之后,其他指针不知道资源被释放,就会处于悬挂的状态。上面两种问题都有可能造成踩内存的情况,当一块内存被释放之后,可能会被其他分配的内存占用,这时候原来的指针调用的时候还会误以为这是之前的内存而导致问题。同时,new调用之后未调用delete,或者重复调用delete而产生的coredump问题,都有可能发生。
为了解决上述问题,C++引入了智能指针。智能指针主要是根据RAII的思想,简单的来说就是利用对象的生命周期来管理,在构造函数中进行加锁,在析构函数进行解锁。

智能指针分类

shard ptr

shared ptr从名字可以看出这个是共享的智能指针,主要用于解决多个指针指向同一块内存的时候,其中一块释放之后,其他的指针未被告知的情况。实现的原理就是利用引用计数的方式,shared ptr内部会计算有多少个共享指针指向这块内存,当这个引用计数变为0的时候,就会自动释放这块内存。当然,相比于裸指针来说,使用这样的方法可能会一定程度上增加系统的性能损耗,但我个人认为这样的损耗是可以忽略不计的。
shared ptr有如下几种方式进行初始化:

#include <iostream>
#include <memory>  // 使用智能指针需要包含的头文件
using namespace std;

int main()
{
shared_ptr<int> ptr1(new int(100)); // 初始化一块100大小的int类型的内存 count 1
shared_ptr<int> ptr2(ptr1);		// 拷贝构造函数 count 2
shared_ptr<int> ptr3 = ptr2;		// 赋值构造函数 count 3
shared_ptr<int> ptr4(std::move(ptr1));    // 移动构造函数 count 3
shared_ptr<int> ptr5 = std::move(ptr2);    // 移动赋值构造函数 count 3
}

上述代码中每次调用,前三个内部的引用计数都会加1,但是使用移动语义时,由于只是进行资源的转移,所以引用计数不会发生变化。可以使用内置函数use_count来获取使用的个数,使用get可以返回指向这块内存的裸指针。
除了以上方法之后,还可以使用make_shared方法构造,假设有一个类A,需要构造一个A的shared ptr。同时也可以使用reset进行初始化,但需要注意分配内存是类需要和之前指针指向的类型吻合。

std::shared_ptr<A> a = std::make_shared<A>();
a.reset(); // 调用reset之后,对应的引用计数会减一,由于目前的引用计数就是1,减去1之后就变为0,这时候就会调用析构函数
std::shared_ptr<A> b = std::make_shared<A>();
b.reset(new A(1)); // 这个reset调用时会先将b的引用计数减1,当引用计数变为0的时候同样会调用析构函数
				   //随后将b指向一块新的内存,因此这也是一种新的初始化的方法
				   // 但需要注意的是reset再次进行初始化是new的类型需要和之前的类型吻合

删除器

这个删除器的意义在于shared ptr提供了自定义的方式调用指针析构时调用的析构函数。默认情况下,会调用类本身自带的析构函数。但也可以通过指定的方式进行调用。
同样假设有一个类A

std::shared_ptr<A> a(new A(), [](A* a){
	// 自定义的释放内存的操作
	delete a;
});

// 还有一种使用场景在于new一个数组的时候,正常的默认调用析构只会调用一次析构
std::shared_ptr<A> a(new A[5]);
// 这种方式调用的时候数组析构会将内部的数组析构
std::shared_ptr<A> a(new A[5], [](A *a){
	delete[] a;
});
// 同时c++还提供了一种默认的delete函数
std::shared_ptr<A> a(new A[5], default_delete<A[]>());

unique ptr

unique ptr独享他指向的对象,也就是说同时只有一个unique ptr指向一个对象,需要转移时使用move进行转移。当unique ptr被销毁的时候,对应的对象资源也会被释放。因此调用reset时,对应的资源也会被释放。

初始化

unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = std::move(ptr1);
// ptr2.reset();
ptr2.reset(new int(11)); // 也可以这样去将ptr2再次进行指向

删除器

unique ptr的删除器使用和shared ptr的删除器使用有些区别,需要在调用的指定函数的类型
unique ptr还能直接传入数组类型的对象进行管理

using tFunc = void(*)(A*);
unique_ptr<A, tFunc> ptr2(A, [](A* a){
	delete a;
});

// 当lamda表达式的捕获器捕获外部数据时,我们就无法使用函数指针的方式指定
// 这时候这个仿函数就需要使用function进行包装
unique_ptr<A, std::function<void(A *)>> ptr2(A, [=](A* a){
	delete a;
});

// 直接释放数组内存,同样,shared_ptr也支持这样的写法
unique_ptr<A[]> ptr6(new A[5]);

weak ptr

弱引用指针并不能单独的管理一块内存,它主要是用来辅助shared_ptr用于解决循环引用,以及返回shared_ptr返回this指针的问题。因此进行初始化的时候也可以使用shared ptr来进行初始化。
use_count方法也是用于当前所观测资源的引用计数;expired函数用于检查观测的资源是否被释放,返回true表示被释放,反之为false。
lock方法可以返回weak ptr当前管理的这个shared ptr的对象;调用reset方法则会清空weak ptr 对象,使其不再管理这个shared ptr

struct Test
{
	shared_ptr<Test> getSharedPtr() {
		return shared_ptr<Test>(this);
	}
};

int main()
{
	shared_ptr<Test> ptr1(new Test());
	shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
	
	shared_ptr<Test> ptr1(new Test);
	weak_ptr<Test> ptr2(ptr1);
	weak_ptr<Test> ptr3 = ptr1;

	weak_ptr<Test> ptr4; // 这样的话就是一个空的对象

	shared_ptr<int> sp1 = std::make_shared<int>(111);
	weak_ptr<int> wp1 = sp1;
	shared_ptr<int> sp2 = wp1.lock()	// 调用lock方法也可以返回weak ptr当前管理的这个shared ptr的对象
	std::cout << wp1.use_count() << std::endl;
	std::cout << wp1.expired() << std::endl;
	wp1.reset();
	std::cout << wp1.expired() << std::endl;
	std::cout << wp1.use_count() << std::endl;
}

再看一下这个weak ptr是如何辅助shared ptr解决,shared ptr比较难处理的问题的。
目前shared ptr使用的时候需要注意以下几点:

  1. 不能使用原始地址初始化多个共享智能指针;
  2. 不能返回This的智能指针对象;
  3. 共享智能指针不能进行循环引用
struct A
{
	A(){
		std::cout << "call A() " << std::endl;
	}
	shared_ptr<A> getSharedPtr() {
		return shared_ptr<Test>(this);
	}

	~A(){
		std::cout << "call ~A() " << std::endl;
	}
};

// 上述返回This对象的智能指针会造成重复析构,c++11中的解决方法是类继承enable_shared_from_this这个模板类,再调用函数
// 	shared_from_this函数这样就可以在不影响引用计数的情况下返回共享智能指针。
struct Apublic enable_shared_from_this<A>
{
	A(){
		std::cout << "call A() " << std::endl;
	}
	shared_ptr<A> getSharedPtr() {
		return shared_from_this(); // 内部会调用weak ptr调用lock方法获取共享智能指针对象 
	}

	~A(){
		std::cout << "call ~A() " << std::endl;
	}
};

int main()
{
	A* a = new A();
	// 这样的初始化方式的问题在于两个共享智能指针创建出来之后都会指向这块a的内存,这样就会调用两次析构函数
	shared_ptr<A> sp1(a);
	shared_ptr<A> sp2(a);

	// 这个问题sp4返回的This指针初始化的智能指针和sp3创建的智能指针共同管理愉快内存,这样就会造成析构时重复
	shared_ptr(A) sp3(new A);
	shared_ptr(A) sp4 = sp3->getSharedPtr();
}

对于循环引用的问题,再看这个例子

struct Test
{
	shared_ptr<Test> getSharedPtr() {
		return shared_ptr<Test>(this);
	}
};

int main()
{
	shared_ptr<Test> ptr1(new Test());
	shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
	
	shared_ptr<Test> ptr1(new Test);
	weak_ptr<Test> ptr2(ptr1);
	weak_ptr<Test> ptr3 = ptr1;

	weak_ptr<Test> ptr4; // 这样的话就是一个空的对象

	shared_ptr<int> sp1 = std::make_shared<int>(111);
	weak_ptr<int> wp1 = sp1;
	shared_ptr<int> sp2 = wp1.lock()	// 调用lock方法也可以返回weak ptr当前管理的这个shared ptr的对象
	std::cout << wp1.use_count() << std::endl;
	std::cout << wp1.expired() << std::endl;
	wp1.reset();
	std::cout << wp1.expired() << std::endl;
	std::cout << wp1.use_count() << std::endl;
}

再看一下这个weak ptr是如何辅助shared ptr解决,shared ptr比较难处理的问题的。
目前shared ptr使用的时候需要注意以下几点:

  1. 不能使用原始地址初始化多个共享智能指针;
  2. 不能返回This的智能指针对象;
  3. 共享智能指针不能进行循环引用
struct A
{
	shared_ptr<B> bsp; 
	A(){
		std::cout << "call A() " << std::endl;
	}
	~A(){
		std::cout << "call ~A() " << std::endl;
	}
};

struct B
{
	shared_ptr<A> asp; 
	B(){
		std::cout << "call B() " << std::endl;
	}
	~B(){
		std::cout << "call ~B() " << std::endl;
	}
};

int main()
{
	shared_ptr<A> ap(new A);
	shared_ptr<B> bp(new B);
	ap->bsp = bp;
	bp->asp = ap;
}

这样的话,例子中定义的智能指针内部的引用计数一直无法减为0,导致问题发生。
解决的方法就是使用weak ptr去监测shared ptr,而不是直接使用shared ptr,这样引用计数就不会增加。

struct A
{
	weak_ptr<B> bsp; 
	A(){
		std::cout << "call A() " << std::endl;
	}
	~A(){
		std::cout << "call ~A() " << std::endl;
	}
};

struct B
{
	weak_ptr<A> asp; 
	B(){
		std::cout << "call B() " << std::endl;
	}
	~B(){
		std::cout << "call ~B() " << std::endl;
	}
};

int main()
{
	shared_ptr<A> ap(new A);
	shared_ptr<B> bp(new B);
	ap->bsp = bp;
	bp->asp = ap;
}

结束语

以上就是智能指针的详解,希望对大家有帮助。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值