首页 > 编程语言 >C++ 智能指针概述

C++ 智能指针概述

时间:2023-09-21 23:33:00浏览次数:68  
标签:parent C++ 概述 内存 child shared ptr 指针

原始指针

要想了解智能指针,就需要首先了解原始指针的痛点,原始指针有几点问题

  1. 忘记释放内存 -> 产生内存泄漏
  2. 在尚有指针引用内存的情况下释放内存(使用已经释放掉的对象) -> 产生引用非法内存的指针
  3. 同一块内存释放2次

智能指针的产生本质上都是为了解决这些问题

关于使用new动态分配对象的初始化问题,目前实际编码的结果和书籍内容存在冲突,《C++ Primer》p407 指出int *pi = new int中pi指向一个未初始化的int,其值是未定义,但是从实际编码结果来看,其遵循默认初始化的结果并非未定义,和int a表现出的并不相同(红色标注的位置即为和理解冲突的位置)

智能指针说明

  1. shared_ptr:允许多个指针指向同一个对象
  2. unique_ptr:独占所指向的对象
  3. weak_ptr:伴随类,一种弱引用,指向shared_ptr管理的对象

智能指针初始化

  1. 默认初始化的智能指针中保存一个空指针

unique_ptr

首先unique_ptr作为智能指针,是在原始指针基础上出现的。原始指针容易出现内存泄漏问题,智能指针利用RAII方法来解决这一问题。

unique_ptr为了避免重复释放(double free)问题出现,禁止拷贝(删除了拷贝构造函数) 这样的特性带来的问题是无法进行参数传递(因为参数传递需要拷贝构造函数),有两种解决方法 想要解决这个问题,首先需要理解为什么unique_ptr会禁止拷贝,对一个指针进行拷贝,两个指针指向相同的内存空间,若释放了一个指针,那么会导致另一个指针空悬。

上述问题出现的原因可以总结为:一份资源的多个备份,在生命周期发生变更时,无法保证数据一致性 如果说生命周期不发生改变,那么多个备份也是没有问题的,因此在确保函数无需接管资源生命周期的控制权时,可以通过get()方法获取到原始指针,完成函数参数传递 如果生命周期可能发生变更,那么需要确保资源始终只有一份,那么参数传递可以通过移动语义完成

shared_ptr

#include <memory>
std::shared_ptr<typename> object;

unique_ptr虽然解决了原始指针资源易忘记释放的问题,但是为了解决重复释放问题,采用了禁止拷贝的方式,在实际应用场景中较难使用 因此出现了采用引用计数方案的(类似软链接的实现方式,共享Inode,当计数为0时才会真正释放资源)shared_ptr,记录有多少个shared_ptr指向相同对象,在引用计数变为0后,自动释放对象

shared_ptr构造函数是explicit的,因此无法进行隐式类型转换式的初始化,必须通过直接初始化,以下代码展示了这一点

shared_ptr<int> p1 = new int(1024); // 错误,由于explicit的存在,无法进行隐式类型转换式的初始化
shared_ptr<int> p2(new int(1024));  // 正确,可以进行直接初始化

关于shared_ptr和原始指针 智能指针和原始指针是可以混合使用的,但由于智能指针的生命周期控制权在于类,而原始指针的控制权在于开发者,当两者混合后,可能会因为生命周期产生一些问题,以以下代码为例进行说明

void process(shared_ptr<int> ptr) {}

int *x(new int(1024));       // x是一个普通指针
process(shared_prt<int>(x)); // 正确,可以用原始指针初始化智能指针
int j = *x;                  // 未定义,x为空悬指针

shared_ptr利用引用计数对指针生命周期进行控制,直接将shared_ptr指针作为实参和用原始指针初始化得到的shared_ptr作为实参的区别在于引用计数不同,前者引用计数为2,后者仅为1。process函数返回后引用计数分别变为1和0,前者没有问题,后者由于引用计数变为0会将内存释放,原始指针变为空悬指针,解引用操作为未定义行为。

总结来说,由于无法得知智能指针负责的对象何时会释放,因此使用原始指针访问智能指针负责对象是危险行为

一个智能指针是可以获取到一个对应的原始指针的,二者指向相同的内存地址,此时就变为了上述智能指针和原始指针混用的情况,需要注意生命周期问题。

c++中除了shared_pointer和weak_pointer是浅拷贝,unique_pointer禁止拷贝,其他均为深拷贝

weak_ptr

weak_ptr是shared_ptr的弱化版本,其不改变引用计数

shared_ptr虽然利用引用计数解决了unique_ptr实际应用较困难的问题,但是恰恰是这个机制也为其带来了一些问题。 若智能指针作为类的成员变量,并且类之间存在一定的从属关系,此时可能会出现循环引用的问题,导致最终资源不会被释放,以以下代码为例

#include <memory>

struct C {
    std::shared_ptr<C> m_child;
    std::shared_ptr<C> m_parent;
};

