C++智能指针学习

字节流动

共 1836字,需浏览 4分钟

 ·

2022-04-16 13:38

前言:

大家好,今天继续给大家分享一篇cpp文章,最近我也在复习关于STL的基础知识,后期会分享这块的知识,今天分享这篇文章后,继续总结一下音视频里面的同步知识点,这篇文章明天早上发布出来。

如果您在看完文章或者看的过程中,有任何疑问都可以私聊我,可以相互探讨,一起进步!

好了,废话就不多说,看内容:

一、智能指针的概念:

C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。

这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?

在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。但是智能指针也不只是说的简简单单的自动释放内存这么简单,在使用上有一些坑需要注意,将会在下文中提到。

C++中有四种智能指针:

  • 1、auto_ptr

  • 2、unique_ptr

  • 3、shared_ptr

  • 4、week_ptr

其中后三个是 C++11 支持,第一个已经被 C++11 弃用且被 unique_ptr 代替,不推荐使用。下文将对其逐个说明。

1、std::auto_ptr:

在这个年代讨论 std::auto_ptr 不免有点让人怀疑是不是有点过时了,确实如此,随着 C++11 标准的出现(最新标准是 C++20),std::auto_ptr 已经被彻底放弃,取而代之是 std::unique_ptr。

然而,之所以还向介绍一下 std::auto_ptr 的用法以及它的设计不足之处是想更多了解 C++ 语言中智能指针的发展过程,一项技术如果我们了解它过去的样子和发展的轨迹,我们就能更好地掌握它。

上面说到c++的新的特性学习,但是在嵌入式领域,c++还不会用太新的版本,但是作为自身学习的话,新的东西,还是要去探索学习的,紧跟技术发展,为此我发现这两个学习c++的网站不错,一个是cplusplus和standard c++以及boost库:

  • cplusplus:
http://www.cplusplus.com/

这里面也可以学习STL:

http://www.cplusplus.com/reference/stl/
  • Standard c++:
https://isocpp.org/
  • boost:
https://www.boost.org/

好的,我们继续这个std::auto_ptr分享,下面是简单的代码试验:

#include 
int main()
{
    //初始化方式1
    std::auto_ptr ap1(new int(8));
    //初始化方式2
    std::auto_ptr ap2;
    ap2.reset(new int(8));
    return 0;
}

编译结果:

从警告deprecated 单词表明这个std::auto_ptr已经在新的标准中被抛弃了。

智能指针对象 ap1 和 ap2 均持有一个在堆上分配 int 对象,其值均是 8,这两块堆内存均可以在 ap1 和 ap2 释放时得到释放。这是 std::auto_ptr 的基本用法。

