[C++11#48][智能指针] RAII原则 | 智能指针的类型 | 模拟实现 | shared_ptr | 解决循环引用

目录

一.引入

[1. 为什么需要智能指针?](#1. 为什么需要智能指针?)

[2. 什么是内存泄漏?](#2. 什么是内存泄漏?)

内存泄漏分类

[3.回忆 this](#3.回忆 this)

[二. 原理](#二. 原理)

[1. RAII 资源获取即初始化](#1. RAII 资源获取即初始化)

2.像指针一样

[三. 使用](#三. 使用)

[1. 问题: string 的浅拷贝](#1. 问题: string 的浅拷贝)

2.解决

auto_ptr

[自定义 auto_ptr](#自定义 auto_ptr)

[unique_ptr - 独占式智能指针](#unique_ptr - 独占式智能指针)

[shared_ptr - 共享式智能指针](#shared_ptr - 共享式智能指针)

[⭕ shared_ptr 的赋值 sum](#⭕ shared_ptr 的赋值 sum)

[weak_ptr - 弱引用指针](#weak_ptr - 弱引用指针)

详解

模拟实现:

四.思考

[1. 智能指针的线程安全性](#1. 智能指针的线程安全性)

[2. 循环引用问题](#2. 循环引用问题)

总结


前言:

java 有虚拟机,有垃圾回收,但性能就没那么高了

C++要手动 delete,但是有了异常之后,一切就变得不可控了

所以就创建 了智能指针接收,出作用域后自动析构

在现代C++编程中,内存管理一直是一个非常重要的课题。智能指针的出现解决了传统指针所带来的内存泄漏、异常安全等问题。本文将从智能指针的需求、RAII思想、常见的智能指针类型以及相关实现原理等方面进行详细探讨。

重点如下:

一.引入

1. 为什么需要智能指针?

在传统的C++编程中,手动管理内存非常繁琐且容易出错。考虑下面的例子:

int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");
    return a / b;
}

void Func()
{
    // 1、如果p1这里new 抛异常会如何?
    // 2、如果p2这里new 抛异常会如何?
    // 3、如果div调用这里又会抛异常会如何?
    int *p1 = new int;
    int *p2 = new int;

    cout << div() << endl;

    delete p1;
    delete p2;
}

int main()
{
    try
    {
        Func();
    }
    catch (exception& e)
    {
        cout << e.what() << endl;
    }

    return 0;
}

在此代码中,虽然我们用 try-catch 捕获异常,并确保资源在异常时能够释放,但这会导致代码冗余且难以维护。此外,如果在 new 操作时抛出异常(例如内存不足),没有捕获到的 p1p2 将导致内存泄漏。

例如:

抛异常后,会直接跳转到catch部分,导致原本的delete被跳过,导致内存泄漏

智能指针通过将资源的生命周期与对象生命周期绑定,使得即便抛出异常,内存也能正确释放,从而极大地提升了代码的健壮性和可维护性。

2. 什么是内存泄漏?

内存泄漏是指程序由于疏忽或错误,未能释放已不再使用的内存。这并不是指内存物理上消失,而是应用程序在分配了一段内存后,因设计错误失去对该段内存的控制,导致这部分内存无法被使用,从而浪费了内存资源。

内存泄漏的危害

特别是对于长期运行的程序(如操作系统、后台服务等),内存泄漏会导致可用内存逐渐减少,进而使得系统响应越来越慢,直至最终卡死或崩溃。

示例代码:

void MemoryLeaks() {
    // 1.内存申请了忘记释放
    int *p1 = (int *)malloc(sizeof(int));
    int *p2 = new int;

    // 2.异常安全问题
    int *p3 = new int[10];
    Func();  // 函数抛异常导致 delete[] p3 未执行,p3 没有被释放。
    delete[] p3;
}

内存泄漏分类

在C/C++程序中,内存泄漏主要分为两类:

  1. 堆内存泄漏(Heap leak)
    堆内存是通过 malloc / calloc / realloc / new 等分配的内存,用完后必须通过 free 或者 delete 释放。若程序设计错误导致内存没有释放,则会产生堆内存泄漏,导致内存浪费。
  2. 系统资源泄漏
    程序使用的系统资源(如套接字、文件描述符、管道等)若未使用相应的函数释放,会导致系统资源浪费,严重时可能会导致系统性能下降或不稳定。

如何检测内存泄漏

  1. Linux下检测
    使用Linux平台的内存泄漏检测工具。
  2. Windows下检测
    使用第三方工具,例如VLD(Visual Leak Detector)。
  3. 其他工具
    可以使用内存泄漏工具对比不同检测工具的优劣,选择合适的工具。

如何避免内存泄漏

  1. 良好的设计规范
    在工程的前期,遵循良好的编码规范,确保所有申请的内存都能被及时释放。
  2. RAII思想与智能指针
    使用RAII(Resource Acquisition Is Initialization)思想或智能指针来管理内存与资源,避免手动释放内存时的遗漏和错误。
  3. 私有内存管理库
    有些公司会开发自己的私有内存管理库,这些库通常带有内存泄漏检测功能。
  4. 使用内存泄漏检测工具
    在出问题时,使用内存泄漏工具进行检测和调试,确保问题能及时修复。

总结:

  • 事前预防:使用智能指针等技术。
  • 事后检测:使用内存泄漏检测工具。

3.回忆 this

在C++中,返回this指针是一种常见的做法,指向当前正在被成员函数操作的对象实例,即对自己的调用处理, 特别是在实现链式调用(也称为流畅接口)时。这允许方法在修改对象的状态后返回对象的引用,从而可以在单个语句中连续调用多个方法。

以下是一个使用this指针实现链式调用的简单例子:

#include <iostream>
class Printer {
public:
    Printer& setFont(const std::string& font) {
        this->font = font;
        return *this; // 返回对象的引用
    }
    Printer& setSize(int size) {
        this->size = size;
        return *this; // 返回对象的引用
    }
    void print() const {
        std::cout << "Printing with font " << font << " and size " << size << std::endl;
    }
private:
    std::string font;
    int size;
};
int main() {
    Printer printer;
    
    // 链式调用
    printer.setFont("Arial").setSize(12).print();
    
    return 0;
}

在这个例子中,Printer类有两个方法setFontsetSize,它们都接受参数并修改对象的内部状态。每个方法在修改状态后都返回this指针的引用,这样就可以在调用一个方法后立即调用另一个方法,形成一个链式调用。

这里是详细解释:

  • setFont方法接受一个字符串参数font,并将其存储在私有成员变量font中。之后,它返回*this,即对象自身的引用。
  • setSize方法接受一个整数参数size,并将其存储在私有成员变量size中。同样,它返回*this
  • main函数中,我们创建了一个Printer对象,并通过链式调用的方式设置了字体和大小,然后调用print方法来打印结果。
    通过返回this指针,我们能够构建出流畅的接口,让代码更加易读和易用。

二. 原理

⭕ 智能指针的原理:

  1. RAII特性 :智能指针通过构造和析构自动管理资源,避免手动释放内存。
  2. 重载操作符 :通过重载 *-> 操作符,智能指针能够像普通指针一样使用,具备解引用和访问对象成员的能力。

1. RAII 资源获取即初始化

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种利用对象生命周期来管理资源的技术。通过RAII思想,资源的分配在对象的构造函数中完成,资源的释放则在析构函数中进行。智能指针正是运用了RAII思想,使得资源管理更加安全。

例如,使用RAII设计的智能指针 SmartPtr

template <class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
    ~SmartPtr() {
        if (_ptr) delete _ptr;
    }
private:
    T* _ptr;
};

SmartPtr 中,构造函数获取资源,析构函数负责释放资源。这就确保了无论函数如何退出,资源都能得到正确释放。

2.像指针一样

上述的 SmartPtr 类只是简单的内存管理类,还不能完全称为智能指针。为了让它像普通指针一样使用,还需要重载 *-> 操作符,使其具备指针的行为。

示例代码:

template <class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
    ~SmartPtr() {
        if (_ptr) {
            delete _ptr;
        }
    }

    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }

private:
    T* _ptr;
};

struct Date {
    int _year;
    int _month;
    int _day;
};

int main() {
    SmartPtr<int> sp1(new int);
    *sp1 = 10;

    SmartPtr<Date> sparray(new Date);
    sparray->_year = 2018;
    sparray->_month = 1;
    sparray->_day = 1;
}

三. 使用

1. 问题: string 的浅拷贝

析构了两次的原因。因为我们没写拷贝构造。

那我们写个拷贝构造进行深拷贝解决一下?

不可以。不能深拷贝,因为智能指针模仿的就是原生指针的行为,期望指向的就是同一个。

测试:

template<class T>
class Smartptr
{
public:
	//RAII
	//保存资源
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}

	//释放资源
	~Smartptr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

int main()
{
	Smartptr<int> sp1(new int);
	Smartptr<int> sp2(sp1);

	return 0;
}

浅拷贝,重复释放,导致内存泄漏

_CrtIsValidHeapPointer(block)。这个错误通常表示你的程序遇到了堆内存问题,可能是由于非法访问或者尝试释放已经被释放过的内存导致的

于是实践当中出现了四种智能指针:

  • C++98 auto_ptr(不好用)
  • C++11 unique_ptr
  • C++11 shared_ptr
  • C++11 weak_ptr

2.解决

C++11及其之后的标准中引入了多种智能指针类型,主要包括 auto_ptr(已废弃)、unique_ptrshared_ptrweak_ptr。下面将对这些智能指针进行介绍。

auto_ptr

  • C++98 一般实践中,很多公司明确规定不要用 auto_ptr
  • 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象

隐患:导致被拷贝对象悬空,访问就会出问题

int main()
{

	bit::auto_ptr<A> ap1(new A(1));
	bit::auto_ptr<A> ap2(new A(2));

	bit::auto_ptr<A> ap3(ap1);

	// 崩溃
	//ap1->_a++;
	ap3->_a++;

	return 0;
}
自定义 auto_ptr
  • RAII

  • 像指针一样

  • 实现拷贝

    namespace bit
    {
    template<class T>
    class auto_ptr
    {
    public:
    auto_ptr(T* ptr)
    :_ptr(ptr)
    {}

      	~auto_ptr()
      	{
      		cout << "delete:" << _ptr << endl;
      		delete _ptr;
      	}
    
      	T& operator*()
      	{
      		return *_ptr;
      	}
    
      	T* operator->()
      	{
      		return _ptr;
      	}
      	
      	// ap3(ap1)
      	// 管理权转移
      	auto_ptr(auto_ptr<T>& ap)
      		:_ptr(ap._ptr)
      	{
      		ap._ptr = nullptr;
      	}
    
      private:
      	T* _ptr;
      };
    

例如:将 ap1 转化给 ap3 后置空,但留下了隐患

int main()
{

	bit::auto_ptr<int> ap1(new int);
	bit::auto_ptr<int> ap2(ap1);

	(*ap2)++;
	(*ap1)++;

	return 0;
}

异常:

优化发展

最早是在 boost 库中出现

boost库出现的智能指针

scoped_ptr

shared_ptr/weak_ptr

  • C++11也搞了智能指针。
    C++11 unique_ptr 就是抄的 scoped_ptr
    C++11 也有 shared_ptr/weak_ptr

unique_ptr - 独占式智能指针

unique_ptr 通过防止拷贝来确保一个对象只能由一个 unique_ptr 所管理。它的拷贝构造函数和赋值运算符都被删除了:

template <class T>
class unique_ptr {
public:
    unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}
    ~unique_ptr() { delete _ptr; }
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
private:
    T* _ptr;
};

适合不需要共享资源的场景。

C++中发展出了 unique_ptr,解决方案非常简单粗暴。 --> 防拷贝,因此也叫做唯一指针

**禁拷贝的时候,也要防赋值,**完整实现如下:

template<class T>
class unique_ptr
{
public:
	//保存资源
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	//释放资源
	~unique_ptr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}

	//拷贝构造 
	unique_ptr(const unique_ptr<T>& up) = delete;

	//赋值重载
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;


	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

需要拷贝的时候,怎么办呢?

shared_ptr - 共享式智能指针

shared_ptr 允许多个指针共享同一份资源,并通过引用计数 来管理资源的生命周期。当最后一个 shared_ptr 销毁时,资源才会被释放:

(面试要手撕,就撕这个)

通过引用计数来实现,期望一个资源一个引用计数

template <class T>
class shared_ptr {
public:
    shared_ptr(T* ptr = nullptr) : _ptr(ptr), _count(new int(1)) {}
    ~shared_ptr() { Release(); }
    shared_ptr(const shared_ptr& sp) : _ptr(sp._ptr), _count(sp._count) {
        (*_count)++;
    }
    shared_ptr& operator=(const shared_ptr& sp) {
        if (this != &sp) {
            Release();
            _ptr = sp._ptr;
            _count = sp._count;
            (*_count)++;
        }
        return *this;
    }
private:
    void Release() {
        if (--(*_count) == 0) {
            delete _ptr;
            delete _count;
        }
    }
    T* _ptr;
    int* _count;
};

测试:

int main()
{
	// C++11
	bit::shared_ptr<A> sp1(new A(1));
	bit::shared_ptr<A> sp2(new A(2));

	bit::shared_ptr<A> sp3(sp1);
	sp1->_a++;
	sp3->_a++;

	cout << sp1->_a << endl;

	bit::shared_ptr<A> sp4(sp2);
	bit::shared_ptr<A> sp5(sp4);
}

赋值的实现

测试 sp1=sp5 sp3=sp5

  1. sp1._pcount--

  2. 赋值

    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
    //避免对同一份资源拷贝,进行一下判断要
    if (_ptr == sp._ptr)
    return *this;

            //左边资源要注意是不是最后一个了
     		if (--(*_pcount) == 0)
     		{
     			delete _ptr;
     			delete _pcount;
     		}
    
             //赋值拷贝
     		_ptr = sp._ptr;
     		_pcount = sp._pcount;
     		++(*_pcount);
    
     		return *this;
     	}
    

特例的解决:

  1. sp3=sp3 会出现野指针
  2. sp4 =sp5

在最开始的时候,最好用资源来判定一下

⭕ shared_ptr 的赋值 sum
  1. 避免对同一份资源拷贝,进行一下判断要
  2. 左边资源要注意是不是最后一个了
  3. 最后 赋值拷贝

shared_ptr 适用于多个对象共享同一资源的场景,但需要注意循环引用的问题。

weak_ptr - 弱引用指针

weak_ptr 是为了解决 shared_ptr 的循环引用问题而引入的,它不增加资源的引用计数,只作为一种弱引用存在,指向 shared_ptr 所管理的资源。通过 lock 方法,可以从 weak_ptr 获取一个 shared_ptr

template <class T>
class weak_ptr {
public:
    weak_ptr() : _ptr(nullptr) {}
    weak_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr) {}
    weak_ptr& operator=(const shared_ptr<T>& sp) { _ptr = sp._ptr; return *this; }
private:
    T* _ptr;
};
详解

引入:结构体内改成智能指针,解决类型不匹配

struct ListNode
{
	int val;
	//ListNode* _next;
	//ListNode* _prev;
	
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;

	//这里主要是看释放没有
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test_shared_ptr2()
{
	//ListNode* n1 = new ListNode;
	//ListNode* n2 = new ListNode;

	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	//n1,n2改成shared_ptr这里会出现类型不匹配的问题
	//一个是智能指针自定义类型对象,一个是普通类内置类型对象
	//因此上面也改成智能指针对象,就可以了
	n1->_next = n2;
	n2->_prev = n1;

	//delete n1;
	//delete n2;
}

发现把next,prev屏蔽掉就可以释放了。

原因是构成了循环引用

刚开始都是只有一个指向,引用计数都是1

命运在此刻形成了闭环

解决**:weak_ptr 可以访问,但不参与资源的管理**

可以看到weak_ptr构造函数只有无参构造、拷贝构造、还有shared_ptr拷贝构造。并没有指针构造 !也就是说不支持RAII并且并不会把资源交给它管理。并且赋值也有shared_ptr的赋值。

#include <iostream>
#include <memory>

using namespace std;

struct ListNode {
int val;
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;

ListNode(int x) : val(x), _next(nullptr) {
    cout << "ListNode(" << val << ") created" << endl;
}

~ListNode() {
    cout << "ListNode(" << val << ") destroyed" << endl;
}
};

int main() {
    {
        // 创建两个节点,n1 和 n2
        shared_ptr<ListNode> n1 = make_shared<ListNode>(1);
        shared_ptr<ListNode> n2 = make_shared<ListNode>(2);

        // 创建循环引用
        n1->_next = n2;
        n2->_prev = n1;

        // 打印节点信息
        cout << "n1's next node value: " << n1->_next->val << endl;
        if (auto prev = n2->_prev.lock()) {  // 使用 weak_ptr::lock() 获取 shared_ptr
            cout << "n2's prev node value: " << prev->val << endl;
        }
    } // 离开作用域,n1 和 n2 应该会被销毁

    cout << "Exiting main function..." << endl;

    return 0;
}
  • 我们创建了两个节点 n1n2,并且设置了 _next_prev 的链接。
  • 通过使用 weak_ptr::lock() 来安全地获取 shared_ptr,避免直接使用 weak_ptr 导致的问题。
  • 在退出 main 函数前,n1n2 会被销毁,因为没有引用计数,所以内存都可以正常释放。

解决原理:

使 _next 和 _prev 不参与计数了

模拟实现:
template<class T>
class weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}

	weak_ptr<T> operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}
	
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
};

weak_ptr 支持了 shared_ptr

注意sharde_ptr的get要加一个cosnt不然会报错,const对象不能调用非const的普通对象的。


四.思考

1. 智能指针的线程安全性

智能指针中的引用计数并非线程安全的。在多线程环境中,多个线程同时对引用计数进行操作,可能会导致计数不准确。为了解决这个问题,C++11中的 shared_ptr 实现是线程安全的,它通过原子操作确保引用计数的正确性。用户在访问被智能指针管理的对象时,需要自行加锁。之后的文章会再详细解释。

2. 循环引用问题

shared_ptr 的一个常见问题是循环引用。例如,两个对象相互持有对方的 shared_ptr,将导致它们的引用计数永远无法变为0,从而无法释放资源。此时,使用 weak_ptr 可以打破循环引用。

总结

智能指针通过 RAII 思想有效地管理了资源,极大减少了内存泄漏的风险。在 C++11 中,unique_ptrshared_ptrweak_ptr 等不同类型的智能指针提供了丰富的功能,以应对不同的场景需求。理解并合理使用智能指针,是现代 C++ 编程的基本功之一。

相关推荐
vener_1 分钟前
LuckySheet协同编辑后端示例(Django+Channel,Websocket通信)
javascript·后端·python·websocket·django·luckysheet
sweetheart7-713 分钟前
LeetCode20. 有效的括号(2024冬季每日一题 11)
c++·算法·力扣··括号匹配
MapleLea1f20 分钟前
26届JAVA 学习日记——Day14
java·开发语言·学习·tcp/ip·程序人生·学习方法
小汤猿人类21 分钟前
SpringTask
开发语言·python
计算机毕设孵化场27 分钟前
计算机毕设-基于springboot的多彩吉安红色旅游网站的设计与实现(附源码+lw+ppt+开题报告)
vue.js·spring boot·后端·计算机外设·课程设计·计算机毕设论文·多彩吉安红色旅游网站
爪哇学长28 分钟前
解锁API的无限潜力:RESTful、SOAP、GraphQL和Webhooks的应用前景
java·开发语言·后端·restful·graphql
老赵的博客35 分钟前
QT 自定义界面布局要诀
开发语言·qt
gma99941 分钟前
brpc 与 Etcd 二次封装
数据库·c++·rpc·etcd
ö Constancy44 分钟前
设计LRU缓存
c++·算法·缓存
p-knowledge1 小时前
建造者模式(Builder Pattern)
java·开发语言·建造者模式