int main() {
    std::shared_ptr<C> parent = std::make_shared<C>();
    std::shared_ptr<C> child = std::make_shared<C>();

    parent->m_child = child;
    child->m_parent = parent;

    parent = nullptr;
    child = nullptr;

    return 0;
}

以上代码实现的从属关系如下图所示, 从上图可以看出,parent和child两片内存区域最初的引用计数均为2 parent = nullptr产生的效果如下图所示,这里要明白parent就是一个指针,赋值为nullptr只是让这个指针不再指向原来的内存区域。只有当这个指针是最后一个指向这片内存区域的指针时,修改其指向才会释放内存区域,由于此时除了parent的指针外,还存在其他指针指向parent对应的内存区域,因此此时内存并不会释放。parent和child内存区域的引用计数分别变为1和2(由于parent指针已经释放,故无法通过该指针获取引用计数值,可以额外创建一个parent指针副本来获取引用计数) child = nullptr产生的效果如下图所示,最终parent和child对应内存区域的引用计数分别为1和1,因此对应的内存区域并不会释放。这里比较难理解的是m_parentm_child是处在结构体当中的,但是parentchild都被赋值为nullptr了,为什么结构体仍然存在。这里parent和child只是一个指向结构体的指针变量,指针变量为nullptr并不代表结构体所占的内存就被释放了,只有当没有指针指向内存区域时,才会释放对应的内存区域。 因此最终的结果是,parent和child对应的内存区域均没有释放

产生上述问题,是由于从属关系混乱导致的,shared_ptr会增加引用计数,这个指针只要存在,指向的资源就不会被释放,类似拥有这个资源的含义,拥有它的所有权。 在child类中,由于m_parentshared_ptr指针,这个意思就相当于child拥有parent,显然这是不正确,因此一种解决方式就是把类中m_parent修改为weak_ptr

#include <memory>

struct C {
    std::shared_ptr<C> m_child;
    std::weak_ptr<C> m_parent;
};

int main() {
    std::shared_ptr<C> parent = std::make_shared<C>();
    std::shared_ptr<C> child = std::make_shared<C>();

    parent->m_child = child;
    child->m_parent = parent;

    parent = nullptr;
    child = nullptr;

    return 0;
}

此时,代码呈现出的逻辑效果如下图所示,图中实线箭头表示shared_ptr,虚线箭头表示weak_ptr,注意weak_ptr并不影响引用计数。 此时parent和child对应的内存引用计数分别为1和2 parent = nullptr之后,由于parent对应内存引用计数变为0,因此此内存区域被释放,parent->m_child指针也被销毁,因此child对应内存区域引用计数变为1。对应结构如下图所示 child = nullptr之,由于此时child指针是最后一个指向child内存区域的指针,当其不再指向这篇内存区域后,引用计数变为0,同时内存区域被释放,效果如下图所示

四种指针之间的关系

原始指针和unique_ptr:原始指针可理解为unique_ptr的弱引用 weak_ptr和shared_ptr:weak_ptr是shared_ptr的弱引用 由于智能指针会自行进行资源回收,因此访问弱引用时指针可能已经失效,weak_ptr相较于原始指针提供了失效检测功能,当指向内存空间被释放时,不会出现访问错误

具体是用哪种组合取决于实际的应用场景,在讲述shared_ptr循环引用例子中的parentchild场景描述并不是非常清晰,假设一个parent可能有多个child,那么应当使用shared_ptrweak_ptr的组合,如果一个parent只能有一个child,那么应当使用unique_ptr和原始指针的组合。

智能指针常用函数和类方法

  1. shared_ptr
functions meaning
shared_ptr<T> sp null intelligence pointer, which can point to a T type object
make_shared<T>(args) use args to initialize a shared_ptr and return it
shared_ptr<T> p(q) p is the copy of shared_ptr q, this operation will increate the counter of q
p = q decrease the reference count of original object of p, increase it of q
p use p for conditional judgement, if p refers to an object, the result is true
*p dereference p, get the object which is refered by p
p->mem equal to (*p).mem
p.get() get origin pointer
swap(p, q) / p.swap(q) exchange the pointer of p and q
p.use_count() get the reference count of intelligence pointer p
p.unique() if the reference count of p equals 1, it returns true, otherwise false
  1. unique_ptr
functions meaning
unique_ptr<T> up null intelligence pointer, which can point to a T type object
p = q decrease the reference count of original object of p, increase it of q
p use p for conditional judgement, if p refers to an object, the result is true
*p dereference p, get the object which is refered by p
p->mem equal to (*p).mem
p.get() get origin pointer
swap(p, q) / p.swap(q) exchange the pointer of p and q
u = nullptr release the object pointed by u and set u to be nullptr
u.release() u gives up the control power to pointer, return the pointer and set u to be nullptr
u.reset(p) if p is nullptr, release the object pointed by u, otherwise release the original object and set u point to p

