ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

对C++中的智能指针的理解和基本用法总结

2022-02-08 12:58:00  阅读:152  来源: 互联网

标签:p1 C++ 用法 计数 引用 shared ptr 指针


文章目录

1 智能指针的概述

毫无疑问,智能指针相比于普通的裸指针(也就是我们直接用 new出来的对象的指针)更加智能,最明确的体现在于,可以自动帮你管理内存泄漏的问题,也就是说是,使用智能指针,不需要你手动去delete一个指针;
简单的说:只能指针就是对普通的裸指针进行了一层包装,包装之后,就使得这个指针更加智能,能够自动在合适时间帮你去释放内存;

C++标准库提供了四种智能指针的使用:
std::auto_ptr; c++98就有的一种智能指针,但是现在被遗弃,完全被std::unique_ptr所取代;
下面三种都是C++11提供的新智能指针;
std::unique_ptr; 一种独占式智能指针,同一个时间内只能有一个指针指向该对象;
std::shared_ptr;多个指针可以指向同一个对象的指针;
std::weak_ptr;一种辅助std::shared_ptr指针而存在的;

使用智能指针时候,记得包含头文件#include<memory>


2 shared_ptr基础理解

shared_ptr指针用共享所有权的方式来管理所指向对象的生命周期的;也就是说:一个对象不仅可以被一个单独的shared_ptr所指向,也可以被多个shared_ptr所指向,多个shared_ptr相互协作,共同管理所指向对象的生命周期,当所指向对象不被需要时候,就把所指向对象释放掉它的内存。


而我们使用shared_ptr前:需要思考一个问题,所指向的对象是否需要多个指针所指向,也就是多个指针可以共享一个对象(多个指针指向同一份内存的意思);


shared_ptr管理内存的原理是:使用引用计数的方式。这种方式:可以在指向一份对象的最后一个智能指针shared_ptr不再需要指向该内存时候,就释放该对象的空间;
在这里插入图片描述

那我们思考一个问题:最后一个指向该对象内存的shared_ptr指针是什么时候才会销毁该对象内存空间
1.该shared_ptr指针被析构的时候,也就是该指针的生命周期结束时候;
2.这个shared_ptr指向其他对象的时候;


3 shared_ptr的初始化方式

我们要知道智能指针就是一个模板,本质智能指针是一个对象,之所以叫它为指针,因为该智能指针类里面重载了->运算符,使这个对象能够像指针一样,指向它所需要的内存;


3.1 默认初始化

所以智能指针的初始化方式就是和容器的初始化方式差不多:基本形式如下
shared_ptr<指向的类型>智能指针名
比如:

shared_ptr<string> p; //指向一个类型为string的智能指针p1,

//这种方式为默认初始化,也就是会给智能指针赋值为nullptr;

3.2 配合 new的初始化

//指向一个类型为int类型的智能指针, 并且指向该对象的初始值为10;
shared_ptr<int> p1(new int(10)); 
//指向一个类型为int类型的智能指针, 并且指向该对象的初始值为0;
shared_ptr<int> p2(new int());

我们要知道,指向的对象的初始化方式使由该对象决定的,也就是new 后面的类型加上()里面的方式。


3.3 shared_ptr错误使用方式

shared_ptr<int> p = new int(10); //这种方式使错误的,因为shared_ptr是不支持隐式构造的;
//也就是说: = 号右边是 int* 类型, = 左边是 shared_ptr类型,类型不对应是不行;

对于智能指针作为返回值的方式也是:下面的方式也是不行

shared_ptr<int> fun(int x)
{
	return new int(x); //这种方式也是错误的,原因也是不支持隐式构造,也就是类型不对应
}

起始我们可以用普通的裸指针去初始化智能指针,但是这种方式是不被推荐的,最好不要使用:

int* p = new int();
shared_ptr<int> p1(p);

上面的方式没有错误,但是不建议这么使用


3.4 使用std:: make_shared函数来初始化

