初识C++ · 智能指针

目录

前言:

1 智能指针的发展历史

2 unique_ptr和shared_ptr的基本使用

3 shared_ptr的模拟实现

4 有关定制删除器


前言:

智能指针的引入,我们得先从异常开始说起,异常面临的一个窘境是new了多个对象,抛异常了会导致先new的对象没有析构,从而导致内存泄漏的问题,解决方法是使用RAII

RAII是一种技术,英文的全称是:Resource Acquisition Is Initialization,是利用对象的声明周期来解决内存泄漏的一种技术。

就像这样:

template<class T>
class Smart_ptr
{
public:
	Smart_ptr(T* ptr)
		:_ptr(ptr)
	{}


	~Smart_ptr()
	{
		cout << "~Smart_ptr" << endl;
	}

private:
	T* _ptr;
};
double Division(int a, int b)
{
	if (b == 0)
	{
		throw string("除数为0");
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	Smart_ptr<int> p1 = new int[1000];
	Smart_ptr<int> p2 = new int[1000];

	int a = 0, b = 0;
	cin >> a >> b;
	Division(a, b);

}
int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "捕获到未知异常" << endl;
	}
	return 0;
}

之前的忧虑来源于第二个抛异常之后前面的对象不会析构的问题,那么现在用一个智能指针对象来接受指针,不仅起到了管理空间的作用,还可以调用原本的行为,比如* -> ,这里还没有重载。

上文有些许未介绍的,比如exception,是C++异常体系里面的基类,所以捕捉到了异常我们就可以用exception来捕捉,打印是用的what函数,就和前面提到的示例代码是一样的。

现在就对智能指针有了一个简单的理解了,现在我们就深入了解一下。


1 智能指针的发展历史

提到智能指针,我们首先应该想到的是C++98里面的auto_ptr,早在C++98就引入了智能指针,但是使用确实让人一言难尽,它的本质是转移控制权,所以会导致一个悬空,像move左值一样,导致原来的自定义对象变为空了,这就不是将亡值的施舍了,是活生生的夺舍:

就像文档里面提到的,推荐使用unique_ptr而不是auto_ptr,因为是指针,所以需要重载函数使得该类有指针的对应行为,比如* -> ,这里我们先使用,所在的头文件是memory:

int main()
{
	auto_ptr<string> s1(new string("aaaa"));
	auto_ptr<string> s2(s1);
	return 0;
}

当我们通过监视窗口可以看到:

执行了拷贝构造之后,s1就悬空了,相应的,s2获得了控制权,但是,属实有点鸡肋。

比如之后还要访问s1,那就报错了,反正呢,很多公司是禁止使用auto_ptr的。

那么呢,在03年的时候,引入了一个库boost,这个库是由C++委员会的部分工作人员搞的,在C++11里面引入的很多新特性都是在boost里面借鉴的,其中就涉及到了智能指针,在boost里面有三种智能指针,分别是scoped_ptr,shared_ptr,weak_ptr,其中也有引入数组,但是C++11并没有采纳。

在C++11引入智能指针的时候就借鉴了boost的智能指针,但是有一个指针改了一个名,scoped_ptr改成了unique_str,为什么改呢,咱也不知道,学就完事儿了。

所以今天的重点就是unique_str和shared_ptr和weak_ptr,其中shared_ptr是人人都要会的。


2 unique_ptr和shared_ptr的基本使用

对于使用方面都是很简单,咱们先看一个文档:

模板参数有一个D = 什么什么的,这时定制删除器,本文后面会介绍,现在先不急。

基本的函数使用就是:

这就是所有的成员函数了,get函数是获取原生指针,release是显式的释放指针,但不是显式的析构这个类,同样的,既然是指针,就应该具备指针的行为,比如* ->等,有了stl的基本,这些我们应该是看一下文档就应该知道怎么使用的,这里再看看构造函数:

注意的是unique_ptr是不支持拷贝构造的,重载的第9个函数,拷贝函数被delete修饰了,所以不支持。其中支持auto_ptr 右值构造什么的,咱们先不管,主要了解前面几个就可以了。

class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A> sp1(new A[10]);
	unique_ptr<A> sp2(sp1);
	A* p = sp1.get();
	cout << p << endl;

	sp1->_a1++;
	sp1->_a2++;

	return 0;
}

找找错误?

第一个 不能拷贝构造,第二析构会报错,因为开辟的是一个数组,基本类型是A,应该是A[],这就和定制删除器有关系了,所以这里的正确代码是:

class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A[]> sp1(new A[10]);
	//unique_ptr<A> sp2(sp1);
	A* p = sp1.get();
	cout << p << endl;

	//sp1->_a1++;
	//sp1->_a2++;
	sp1[1]._a1++;
	sp1[1]._a2++;

	return 0;
}

这里的执行结果就是析构了十次:

为什么new了十个空间,基本类型也要是一个数组,这里的解决方案是定制删除器,先不管。

然后就是shared_ptr的基本使用:

相比之下shared_ptr就朴实无华很多了。

成员函数这么多,这里和pair有点像的是,有一个make_shared的东西,在构造的时候会用到:

因为它支持拷贝构造,欸~所以我们可以使用make_shared构造,产生临时对象拷贝一个就行。

