终于有一篇小白能看懂的智能指针详解了!

Posted CodeBowl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了终于有一篇小白能看懂的智能指针详解了!相关的知识,希望对你有一定的参考价值。

内容较多,建议收藏,以后复习

为什么会出现智能指针?

对于程序员来说,如果我不需要,或者我没那么需要,再或者说有替代品,我都不会再去实现一个新东西。

废话,程序员哪里有勤快的!

既然它被发明出来,一定是解决问题的!

在之前,我愚昧的小白生涯中,见到复杂的指针,时常俩股战战,因为搞不懂什么值传递、地址传递、实参、形参、引用、一级指针、多级指针、数组指针、函数指针等等概念,

但是仗着胆子大,就直接上手,唉,似乎没有那么难?

什么动态申请内存,指针赋值,输出字符串,手到擒来,展示!

char *ch = (char*)malloc(12);
memcpy(ch, "Hello World", 12);
std::cout << ch << std::endl;

在我的黑框框编程生涯中,它是没有打击我的,总是非常给面子的输出正确答案!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lyu9xOL1-1630808532107)(C:\\Users\\11073\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210904170337518.png)]

那时候,我以为我和C++的故事会一直这么美好!

那么问题来了?是谁打破了这一美好的幻境!

内存泄漏打破美好

大学期间,上课走神的我,上完了《C++程序设计》之后,都没有听过这个“内存泄漏”,也正是我对他的不熟悉,打破了我和C++稳固的关系,(程序:“呵,我不是能运行就行?”)

什么是内存泄漏?

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。以上定义来自于百度,并且十分准确。

什么!我之前忘记使用free()竟然会造成这么大的后果!

但是我已经形成习惯,已经很想记住,但总是不经意间会忘记释放内存,毕竟“手动”,一定不是最佳方案。这里有一个思想:RAII 资源获取即初始化技术:对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在 离开作用域时调用)的时候释放空间。

在我以为我和C++缘分尚浅的时候,C++11搬来救兵,“智能指针”横空出世,打败了内存泄漏,拯救了我们的关系。

智能指针

C++11 引入了智能指针的概念,使用了引用计数的想法,让程序 员不再需要关心手动释放内存。也就是说,原本需要我们手动就一一对应的malloc/free或new/delete的工作由智能指针来代替了!

这种自动化,程序员最爱!

这些智能指针就包括 std::shared_ptr/std::unique_ptr/std::weak_ptr, 使用它们需要包含头文件 。

unique_ptr

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  1. 拥有它指向的对象
  2. 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  3. 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

功能

unique_ptr 可以实现如下功能:

  1. 为动态申请的内存提供异常安全
  2. 讲动态申请的内存所有权传递给某函数
  3. 从某个函数返回动态申请内存的所有权
  4. 在容器中保存指针

实验

#include<iostream>
#include<Windows.h>
#include<memory>
using namespace std;

class Test
{
public:
    Test(string s)
    {
        str = s;
        cout << "Test creat\\n";
    }
    ~Test()
    {
        cout << "Test delete:" << str << endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout << str << endl;
    }
private:
    string str;
};


unique_ptr<Test> fun()
{
    return unique_ptr<Test>(new Test("789"));
}

int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if (ptest == NULL)
        cout << "ptest = NULL\\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数    
    ptest2->print();
    return 0;
}

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值。

shared_ptr

std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除 显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的 不对称。

std::make_shared 就能够用来消除显式的使用 new,所以 std::make_shared 会分配创建传入参 数中的对象,并返回这个对象类型的 std::shared_ptr 指针。例如:

#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i) 
{
	(*i)++;
}
int main() 
{
	// auto pointer = new int(10); // illegal, no direct assignment
	// Constructed a std::shared_ptr
	auto pointer = std::make_shared<int>(10);
	foo(pointer);
	std::cout << *pointer << std::endl; // 11
										// The shared_ptr will be destructed before leaving the scope
	return 0;
}

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数,并 通过 use_count() 来查看一个对象的引用计数。例如:

auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数 +1
auto pointer3 = pointer; // 引用计数 +1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset

成员函数

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.

缺陷

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

#include <iostream>  
using namespace std;  
#include <memory>  
  
class B;  
  
class A  
{  
public:  
    shared_ptr<B> ptrA_B;  
public:  
    A()  
    {  
        cout << "调用class A的默认构造函数" << endl;  
    }  
    ~A()  
    {  
        cout << "调用class A的析构函数" << endl;  
    }  
};  
  
class B  
{  
public:  
    shared_ptr<A> ptrB_A;  
public:  
    B()  
    {  
        cout << "调用class B的默认构造函数" << endl;  
    }  
    ~B()  
    {  
        cout << "调用class B的析构函数" << endl;  
    }  
};  
  
int main()  
{  
    shared_ptr<B> ptrB = make_shared<B>();  
    shared_ptr<A> ptrA = make_shared<A>();  
    ptrA->ptrA_B = ptrB;  
    ptrB->ptrB_A = ptrA;  
}  

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引 用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样 就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了 内存泄露,如图:

weak_ptr

weak_ptr详解:https://blog.csdn.net/weixin_45590473/article/details/113057545

解决shared_ptr的问题的办法就是使用弱引用指针 std::weak_ptr,std::weak_ptr 是一种弱引用(相比较 而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加,由此我们引入了弱指针。

当换用弱引用时候,最终的 释放流程如图:

在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。 std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于 检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true。

总结

智能指针这种技术并不新奇,在很多语言中都是一种常见的技术,现代 C++ 将这项技术引进,在 一定程度上消除了 new/delete 的滥用,是一种更加成熟的编程范式。

以上是关于终于有一篇小白能看懂的智能指针详解了!的主要内容,如果未能解决你的问题,请参考以下文章

小白也能看懂的 DFS 算法本质详解

小白也能看懂的 DFS 算法本质详解

小白也能看懂的 DFS 算法本质详解

小白也能看懂的dubbo3应用级服务发现详解

小白都能看懂的java虚拟机内存区域划分

马尔可夫链 ▏小白都能看懂的马尔可夫链详解