其实我们可以使用一个特别的函数模板make_shared的函数去初始化shard_ptr指针,这种方式初始化也是被认为最安全,最高效的一种分配方式.(ps:虽然我不知道高效在哪里,但是看书资料是这么说的)

make_shared返回值是指向该对象的shared_ptr指针。

//类似:shared_ptr<int> p (new int(10));
shared_ptr<int> p = std::make_shared<int>(100);

//类似shared-ptr<string> p(new string(5,'a'));
shared_ptr<string> p = std::make_shared<string>(5,'a');

//还可以结合 auto使用
auto p = std::make_shared<string>(5,'a'); //这种写法比较简洁

make_shared函数的参数,是根据指向对象的初始化方式的参数。


但是,使用make_shared函数来初始化,shared_ptr指针的话,那么该shared_ptr指针就无法自定义删除器了。(至于什么是删除器,后面再讲);


4 shared_ptr引用计数的增加和减少

我们知道shared_ptr是通过引用计数来管理指向对象内存空间的释放的。
那么只要shared_ptr的引用计数为0时候,那么就会自动释放指向该对象的内存空间;


//此时指向int类型的共享指针p1的引用计数为1
shared_ptr<int> p1 = std::make_shared<int>(10); 
//此时指向int类型的共享指针p2的引用计数为2,同时p1的引用计数也是2
auto p2(p1);

可以这么理解:指向同一份对象内存的每个shared_ptr都关联着一个引用计数,当有新的shared_ptr指向该同一份对象内存空间时候,那么所有指向该同一份内存对象的shared_ptr都会增加1个引用计数;
减少也是同理;


shared_ptr作为形参的引用计数理解,此时的形参不是引用的方式

void fun(shared_ptr<int> p ) 
{
	//代码逻辑
	//...
}

int main()
{
	shared_ptr<int> p1(new int(10)); //引用计数有一个
	fun(p1); //当传参给fun函数形参p时候,在fun函数内部,形参还没销毁时候,
			//这个shared_ptr的引用计数就是2,当p出来作用域的时候,
			//那么引用计数就会减1,也就是说形参p是对象生命周期结束了,相当于引用计数没变化
	return 0;
}

所以总的来说,形参是赋值的方式接收实参的话,那么引用计数就是现+1后减1,相当于没变化;


那么也很容易理解另一种情况就是,以shared_ptr引用的方式去接收实参,引用计数就是实实在在的没发生变化;

void fun(shared_ptr<int>& p ) 
{
	//代码逻辑
	//...
}

int main()
{
	shared_ptr<int> p1(new int(10)); //引用计数有一个
	fun(p1); //当传参给fun函数形参p时候,由于是引用的方式,所以引用计数没变,还是1,		
	return 0;
}

还有一种以返回值的方式:shared_ptr做函数的返回值,以值的方式接收返回值:
这种情况:分两种:

第一:当调用该函数时候,没有变量去接受该返回值,那么引用计数不变;其实这个不变,也算是变化的了,因为返回时候,相当于是赋值拷贝一份shared_ptr对象,那么这个赋值过去的share_ptr就会指向同一份对象的内存,此时引用计数就会+1,但是由于没有变量接收返回值,所以引用计数又减1,所以相当于没有变化;
第二:当调用该函数时候,有变量去接收返回值,那么引用计数+1;

//第一种情况:
shared_ptr<int> fun(shared_ptr<int>& p ) 
{
	//代码逻辑
	//...
	return p;
}

int main()
{
	shared_ptr<int> p1(new int(10)); //引用计数有一个
	fun(p1); //没人接收fun函数返回值,引用计数还是1,		
	return 0;
}
//第2种情况:
shared_ptr<int> fun(shared_ptr<int>& p ) 
{
	//代码逻辑
	//...
	return p;
}

int main()
{
	shared_ptr<int> p1(new int(10)); //引用计数有一个
	auto p2 = fun(p1); //有人接收,引用计数还是1,		
	return 0;
}



引用计数减少的情况:
第一:当指向同一份对象的shared_ptr指向另一个对象空间时候,此时引用计数就会减少

