[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++ 编程的基本功之一。

相关推荐
Asthenia041231 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