C/C++教程

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

本文主要是介绍对C++中的智能指针的理解和基本用法总结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 1 智能指针的概述
  • 2 shared_ptr基础理解
  • 3 shared_ptr的初始化方式
    • 3.1 默认初始化
    • 3.2 配合 new的初始化
    • 3.3 shared_ptr错误使用方式
    • 3.4 使用std:: make_shared函数来初始化
  • 4 shared_ptr引用计数的增加和减少
  • 5 shared_ptr常用的成员函数
    • 5.1 use_count成员函数
    • 5.2 unique成员函数
    • 5.3 reset成员函数
  • 6 指定删除器和指向数组的问题
  • 7 shared_ptr带来的循环引用问题--weak_ptr解决方案
  • 8 unique_ptr的基本使用
  • 9 智能指针选取问题

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;


这篇关于对C++中的智能指针的理解和基本用法总结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!