shared_ptr<int> p1(new int(10)); //引用计数为1
auto p2(p1); //引用计数为2,p1和p2都是2

p2 = std::make_shared<int>(20); //此时p2的引用计数为1,是因为它指向了新的空间,
								//p1的引用计数为1,是因为p2指向了新的对象空间导致的;

第二:当shared_ptr的指针,离开了作用域后,调用自己的析构函数,此时,引用计数也会减少;

shared_ptr<int> p1(new int(10)); //引用计数为1
auto p2(p1); //引用计数为2,p1和p2都是2

void fun()
{
	shared_ptr p3(p1); //引用计数为3
	shared_ptr p4(p2); //引用计数为4
}
//当p3,p4离开作用域后,引用计数又变为2了

5 shared_ptr常用的成员函数


5.1 use_count成员函数

use_count成员函数使用来统计有多少个shared_ptr指针指向同一份内存空间对象的;

shared_ptr<int> p1(new int(10));
int nums = p1.use_count(); //nums = 1此时有一个引用计数

shared_ptr<int> p2(p1);
int nums = p2.use_count(); //nums = 2此时有一个引用计数

shared_ptr<int> p3(p2);
int nums = p3.use_count(); //nums = 3此时有一个引用计数

有一个细节:就是shared_ptr的对象,用哪个调用use_count函数都是可以的,p1,p2,p3调用use-count都是可以的


5.2 unique成员函数

这个成员函数主要是判断:shared_ptr指针是否只有一个智能指针指向该对象,如果是:返回true,如果不是:返回false;

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

if(p1.unique()) //此时条件成立,因为只有一个引用计数
{
	//输出这个结果
	cout<<"只有一个shared_ptr指针指向同一份内存空间"<<endl;
}else
{
	cout<<"多个shared_ptr指向同一份内存空间"<<endl;
}

shared_ptr<int> p2(p1);

if(p1.unique()) //此时不成立条件成立,因为只有2个引用计数
{
	cout<<"只有一个shared_ptr指针指向同一份内存空间"<<endl;
}else //输出这个结果
{
	cout<<"多个shared_ptr指向同一份内存空间"<<endl;
}


5.3 reset成员函数

reset成员函数就是重置shared_ptr指针的的意思。

reset成员有两个重载版本: 第一个无参数的版本:重置该shared_ptr为空,同时引用计数减一,如果减到0就释放指针指向的内存空间;
第二个有参数的版本:重置shared_ptr指向为该参数的内存空间对象中,并且原来的内存空间对象的引用计数减一,如果减到0那么就释放该内存空间;


无参数的reset函数

shared_ptr<int> p1(new int(10)); //引用计数为1
p1.reset(); //p1指向空,由于引用计数会减一,减了之后变为0,就释放了该内存

shared_ptr<int> p1(new int(10)); //引用计数为1
shared_ptr p2(p1); //引用计数为2
p1.reset(); //p1指向空,由于引用计数会减一,减了之后变为1,p2的引用计数为1,P2指向的内存空间没有被释放

有参数的版本reset函数

shared_ptr<int> p1(new int(10)); //引用计数为1
p1.reset(new int(20)); //p1指向新开辟的对象内存空间,由于引用计数会减一,
						//所以原来P1指向的对象的内存空间被 释放了

shared_ptr<int> p1(new int(10)); //引用计数为1
shared_ptr p2(p1); //引用计数为2
p1.reset(new int(20)); //p1指向新开辟的对象内存空间,由于引用计数会减一,
						//减了之后变为1,p2的引用计数为1,P2指向的内存空间没有被释放

6 指定删除器和指向数组的问题

C++的智能指针初始化的第二个参数,可以指定自定义的删除器,其实这个删除器就是一个函数指针,并且是单参数的函数指针,当然,你也可以传lambda表达式。
如果不指定第二个初始化的参数,那么就是使用默认的删除器,也就是直接delete的版本;