std::auto_ptr 真正让人容易误用的地方是其不常用的复制语义,即当复制一个 std::auto_ptr 对象时(拷贝复制或 operator= 复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:

#include 

int main()
{
    //测试拷贝构造
    std::auto_ptr ap1(new int(8));
    std::auto_ptr ap2(ap1);
    if (ap1.get() != NULL)
    {
        std::cout << "ap1 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap1 is empty." << std::endl;
    }
    if (ap2.get() != NULL)
    {
        std::cout << "ap2 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap2 is empty." << std::endl;
    }
    
    //测试赋值构造
    std::auto_ptr ap3(new int(8));
    std::auto_ptr ap4;
    ap4 = ap3;
    if (ap3.get() != NULL)
    {
        std::cout << "ap3 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap3 is empty." << std::endl;
    }

    if (ap4.get() != NULL)
    {
        std::cout << "ap4 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap4 is empty." << std::endl;
    }
    return 0;
}

运行结果如下:

ap1 is empty.
ap2 is not empty.
ap3 is empty.
ap4 is not empty.

分析:上述代码中分别利用拷贝构造(ap1 => ap2)和 赋值构造(ap3 => ap4)来创建新的 std::auto_ptr 对象,因此 ap1 持有的堆对象被转移给 ap2,ap3 持有的堆对象被转移给 ap4。而 ap1 和 ap2 已经指向 NULL,若现在再对 ap1 和 ap2 进行访问并操作,将会出现内存错误问题。

由于 std::auto_ptr 这种不常用的复制语义,我们应该避免在 stl 容器中使用 std::auto_ptr,例如我们绝不应该写出如下代码:

std::vector> myvectors;

当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们想看到的,会造成很多意想不到的错误。

以史为鉴,作为 std::auto_ptr 的替代者 std::unique_ptr 吸取了这个经验教训。下文会来详细介绍。

正因为 std::auto_ptr 的设计存在如此重大缺陷,C++11 标准在充分借鉴和吸收了 boost 库中智能指针的设计思想,引入了三种类型的智能指针,即:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。

  • boost 还有 scoped_ptr,C++11 并没有全部照搬,而是选择了三个最实用的指针类型。在 C++11 中可以通过 std::unique_ptr 达到与 boost::scoped_ptr 一样的效果。

所有的智能指针类(包括 std::unique_ptr)均包含于头文件中。

所以在 C++11及后续语言规范中 std::auto_ptr 已经被废弃,你的代码不应该再使用它。

2、std::unique_ptr:

作为对 std::auto_ptr 的改进,std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。

可以使用以下方式初始化一个 std::unique_ptr 对象:

int main()
{
    //初始化方式1
    std::unique_ptr up1(new int(123));
    //初始化方式2
    std::unique_ptr up2;
    up2.reset(new int(123));
    //初始化方式3 (-std=c++14)
    std::unique_ptr up3 = std::make_unique(123);
}

应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已经解释过了,有兴趣的可以阅读此书相关章节.

这本电子书,我这边有,有需要的朋友,可以私下找我领取就行:

令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:

template 
std::unique_ptr make_unique(Ts &&...params)
{
return std::unique_ptr(new T(std::forward(params)...));
}

鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。

template 
class unique_ptr
{
    //省略其他代码...

    //拷贝构造函数和赋值运算符被标记为delete
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;
};

因此,下列代码是无法通过编译的:

std::unique_ptr up1(std::make_unique(123));;

//以下代码无法通过编译
//std::unique_ptr up2(up1);
std::unique_ptr up3;
//以下代码无法通过编译
//up3 = up1;

禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:

#include 

std::unique_ptr func(int val)
{
    std::unique_ptr up(new int(val));
    return up;
}

int main()
{
    std::unique_ptr up1 = func(123);

    return 0;
}

上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 up1。

既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

int main()
{
    std::unique_ptr up1(std::make_unique(123));
    std::unique_ptr up2(std::move(up1));
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
    
    std::unique_ptr up3;
    up3 = std::move(up2);
    std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;
    
    return 0;
}

运行结果:

up1 is NULL
up2 is NULL

以上代码利用 std::move 将 up1 持有的堆内存(值为 123)转移给 up2,再把 up2 转移给 up3。最后,up1 和 up2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数或移动赋值运算符的类才行,而 std::unique_ptr 正好实现了这二者,以下是实现伪码:

template 
class unique_ptr
{
    //其他函数省略...
public:
    unique_ptr(unique_ptr &&rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
    }

    unique_ptr &operator=(unique_ptr &&rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
        return *this;
    }

private:
    T *m_pT;
};

这是 std::unique_ptr 具有移动语义的原因。关于移动构造和 std::move 也是 C++11 的新特性,这里不详细说明。

std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:

int main()
{
    //创建10个int类型的堆对象
    //形式1
    std::unique_ptr up1(new int[10]);
    //形式2
    std::unique_ptr up2;
    up2.reset(new int[10]);
    //形式3
    std::unique_ptr up3(std::make_unique(10));

    for (int i = 0; i < 10; ++i)
    {
        up1[i] = i;
        up2[i] = i;
        up3[i] = i;
    }

    for (int i = 0; i < 10; ++i)
    {
        std::cout << up1[i] << ", " << up2[i] << ", " << up3[i] << std::endl;
    }

    return 0;
}

程序执行结果如下:

0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9

std::shared_ptr 和 std::weak_ptr 也可以持有一组堆对象,用法与 std::unique_ptr 相同,下文不再赘述。

另外,std::unique_ptr 有几个常用函数如下:

void reset(pointer p = pointer())

释放当前由 unique_ptr(如果有)管理的指针并获得参数 p(参数 p 默认为 NULL)的所有权。如果 p 是空指针(例如默认初始化的指针),则 unique_ptr 变为空,调用后不管理任何对象。

pointer release()

返回管理的指针并将其替换为空指针, 释放其管理指针的所有权。这个调用并不会销毁托管对象,但是将 unique_ptr 对象管理的指针解脱出来。如果要强制销毁所指向的对象,请调用 reset 函数或对其执行赋值操作。

element_type* get()

返回存储的指针,不会使 unique_ptr 释放指针的所有权。因此,该函数返回的值不能于构造新的托管指针,如果为了获得存储的指针并释放其所有权,请调用 release。

void swap (unique_ptr& x)

将 unique_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。

3、自定义智能指针对象持有的资源的释放函数:

默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:

class Socket
{
public:
    Socket()
    {
    }

    ~Socket()
    {
    }

    //关闭资源句柄
    void close()
    {
    }
};

int main()
{
    auto deletor = [](Socket *pSocket)
    {
        //关闭句柄
        pSocket->close();
        //TODO: 你甚至可以在这里打印一行日志...
        delete pSocket;
    };

    std::unique_ptr upSocket(new Socket(), deletor);

    return 0;
}

自定义 std::unique_ptr 的资源释放函数其规则是:

std::unique_ptr

其中 T 是你要释放的对象类型,DeletorPtr 是一个自定义函数指针。上述代码 28 行表示 DeletorPtr 有点复杂(是 C++11 中的 Lambda 函数),我们可以使用 decltype(deletor) 让编译器自己推导 deletor 的类型,因此可以将 28 行代码修改为:

std::unique_ptr upSocket(new Socket(), deletor);

剩下的两个过两天再分享,先吸收消化这两个!

文章整理:txp玩Linux


推荐:

面试常问的 C/C++ 问题,你能答上来几个?

C++ 面试必问:深入理解虚函数表

很多人搞不清 C++ 中的 delete 和 delete[ ] 的区别

看懂别人的代码,总得懂点 C++ lambda 表达式吧

Java、C++ 内存模型都不知道,还敢说自己是高级工程师?

C++ std::thread 必须要熟悉的几个知识点

现代 C++ 并发编程基础

现代 C++ 智能指针使用入门

c++ thread join 和 detach 到底有什么区别?

C++ 面试八股文:list、vector、deque 比较

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报