广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >一篇文章带你了解C++智能指针详解
  • 648
分享到

一篇文章带你了解C++智能指针详解

2024-04-02 19:04:59 648人浏览 八月长安
摘要

目录为什么要有智能指针?智能指针的使用及原理RALLshared_ptr的使用注意事项创建多个 shared_ptr 不能拥有同一个对象shared_ptr 的销毁shared_pt

为什么要有智能指针?

因为普通的指针存在以下几个问题:

  • 资源泄露
  • 野指针
    •  未初始化
    • 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道
  • 异常安全问题
  • 如果在 malloc和free 或者 new和delete 之间如果存在抛异常,那么也会导致内存泄漏。

资源泄漏示例代码:


int main(){
	int *p = new int;
	*p = 1;
	p = new int; // 未释放之前申请的资源,导致内存泄漏 
	delete p;
	
	return 0;
}

野指针示例代码:


int main(){
	int *p1 = new int;
	int *p2 = p1;
	delete p1; 
	*p2 = 1; // 申请的内存已经被释放掉了,  
	
	return 0;
}

int main(){
	int *p;
	*p = 1; // 程序直接报错, 使用了未初始化的变量
	return 0;
}

解决方法:智能指针

智能指针的使用及原理

  • 具有RALL 特性
  • 重载了 operator* 和 operator ->,使其具有了指针一样的行为

RALL

RALL(Resource AcquIstion Is Initialization)是一种利用对象生命周期来控制程序资源(如内存,文件句柄,网络连接,互斥量等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。相当于利用 对象 管理了一份资源。这样的优势在于

1.不需要显式的释放资源(对象析构时,自动释放资源)

2.采用这种方式,对象所需的资源在其生命周期内始终保持有效。

智能指针就是一个实例出来的对象

c++98版本的库中就提供了auto_ptr的智能指针。但是 auto_ptr存在当对象拷贝或者赋值之后,前面的对象就悬空了。

C++11 提供更靠谱的并且支持拷贝的 shared_ptr

shared_ptr :

通过引用计数的方式实现多个shared_ptr 对象之间共享资源。

shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

unique_ptr :

确保一个对象同一时刻只能被一个智能指针引用,可以转移所有权(可以从一个智能指针转移到另一个智能指针)

auto_ptr :

C++11 已弃用, 与unique_ptr 类似

使用时,需包含头文件


 #include <memory>

shared_ptr的使用注意事项

创建


1. 
shared_ptr<int> ptr{new int(3)};
2.
shared_ptr<int> ptr;
ptr.reset(new int(3));
3.
shared_ptr<int> ptr = make_shared<int>(3);

shared_ptr 支持使用比较运算符,使用时,会调用共享指针内部封装的原始指针的比较运算符。

支持

==、!=、<、<=、>、>=

使用 比较运算符 的前提 必须是 同类型

示例:


shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<int> p2 = make_shared<int>(2);
shared_ptr<int> p3;
shared_ptr<double> p4 = make_shared<double>(1);

bool b1 = p1 < p2; 		// true
bool b2 = p1 > p3;		// true, 非NULL 指针与 NULL 指针相比 ,都是大于
bool b3 = p3 == p3;		// true
bool b4 = p4 < p2		// 编译失败,类型不一致

shared_ptr 可以使用强制类型转换,但是不能使用普通的强制类型转换符

1.shared_ptr 强制类型转换符 允许将其中包含的指针强制转换为其它类型

2.不能使用普通的强制类型转换运算符,否则会导致未定义行为

3.shared_ptr 的强制类型转换运算符包括
static_pointer_cast
dynamic_pointer_cast
const_pointer_cast

示例:


shared_ptr<void> p(new int);	// 内部保留 void* 指针
static_pointer_cast<int*>(p);	// 正确的 强制类型转换方式
shared_ptr<int> p1(static_cast<int*>(p.get()));	// 错误的强制类型转换方式,未定义错误

多个 shared_ptr 不能拥有同一个对象

利用代码理解

示例:


class Mytest{
public:
	Mytest(const string& str)
	:_str(str){}
	~Mytest(){
		std::cout << _str << "destory" << std::endl;
	}
private:
	string _str;
};

int main(){
	Mytest* p = new Mytest("shared_test");
	shared_ptr<Mytest> p1(p); 	// 该对象可以正常析构
	shared_ptr<Mytest> p2(p); // 对象销毁时,错误,读取位置 0xDDDDDDDD 时发生访问冲突。
	return 0;
}

上述代码, 共享指针 p1 对象在程序 结束时,调用析构,释放了p 所指向的空间, 当 p2 进行析构的时候,又释放p所指向的空间, 但是由于已经释放过了, 重复释放已经释放过的内存,导致段错误。

可以使用 shared_from_this 避免这种问题

改进代码:


class Mytest:public enable_shared_from_this<Mytest> {
public:
    Mytest(const string& str)
        :_str(str) {}
    ~Mytest() {
        std::cout << _str << "destory" << std::endl;
    }
    shared_ptr<Mytest> GetSharedptr() {
        return shared_from_this();
    }
private:
    string _str;
};
int main() {
    Mytest* p = new Mytest("shared_test");
    shared_ptr<Mytest> p1(p);
    shared_ptr<Mytest> p2 = p->GetSharedptr(); // 正确做法
    
    return 0;
}

shared_ptr 的销毁

shared_ptr 在初始化的时候,可以定义删除器,删除器可以定义为 普通函数、匿名函数、函数指针等符合要求的可调用对象

示例代码:


void delFun(string* p) {
    std::cout << "Fun delete " << *p << endl;
    delete p;
}
int main() {

    std::cout << "begin" << std::endl;
    shared_ptr<string> p1;
    {
        shared_ptr<string> p2(new string("p1"), [](string* p) {
            std::cout << "Lamda delete " << *p << std::endl;
            delete p;
        });
        p1 = p2;
        shared_ptr<string> p3(new string("p3"), delFun);
    }
    std::cout << "end" << std::endl;
    return 0;
}

执行结果:

begin
Fun delete p3
end
Lamda deletep1

分析结果:

首先 ,p3在{ }作用域内 ,生命周期最先结束,调用delFun作为删除器

其次,p2 也在{ } 作用域内,生命周期也结束了,但是因为 p1 和 p2 指向了同一个对象,所以p2 销毁只是将其 对象 引用计数 -1。

最后,程序运行结束,p1销毁,其对象引用计数-1 变为0,调用 删除器,销毁对象。


shared_ptr<char> p(new char[10]); // 编译能够通过,但是会造成资源泄漏
// 正确做法
shared_ptr<char> p(new char[10], [](char* p){
	delete p[];
	});
// 正确做法
shared_ptr<char> p(new char[10], default_delete<char[]>());
  • 可以为数组创建一个shared_ptr ,但是这样会造成资源泄露。因为 shared_ptr 提供默认的删除调用的是 delete,而不是 delete[]
  • 可以使用自定义删除器,删除器中使用 delete[]
  • 可以使用 default_delete 作为删除器,因为它使用 delete[]

shared_ptr 存在的问题:

1.循环引用
不同对象相互引用,形成环路

2.想要共享但是不想拥有对象

shared_ptr 的线程安全问题

1. shared_ptr 对象中引用计数是多个shared_ptr对象共享的,两个线程中shared_ptr的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2 这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加的,也就是说引用计数的操作是线程安全的。

2.shared_ptr 管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。


// 1.因为线程安全问题是偶现性问题,main函数的n改大一些概率就变大了,就
容易出现了。
void SharePtrFunc(shared_ptr<Date>& sp, size_t n)
{
	cout << sp.Get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
		copy->_year++;
		copy->_month++;
		copy->_day++;
	}
}
int main()
{
	shared_ptr<Date> p(new Date);
	cout << p.Get() << endl;
	const size_t n = 100;
	thread t1(SharePtrFunc, p, n);
	thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	return 0;
}