为什么要指定自己的删除器呢?
因为智能指针在管理数组指针时候,需要释放数组的内存,假如使用默认的删除器,也就是直接delete,那么就会导致内存泄漏了,所以需要自己指定自己删除器,去释放数组内存;


class A
{
public:
	A()
	{
		cout<<"A()构造函数执行"<<endl;
	}
	~A()
	{
		cout<<"A()析构函数执行"<<endl;
	}
};

int main()
{
	//shared_ptr<A> p(new A[10]); //试图开辟10个A类的数组空间,用智能指针P去指向它;
								//但是这会报错,报错原因就是默认删除器使用的delete p,
								//这样只能析构一个数组元素,剩下的9个没有析构成功
								//而我们需要的是delete[]p的方式释放内存,所以要自己指定删除器
	
	shared_ptr<A> p(new A[10],[](A* p){
	 delete[] p;}); //用lambda表达式指定删除器
	 				//这样就可以释放干净数组的内存了
	 				
	//其实,删除器还有一种是C++ 标准库提供的类模板std::default_delete
	//这种方式也可以用来删除数组
	shared_ptr<A> p2(new A[10], std::default_delete<A[]>());
	return 0;
}

在C++17提供了一种更加方便的方式来管理数组的,但是这种在C++11 和14都是不支持的,所以可能老的编译器会报错.

只在<>尖括号 和()小括号里面的类型都加上[ ]中括号即可。

shared_ptr<A[]> p(new A[10]); //c++17就开始支持这种写法来管理数组

7 shared_ptr带来的循环引用问题–weak_ptr解决方案

什么是循环引用?什么又是weak_ptr;

什么是weak_ptr指针
1.首先我们得知道weak_ptr:是一种辅助shared_ptr的智能指针;
也就是说,weak_ptr本身是不可以被单独使用的
不可以被单独使用的意思:weak_ptr<int> p(new int(10)) 这种方式是不可以创建weak_ptr对象的,这是错误的用法;
2.weak_ptr的对象只能指向一个由shared_ptr创建的对象,但是weak_ptr是不管理shared_ptr指针指向的对象内存的空间生存周期的;这个weak_ptr是不会增加shared_ptr的引用计数的。
也就是说shared_ptr所指向的对象该释放空间就释放空间,和weak_ptr没有关系,尽管weak_ptr还是指向该对象的内存空间,只要shared_ptr的引用计数为0,那么就会释放该对象内存空间;


我们知道weak_ptr就是用来辅助shared_ptr使用的,那么是如何辅助呢?
首先我们得认识什么是循环引用得问题。
那么我们现来设一个场景类:一个人类,有一辆车;一个车类,需要有一个人;

在People类设计一个成员变量 shared_ptr<Car>类型的指针;
在Car类设计一个成员变量 shared_ptr<People>类型的指针;

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

class Car; //前置声明,使得People类里面认识Car类
class People
{
public:
	shared_ptr<Car> _car;
	People()
	{
		cout << "People的构造函数执行" << endl;
	}
	~People()
	{
		cout << "People的析构函数执行" << endl;
	}
};
class Car
{	
public:
	shared_ptr<People> _people;
	Car()
	{
		cout << "car的构造函数执行" << endl;
	}
	~Car()
	{
		cout << "car的析构函数执行" << endl;
	}

};
void test()
{
	shared_ptr<People> people(new People()); //开辟 People的堆空间

	shared_ptr<Car> car(new Car()); //开辟 Car的堆空间
	//再让类里的成员变量互相指向对方的shared_ptr指针
	people->_car = car;  //这会使得指向 car 对象的shared_ptr有2个引用计数
	car->_people = people;//这会使得指向 People 对象的shared_ptr有2个引用计数
}
int main()
{
	test();
	return 0;
}

一旦我调用上面的test函数,你猜会输出什么结果?是否由正常的两次构造函数,和两次析构函数的调用呢?
在这里插入图片描述
很明显,当我执行这行代码的时候,并没有显示正确的两次析构函数,也就是说,这段代码出现了一个很严重的问题,那就是内存泄漏了。