简单介绍一下其中的成员函数,use_count是用来计算计数的,因为在智能指针要实现的一个事就是,管理资源更加简单,比如多个对象共同管理一块资源,新加了一个对象管理资源,引入计数就++,反之--,对比auto那里不能拷贝,就是因为如果拷贝了,造成了多次析构的问题,就会报错。

那么要验证计数很简单:

int main()
{
	//shared_ptr<int> sp1(new int);
	shared_ptr<int> sp1 = make_shared<int>(1);
	cout << sp1.use_count() << endl;
	{
		shared_ptr<int> sp2(sp1);
		cout << sp2.use_count() << endl;
	}
	cout << sp1.use_count() << endl;
	return 0;
}

既然是出了局部域就会销毁,那么我们创建一个局部域即可:

简单使用了解一下可以了,咱们这里简单模拟实现一下。


3 shared_ptr的模拟实现

目前对于shared_ptr我们的简单理解有了,那么现在简单捋一下,智能指针防止多次析构的方法是使用计数的方式,那么我们就需要一个计数了,问题是这个变量我们如何创建呢?

template<class T>
class shared_ptr
{
private:
	T* ptr;
	int count;
};

这样可行吗?显然是不可行的,因为我们创建一个对象,指向空间,就会让这个类多次实例化,每个对象都有一个count,实例化一次count++,然后呢就导致每个对象都有count,count的都是一样的,我们指向一个空间,创建了n个对象,预期是计数为n,但是创建的对象是独立的,析构只会析构某个对象的count,从而count的大小是n * n,所以结果是不可预估的。

那么我们应该用static吗?每创建一个对象就++一次,看起来好像可以,但是我们如果指向的空间不是一个呢?new了两个空间,就会导致两个空间公用一个计数,更不行了。

所以解决办法是创建一个指针,每创建一个对象,指针指向的空间,即计数空间就++:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		(*_pcount)++;
    }


	int use_count()
	{
		return *_pcount;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pcount;
};

这是一般的函数,重点在于赋值重载 和 定制删除器。

对于赋值重载来说,指向的空间修正,指向的计数空间修正,那么原来指向的空间是否需要修正呢?这就要看计数是否到0了,所以需要判断是否到0,到了就析构:

那么析构函数,简单的,new了空间删除就完事了:

void release()
{
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
	}
}
~shared_ptr()
{
	release();
}

赋值重载判断+ 改定指向:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (--(sp._pcount) == 0)
	{
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;

	}
	return *this;
}

但是,万一有人突发奇想,想自己给自己赋值呢?再万一,有人在指向空间是同一块的基础上相互赋值呢?

所以我们不妨判断一下指向空间的地址是不是一样的:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}
	return *this;
}

shared_ptr的基本实现就完成了。

现在引入一个线程安全的问题:
 

mutex mtx;
void Func(Ptr::shared_ptr<list<int>> sp, int n)
{
	cout << sp.use_count() << endl;
	
	for (int i = 0; i < n; i++)
	{
		Ptr::shared_ptr<list<int>> copy1(sp);
		Ptr::shared_ptr<list<int>> copy2(sp);
		Ptr::shared_ptr<list<int>> copy3(sp);

		mtx.lock();
		sp->push_back(i);	
		mtx.unlock();

	}
}

int main()
{
	Ptr::shared_ptr<list<int>> sp1(new list<int>);
	cout << sp1.use_count() << endl;

	thread t1(Func, sp1, 100);
	thread t2(Func, sp1, 200);

	t1.join();
	t2.join();

	cout << sp1->size() << endl;
	cout << sp1.use_count() << endl;

	return 0;
}

解决方法是引入atomic,原子操作:

shared_ptr(T* ptr)
	:_ptr(ptr)
	, _pcount(new std::atomic<int>(1))
{}

private:
	T* _ptr;
	std::atomic<int>* _pcount;

还有一个问题就是,如果是交叉指向,就会导致无法析构:

struct Node
{
	//std::shared_ptr<Node> _next;
	//std::shared_ptr<Node> _prev;

	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;
	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};
int main()
{
	std::shared_ptr<Node> p1(new Node);
	std::shared_ptr<Node> p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	cout << p1->_next.use_count() << endl;
	cout << p2->_prev.use_count() << endl;

	return 0;
}

 此时就需要弱指针,weak_ptr,只是指向空间,但是计数不++即可。


4 有关定制删除器

为什么会引入定制删除器呢?

int main()
{
	Ptr::shared_ptr<int> sp1((int*)malloc(sizeof(4)));
	Ptr::shared_ptr<FILE> sp2(fopen("test.txt", "w"));

	return 0;
}

在内存管理章节提及,内存管理的函数不能混合使用,何况是FILE呢,FILE*要使用fclose,所以我们应该定制一个删除器,但是如何传过去呢?传只能传到构造,传不到析构,所以我们不妨:

template<class D>
shared_ptr(T* ptr, D del)
	: _ptr(ptr)
	, _pcount(new std::atomic<int>(1))
	, _del(del)
{}

private:
	T* _ptr;
	std::atomic<int>* _pcount;
	std::function<void(T*)> _del = [](T* ptr) {delete ptr; };

要删除的时候,传对象就可以了,比如:

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};


int main()
{
	Ptr::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
	Ptr::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
	Ptr::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
	Ptr::shared_ptr<A> sp4(new A);

	return 0;
}

智能指针就介绍到这里。


感谢阅读!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值