shared_ptr 的循环引用


struct Listnode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}

循环引用代码分析:

node1和node2两个智能指针对象指向两个节点,引用计数变成1,不需要手动delete。

node1的_next指向node2,node2的_prev指向node1,引用计数变成2。

node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

也就是说_next析构了,node2就释放了。

也就是说_prev析构了,node1就释放了。

但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

在这里插入图片描述

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

原理:

node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加

node1和node2的引用计数。


struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

unique_ptr

  • 同一个对象,只能有唯一的一个 unique_ptr 指向它
  • 继承了自动指针 auto_ptr,
  • 有助于避免发生异常时导致的资源泄漏

unique_ptr的使用

unique_ptr 定义了*、-> 运算符,没有定义 ++ 之类的指针算法

unique_ptr 不允许使用赋值语法进行初始化,必须使用普通指针直接初始化

unique_ptr 可以为 空

unique_ptr 不能使用普通的复制语义赋值, 可以使用 C++11 的 move() 函数

unique_ptr 获得新对象时,会销毁之前的对象

unique_ptr 防止拷贝的原理:


// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
// C++11防拷贝的方式:delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注编程网的更多内容!

--结束END--

本文标题: 一篇文章带你了解C++智能指针详解

本文链接: https://www.lsjlt.com/news/132524.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 一篇文章带你了解C++智能指针详解
    目录为什么要有智能指针?智能指针的使用及原理RALLshared_ptr的使用注意事项创建多个 shared_ptr 不能拥有同一个对象shared_ptr 的销毁shared_pt...
    99+
    2022-11-12
  • 一篇文章带你了解C语言指针进阶
    目录1.字符指针2.指针数组3.数组指针4.函数指针5.数组传参总结1.字符指针 我们已经知道了数组名在大部分时候表示数组的地址,指针本质上也表示一个地址,那么我们能否用指针来创建一...
    99+
    2022-11-12
  • 一篇文章带你了解C++模板编程详解
    目录模板初阶泛型编程函数模板函数模板概念函数模板格式函数模板的原理函数模板的实例化模板参数的匹配原则类模板类模板的定义格式类模板的实例化总结模板初阶 泛型编程 在计算机程序设计领域...
    99+
    2022-11-12
  • 一篇文章带你了解C++(STL基础、Vector)
    目录STL基本概念STL六大组件STL中容器、算法、迭代器容器算法迭代器初识Vector容器Vector三大遍历算法Vector存放其他数据类型 Vector容器嵌套总结S...
    99+
    2022-11-12
  • 一篇文章带你了解C++的KMP算法
    目录KMP算法步骤1:先计算子串中的前后缀数组NextC++代码:步骤2:查找子串在母串中出现的位置。总结KMP算法 KMP算法作用:字符串匹配 例如母串S = “aaagoogle...
    99+
    2022-11-12
  • 一篇文章带你了解C++中的异常
    目录异常抛出异常基本操作自定义的异常类栈解旋异常接口声明异常变量的生命周期异常的多态c++的标准异常库编写自己的异常类总结异常 在c语言中,对错误的处理总是两种方法: 1,使用整型的...
    99+
    2022-11-13
  • 一篇文章带你详细了解JavaScript数组
    目录一、数组的作用:二、数组的定义:1.通过构造函数创建数组2.通过字面量的方式创建数组三、数组元素四、数组长度五、数组索引(下标)六、数组注意的问题1.数组中存储的数据可以是不一样...
    99+
    2022-11-12
  • 一篇文章带你了解C/C++的回调函数
    目录函数指针概念先来看一个Hello World程序然后,采用函数调用的形式来实现用函数指针的方式来实现函数指针数组回调函数概念标准Hello World程序将它修改成函数回调样式修...
    99+
    2022-11-13
  • 一篇文章带你了解C语言操作符
    目录一、操作符分类 二、算术操作符三、移位操作符1、左移操作符 2、右移操作符2.1算术移位 2.2逻辑移位 四、位操作符 1、按位...
    99+
    2022-11-12
  • 一篇文章带你了解c++运算符重载
    目录友元函数重载:复合赋值Operator pairings自增自减运算符的重载c++20,spaceship operator总结友元函数 一种全局函数,可以在类里声明,其他地方定...
    99+
    2022-11-12
  • 一篇文章带你了解初始Spring
    目录为什么要使用SpringSpring概述Spring容器使用流程1.启动容器2.完成bean的初始化3.注册bean到容器中4.装配bean的属性bean的注册bean属性注入总...
    99+
    2022-11-12
  • 一篇文章带你了解Java SpringBoot Nacos
    目录1、什么是Nacos 1.1与eureka对比1.2与zookeeper对比1.3与springcloud config 对比 2、Spring Cloud Alibaba 套件...
    99+
    2022-11-12
  • 一篇文章带你了解Java Stream流
    目录一、Stream流引入现有一个需求:1.用常规方法解决需求2.用Stream流操作集合,获取流,过滤操作,打印输出二、Stream流的格式三、获取流四、Stream流的常用方法方...
    99+
    2022-11-12
  • 一篇文章带你了解JavaScript-对象
    目录创建对象对象直接量通过new创建对象原型Object.create()属性的查询和设置继承属性访问错误删除属性检测属性序列化对象总结创建对象 对象直接量 对象直接量是由若干名/值...
    99+
    2022-11-12
  • 一篇文章带你了解JavaScript-语句
    目录表达式语句复合语句和空语句复合语句空语句声明语句varfunction条件语句ifif/elseelse ifswitch循环whiledo/whileforfor/in跳转标签...
    99+
    2022-11-12
  • 一篇文章带你了解XGBoost算法
    目录1. 什么是XGBoost1.1 XGBoost树的定义1.2 正则项:树的复杂度1.3 树该怎么长1.4 如何停止树的循环生成2. XGBoost与GBDT有什么不同3. 为什...
    99+
    2022-11-12
  • 一篇文章带你了解jQuery动画
    目录1.控制元素的显示与隐藏 show() hide()2.控制元素的透明度 fadeIn() fadeOut()3:控制元素的高度 slideUp() slideDown()总结 ...
    99+
    2022-11-12
  • 一篇文章带你了解vue路由
    目录概念Vue Router简介Vue Router的特性Vue Router的使用步骤分类嵌套路由动态路由命名路由编程式导航总结概念 路由的本质就是一种对应关系,比如说我们在url...
    99+
    2022-11-13
  • 一篇文章带你了解SQL之CASE WHEN用法详解
    目录简单CASE WHEN函数: 等同于,使用CASE WHEN条件表达式函数实现: THEN后边的值与ELSE后边的值类型应一致,否则会报错。如下:总结简单CA...
    99+
    2022-11-12
  • 一篇文章带你详解Spring的AOP
    目录1、AOP 什么?2、需求3、解决办法1:使用静态代理第一步:创建 UserService 接口第二步:创建 UserService的实现类第三步:创建事务类 MyTransac...
    99+
    2022-11-13
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作