文章首发

【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针

前言

大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针。

为什么需要智能指针

在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内存泄漏。

我们再给两段代码,切身体验下原生指针管理内存的噩梦。

void foo(int n) {
int* ptr = new int(42);
...
if (n > 5) {
return;
}
...
delete ptr;
} void other_fn(int* ptr) {
...
};
void bar() {
int* ptr = new int(42);
other_fn(ptr);
// ptr == ?
}

foo函数中,如果入参n > 5, 则会导致指针ptr的内存未被正确释放,从而导致内存泄漏。

bar函数中,我们将指针ptr传递给了另外一个函数other_fn,我们无法确定other_fn有没有释放ptr内存,如果被释放了,那ptr将成为一个悬空指针,bar在后续还继续访问它,会引发未定义行为,可能导致程序崩溃。

上面由于原生指针使用不当导致的内存泄漏、悬空指针问题都可以通过智能指针来轻松避免。

C++智能指针是一种用于管理动态分配内存的指针类。基于RAII设计理念,通过封装原生指针实现的。可以在资源(原生指针对应的对象)生命周期结束时自动释放内存。

C++标准库中,提供了两种最常见的智能指针类型,分别是std::unique_ptrstd::shared_ptr

接下来我们分别详细展开介绍。

吃独食的unique_ptr

std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。每个 std::unique_ptr 实例都拥有对其所包含对象的唯一所有权,并在其生命周期结束时自动释放对象。

创建unique_ptr对象

我们可以std::unique_ptr的构造函数或std::make_unique函数(C++14支持)来创建一个unique_ptr对象,在超出作用域时,会自动释放所管理的对象内存。示例代码如下:

#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed" << std::endl;
} ~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
};
int main() {
std::unique_ptr<MyClass> ptr1(new MyClass);
// C++14开始支持std::make_unique
std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
return 0;
}

代码输出:

MyClass constructed
MyClass destroyed

访问所管理的对象

我们可以像使用原生指针的方式一样,访问unique_ptr所指向的对象。也可以通过get函数获取到原生指针。

MyClass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 输出 10

释放/重置所管理的对象