How does unique_ptr control to only point to one object ? To be precise, one object can be pointed by multiple unique_ptr, but it will cause some errors, so we say unique_ptr is not allowed to point to multiple objects. The reason is that unique_ptr doesn't use the count to memorize the object number which is pointed by intelligence pointer, so if code leave the action scope, the unique_ptr will release the source, if one object is pointed by multiple unique_ptr, it will cause repeated release, so we say unique_ptr can't point to multiple objects.

A falliable point In my previous understanding, a null pointer, which type is MyClass, can't call functions of MyClass. If we do it, we will get a Segmentation fault error. But when a unique_ptr which value is nullptr, it can also call the get() function, and the result is also nullptr.

To be honest, I don't know its principle now.

  1. weak_ptr
functions meaning
weak_ptr<T> w null intelligence pointer, which can point to a T type object
weak_ptr<T> w(sp) w is a weak_ptr which points to a object that is same as shared_ptr sp
w = p p is a shared_ptr or a weak_ptr
w.reset() set w to be nullptr
w.use_count() return the count of shared_ptr which shares the same object with w
w.expired() if w.use_count() equals 0, it will return true, otherwise return false
w.lock() if w.expired() return true, return a null shared_ptr, otherwise return a shared_ptr which points to w

Reference

标签:parent,C++,概述,内存,child,shared,ptr,指针
From: https://blog.51cto.com/u_14882565/7558486

相关文章

  • 双指针法、滑动窗口法、螺旋矩阵
    1.双指针法解有序数组的平方1.1题目要求LeetCode977有序数组的平方题目内容:给你一个按非递减顺序排序的整数数组nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。示例1:输入:nums=[-4,-1,0,3,10]输出:[0,1,9,16,100]解释:平方后,数组变为[16,1,0,9,100]排序......
  • [算法学习笔记] 浅谈二路归并&双指针&归并排序
    二路归并·双指针是一种优化思想。它可以在\(O(n)\)的复杂度下把两个长度为\(n\)的有序数组合并为一个有序数组。它的具体处理方法如下:定义两个长度为\(n\)的升序数组\(a,b\)。,合并完后长度为\(2n\)的数组\(c\),初始化两个指针\(x=y=1\)(这里数组下标从\(1\)开始)......
  • C++系列十:日常学习-进程间通讯
    目录前言介绍照片:后续:前言V~~~V。介绍进程间通讯(Inter-ProcessCommunication,IPC)是操作系统中的一个重要概念,用于不同进程之间的数据传输和交互。有多种方式可以实现进程间通讯,以下是其中一些常见的方式:管道(Pipe):管道是一种单向通信方式,通常用于具有父子关系的进程之间。它分......
  • C++-内存管理
    今天,和大家分享一些与内存管理相关的知识,本次的内容主要是new和delete的使用。内存这一块的知识,我们在学习C语言的时候,就有作相对细致的了解。我们现在来写几道题。做一个简单的回顾复习。内存的分布我们先来看看,下面一段代码:intglobalVar=1;staticintstaticGlobalVar=1;v......
  • C++中文开发【笑】
    娱乐一下,切勿上纲上线。你会不会还在为代码中众多英文单词感到苦恼。现在只需要引入一个库,你就可以进行C++真·中文开发。示例代码:#include"chinesecpp.h"使用命名空间std;整型划分数组(整型指针数组,整型左下标,整型右下标){ 整型主元位置=(左下标+右下标)......
  • C++ RAII在HotSpot VM中的重要应用
    RAII(ResourceAcquisitionIsInitialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII的做法是使用一个对象,在其构造时获取资源,在对象生命期控制范围之下......
  • 湖南大学个人项目C++互评
    优点模块化设计:代码有一个良好的模块化设计,其中每个类和函数都有一个特定的目的。可扩展性:由于使用了继承和多态,该设计易于扩展。例如,添加新类型的问题生成器相对简单。用户交互:代码包含用户交互,允许用户登录并选择问题类型和数量。文件操作:代码成功地将生成......
  • C++ RAII在HotSpot VM中的重要应用
    RAII(ResourceAcquisitionIsInitialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII的做法是使用一个对象,在其构造时获取资源,在对象生命期控制范围之下......
  • c++并发编程实战-第4章 并发操作的同步
    等待事件或等待其他条件坐车案例想象一种情况:假设晚上坐车外出,如何才能确保不坐过站又能使自己最轻松?方法一:不睡觉,时刻关注自己的位置1#include<iostream>2#include<thread>3#include<mutex>4usingnamespacestd;56mutex_mtx;7boolbFlag=false;......
  • 《探索C++多线程》:condition_variable源码(一)
    https://blog.csdn.net/hujingshuang/article/details/70596630    现在接着学习关于多线程编程的特征,在这一节,将会了解到多线程中的condition_variable(条件变量)的相关知识。     在头文件<condition_variable>中有两种条件变量的类声明与定义:condition_varia......