这个也是循环引用带来的问题,导致内存泄漏,那么我们总结以下什么是循环引用呢?
也就是shared_ptr管理资源内存时候,互相指向的问题,你的shared_ptr指向我的sahred_ptr,我的shared_ptr又指向你的shared_ptr;
画个图更好理解上面的代码
在这里插入图片描述
我们能够很清晰的看到,这里有一个循环的圈子在相互引用这,当我们的people和car 的共享指针声明周期结束时候,也就是在栈空间销毁时候,就会导致共享指针的引用计数减1,但是我们发现仅仅是减1,没有减到0,在堆空间中还是有成员变量的共享指针相互指向对方,这就导致了对象的空间没有被释放的问题;导致了析构函数无法被执行;
在这里插入图片描述


那么我们如何解决这个问题呢?
其实很好解决,只要通过weak_ptr来解决即可,只要在任意一个类中把shared_ptr换成weak——ptr就可以解决循环引用带的问题了,其他代码都不需要变动。

比如我在People类中修改了shared_ptr<Car> _carweak_ptr<Car> _car,当然你也可以在Car类修改,只要修改其中一个就可以了;

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

class Car; //前置声明,使得People类里面认识Car类
class People
{
public:
	weak_ptr<Car> _car; //修改了成了weak_ptr
	People()
	{
		cout << "People的构造函数执行" << endl;
	}
	~People()
	{
		cout << "People的析构函数执行" << endl;
	}
};
class Car
{	
public:
	shared_ptr<People> _people;
	Car()
	{
		cout << "car的构造函数执行" << endl;
	}
	~Car()
	{
		cout << "car的析构函数执行" << endl;
	}

};
void test()
{
	shared_ptr<People> people(new People()); //开辟 People的堆空间

	shared_ptr<Car> car(new Car()); //开辟 Car的堆空间
	//再让类里的成员变量互相指向对方的shared_ptr指针
	people->_car = car;  //这里并不会使得new 的car对象引用计数变为2,依旧是1,因为这是个weak_ptr指针
	car->_people = people;//这会使得指向 People 对象的shared_ptr有2个引用计数
}
int main()
{
	test();
	return 0;
}

查看结果:达到我们的预期,成功释放内存
在这里插入图片描述


那么原理是什么呢? 原理很简单,画个图就明白了
在这里插入图片描述
一旦栈空间的car共享指针离开作用域,那么就会就会释放 new Car对象,因为new Car的共享指针car只有1个引用计数,那么new Car对象就会调用自己的析构函数,一旦调用自己的析构函数,那么就导致new Car对象里面成员变量_people共享指针的引用计数少1,由于在栈空间people的共享指针也离开作用域,那么也就是说new People的共享指针引用计数也会少1,如此一来,由原来的两个引用计数变成0,那么就会释放 new People的空间了.


8 unique_ptr的基本使用

unique_ptr指针就是一种独占式的指针,也就是说,执行一个对象内存时候,只能有一个unique指针指向,不可以有多个;


所以说:基本没什么区别和shared_ptr的用法,那我们只要来分析一些常见的错误即可;

不可以拷贝构造;
不可以赋值初始化;
不可以赋值拷贝;

unique_ptr<string> p1(new string("hello world!"));

//以下三种赋值方式都不行,因为这是一个独占式指针,只能有一个指针指向该对象string的内存单元;
unique_ptr<string> p2(p1);
unique_ptr<string> p3 = p1;
unque_ptr<string> p4;
p4 = p1;

C++ 14还提供一种使用make_unique的函数模板进行初始化unique_ptr指针的,但是C++11是不支持这种写法的

unique_ptr<int> p = std::make_unique<int>(100);

9 智能指针选取问题

假如程序中要使用多个指针指向同一个对象,选用shared_ptr;
假如程序中要使用单个指针指向同一个对象,选用unique_ptr;


标签:p1,C++,用法,计数,引用,shared,ptr,指针
来源: https://blog.csdn.net/m0_46606290/article/details/122807001

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有