使用reset函数可以释放unique_ptr所管理的对象,并将其指针重置为nullptr或指定的新指针。reset`大概实现原理如下

template<class T>
void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept {
// 释放指针指向的对象
delete ptr_;
// 重置指针
ptr_ = ptr;
}

该函数主要完成两件事:

  1. 释放 std::unique_ptr 所管理的对象,以避免内存泄漏。
  2. std::unique_ptr 重置为nullptr或管理另一个对象。

code show time:

#include <iostream>
#include <memory> class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed" << std::endl;
} ~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
}; int main() {
// 创建一个 std::unique_ptr 对象,指向一个 MyClass 对象
std::unique_ptr<MyClass> ptr(new MyClass); // 调用 reset,将 std::unique_ptr 重置为管理另一个 MyClass 对象
ptr.reset(new MyClass);
return;
}

移动所有权

一个对象资源只能同时被一个unique_ptr管理。当尝试把一个unique_ptr直接赋值给另外一个unique_ptr会编译报错。

#include <memory>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = p1; // 编译报错
return 0;
}

为了把一个 std::unique_ptr 对象的所有权移动到另一个对象中,我们必须配合std::move移动函数。

#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1); // ok
std::cout << *p2 << std::endl; // 42
std::cout << (p1.get() == nullptr) << std::endl; // true
return 0;
}

这个例子中, 我们把p1通过std::move将其管理对象的所有权转移给了p2, 此时p2接管了对象,而p1不再拥有管理对象的所有权,即无法再操作到该对象了。

乐于分享的shared_ptr

shared_ptr是C++11提供的另外一种常见的智能指针,与unique_ptr独占对象方式不同,shared_ptr是一种共享式智能指针,允许多个shared_ptr指针共同拥有同一个对象,采用引用计数的方式来管理对象的生命周期。当所有的 shared_ptr 对象都销毁时,才会自动释放所管理的对象。

创建shared_ptr对象

同样的,C++也提供了std::shared_ptr构造函数和std::make_shared函数来创建std::shared_ptr对象。

#include <memory>
int main() {
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = std::make_shared<int>(20);
return;
}

多个shared_ptr共享一个对象

可以通过赋值操作实现多个shared_ptr共享一个资源对象,例如

std::shared_ptr<int>p3 = p2;

shared_ptr采用引用计数的方式管理资源对象的生命周期,通过分配一个额外内存当计数器。

当一个新的shared_ptr被创建时,它对应的计数器被初始化为1。每当赋值给另外一个shared_ptr共享同一个对象时,计数器值会加1。当某个shared_ptr被销毁时,计数值会减1,当计数值变为0时,说明没有任何shared_ptr引用这个对象,会将对象进行回收。

C++提供了use_count函数来获取std::shared_ptr所管理对象的引用计数,例如

std::cout << "p1 use count: " << p1.use_count() << std::endl;

释放/重置所管理的对象

可以使用reset函数来释放/重置shared_ptr所管理的对象。大概实现原理如下(不考虑并发场景)

void reset(T* ptr = nullptr) {
if (ref_count != nullptr) {
(*ref_count)--;
if (*ref_count == 0) {
delete data;
delete ref_count;
}
}
data = ptr;
ref_count = (data == nullptr) ? nullptr : new size_t(1);
}

data指针来存储管理的资源,指针ref_count 来存储计数器的值。

在 reset 方法中,需要减少计数器的值,如果计数器减少后为 0,则需要释放管理的资源,如果减少后不为0,则不会释放之前的资源对象。

如果reset指定了新的资源指针,则需要重新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为nullptr

shared_ptr使用注意事项

避免循环引用

由于 shared_ptr 具有共享同一个资源对象的能力,因此容易出现循环引用的情况。例如:

struct Node {
std::shared_ptr<Node> next;
}; int main() {
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->next = node2;
node2->next = node1;
}

在上述代码中,node1node2 互相引用,在析构时会发现计数器的值不为0,不会释放所管理的对象,产生内存泄漏。

为了避免循环引用,可以将其中一个指针改为 weak_ptr 类型。weak_ptr也是一种智能指针,通常配合shared_ptr一起使用。

weak_ptr是一种弱引用,不对所指向的对象进行计数引用,也就是说,不增加所指对象的引用计数。当所有的shared_ptr都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有weak_ptr都会变成nullptr,这时我们就可以利用expired()方法来判断这个weak_ptr是否已经失效。

我们可以通过weak_ptrlock()方法来获得一个指向共享对象的shared_ptr。如果weak_ptr已经失效,lock()方法将返回一个空的shared_ptr

下面是weak_ptr的基本使用示例:

#include <iostream>
#include <memory> int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 创建shared_ptr对应的weak_ptr指针
std::weak_ptr<int> wp(sp); // 通过lock创建一个对应的shared_ptr
if (auto p = wp.lock()) {
std::cout << "shared_ptr value: " << *p << std::endl;
std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
} else {
std::cout << "wp is expired" << std::endl;
} // 释放shared_ptr指向的资源,此时weak_ptr失效
sp.reset();
std::cout << "wp is expired: " << wp.expired() << std::endl;
return 0;
}

代码输出如下

shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1

回到shared_ptr的循环引用问题,利用weak_ptr不会增加shared_ptr的引用计数的特点,我们将Node.next的类型改为weak_ptr, 避免node1和node2互相循环引用。修改后代码如下

```cpp
struct Node {
std::weak_ptr<Node> next;
}; int main() {
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->next = std::weak_ptr<Node>(node2);
node2->next = std::weak_ptr<Node>(node1); ;
}

避免裸指针与shared_ptr混用

先看看以下代码

int* q = new int(9);
{
std::shared_ptr<int> p(new int(10));
...
q = p.get();
}
std::cout << *q << std::endl;

get函数返回 std::shared_ptr 所持有的指针,但是不会增加引用计数。所以在shared_ptr析构时,将该指针指向的对象给释放掉了,导致指针q变成一个悬空指针。

避免一个原始指针初始化多个shared_ptr

int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 两个shared_ptr指向同一个资源,会导致重复释放
std::shared_ptr<int> ptr2(p);

总结

避免手动管理内存带来的繁琐和容易出错的问题。我们今天介绍了三种智能指针:unique_ptrshared_ptrweak_ptr

每种智能指针都有各自的使用场景。unique_ptr用于管理独占式所有权的对象,它不能拷贝但可以移动,是最轻量级和最快的智能指针。shared_ptr用于管理多个对象共享所有权的情况,它可以拷贝和移动。weak_ptr则是用来解决shared_ptr循环引用的问题。

下一节,我们将自己动手,从零实现一个C++智能指针。敬请期待

【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针的更多相关文章

  1. 【C++深入浅出】智能指针之auto_ptr学习

    起:  C++98标准加入auto_ptr,即智能指针,C++11加入shared_ptr和weak_ptr两种智能指针,先从auto_ptr的定义学习一下auto_ptr的用法. template& ...

  2. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  3. 【C++】智能指针详解(一):智能指针的引入

    智能指针是C++中一种利用RAII机制(后面解释),通过对象来管理指针的一种方式. 在C++中,动态开辟的内存需要我们自己去维护,在出函数作用域或程序异常退出之前,我们必须手动释放掉它,否则的话就会引 ...

  4. [原][C++]拒绝智能指针与指针混用,常见智能指针问题

    公司一个非专科的程序在开发过程中有些毛躁,但是又想使用些新学的技术 这天他正调试呢,发现有一个BUG怎么也找不到原因. 用的好好的内存怎么就突然被删除了呢,好好的指针,怎么就访问越界了呢 没办法,他只 ...

  5. 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱

    必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱 十三.小心使用智能指针.         在前面几节已经很详细了介绍了智能指针适用方式.看起来,似乎智能指针很强大,能够很方便很安全的管理 ...

  6. C++ Primer : 第十二章 : 动态内存之shared_ptr与new的结合使用、智能指针异常

    shared_ptr和new结合使用 一个shared_ptr默认初始化为一个空指针.我们也可以使用new返回的指针来初始化一个shared_ptr: shared_ptr<double> ...

  7. C++11中智能指针的原理、使用、实现

    目录 理解智能指针的原理 智能指针的使用 智能指针的设计和实现 1.智能指针的作用 C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理.程序员自己管理堆内存可以提高了程序 ...

  8. ZT自老罗的博客 Android系统的智能指针(轻量级指针、强指针和弱指针)的实现原理分析

    Android系统的智能指针(轻量级指针.强指针和弱指针)的实现原理分析 分类: Android 2011-09-23 00:59 31568人阅读 评论(42) 收藏 举报 androidclass ...

  9. 异常处理与MiniDump详解(2) 智能指针与C++异常

    write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie 讨论新闻组及文件 一.   综述 <异常处理与MiniDump详解(1) C++异常>稍 ...

  10. 详解 boost 库智能指针(scoped_ptr<T> 、shared_ptr<T> 、weak_ptr<T> 源码分析)

    一.boost 智能指针 智能指针是利用RAII(Resource Acquisition Is Initialization:资源获取即初始化)来管理资源.关于RAII的讨论可以参考前面的文章.在使 ...

随机推荐

  1. c/c++指针从浅入深介绍——基于数据内存分配的理解(上)

    c/c++指针从浅入深介绍--基于数据内存分配的理解(上) 本文是对自我学习的一个总结以及回顾,文章内容主要是针对代码中的数据在内存中的存储情况以及存储中数值的变化来对指针进行介绍,是对指针以及数据在 ...

  2. Linux & 标准C语言学习 <DAY9_1>

        2.函数传参:         1.函数中定义的变量属于该函数,出了该函数就不能再被别的函数直接使用         2.实参与形参之间是以赋值的方式进行传递数据的,并且是单项值传递     ...

  3. 「学习笔记」平衡树基础:Splay 和 Treap

    「学习笔记」平衡树基础:Splay 和 Treap 点击查看目录 目录 「学习笔记」平衡树基础:Splay 和 Treap 知识点 平衡树概述 Splay 旋转操作 Splay 操作 插入 \(x\) ...

  4. 设计模式(二十九)----综合应用-自定义Spring框架-Spring IOC相关接口分析

    1 BeanFactory解析 Spring中Bean的创建是典型的工厂模式,这一系列的Bean工厂,即IoC容器,为开发者管理对象之间的依赖关系提供了很多便利和基础服务,在Spring中有许多IoC ...

  5. 使用golang+antlr4构建一个自己的语言解析器(完结篇)

    Goland 中Antlr4插件 在goland中安装Antlr4插件,用于识别输入的字符在在语法文件中生成的语法树的样子,大概就是如下的摸样 下载步骤: 1.点击文件中的设置选项 2.在插件目录下输 ...

  6. 解决vue中滚轮事件报错 Added non-passive event listener to a scroll-blocking 'mousewheel' event.告警

    参考:https://www.jianshu.com/p/23850d4cade8 参考:让页面滑动流畅得飞起的新特性:Passive Event Listeners

  7. 念一句咒语 AI 就帮我写一个应用,我人麻了...

    原文链接:https://forum.laf.run/d/232 作为人类,我们时常会有自己独特的想法和脑洞大开的创意.然而,这些想法往往因为成本过高而无法实现,毕竟每个人的能力和精力都是有限的,尤其 ...

  8. GitHub+Hexo 搭建博客网站

    Hexo是一款基于Node.js的静态博客框架,依赖少易于安装使用,可以方便的生成静态网页托管在GitHub和Heroku上,是搭建博客的首选框架. 配置Github root@hello:~/cby ...

  9. [Java] 多线程系列之Fork/Join框架[转载]

    1 工作原理 1.1 核心思想:分而治之 & 并行执行 Fork/Join框架是Java 7提供的一个用于并行执行任务的框架, 核心思想就是把大任务分割成若干个小任务,最终汇总每个小任务结果后 ...

  10. sip消息拆包原理及组包流程

    操作系统 :CentOS 7.6_x64      freeswitch版本 :1.10.9 sofia-sip版本: sofia-sip-1.13.14   freeswitch使用sip协议进行通 ...