C++11 智能指针

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

现在有一个抛出异常的场景:

#include <iostream>
using namespace std;

double func2(int a, int b)
{
	// 申请内存
	int* array = new int[1024 * 1024 * 1024]; // 1G

	if (b == 0)
	{
		throw "除数不能为0";
	}
	else
	{
		return a / b;
	}
}

double func1()
{
	try
	{
		return func2(10, 0);
	}
	catch (int)
	{
		cout << "catch (int)" << endl; 
	}
	catch (string)
	{
		cout << "catch (string)" << endl;
	}
}

int main()
{
	try
	{
		cout << func1() << endl;
	}
	catch (const char* str) // 严格匹配,根据throw的类型,拷贝构造或移动构造catch的参数类型
	{
		cout << str << endl;
	}

	return 0;
}

前面关于C++ 异常我们讲到过,由于throw会逐步释放当前栈帧返回上一个函数栈帧,直到找到类型严格匹配的catch,所以当该栈帧空间结束后,栈帧中的对象的资源需要释放

特别是动态开辟的空间,需要手动去释放的资源。

对于当前案例,因为确定会抛出异常,且直接回到上一层(没有找到继续返回,直到被捕获,如果直到main()函数中都没有被捕获到便终止程序),会跳过当前throw的函数栈帧后面的语句,尽管后面有对资源的主动释放。

这样便会造成内存泄漏。

2. 内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害 

2.2 内存泄漏分类 

C/C++程序中一般我们关心两种方面的内存泄漏:

1. 堆内存泄

描述
malloccallocrealloc 或者 new 等函数从堆中分配的内存。使用完后,必须通过调用相应的 freedelete 函数来释放。如果程序设计有误,导致这些内存未被释放,那么这部分内存将无法再被使用,形成堆内存泄漏(Heap Leak)。

2. 系统资源泄漏

描述
系统资源泄漏是指程序在使用系统分配的资源(如套接字、文件描述符、管道等)时,未能正确释放这些资源。未释放的系统资源会导致系统资源浪费,进而可能引起系统性能下降或不稳定。

2.3 如何检测内存泄漏 

在linux下内存泄漏检测:

       工具                                                          描述                                                              
valgrind一个强大的开源程序检测工具
mtraceGNU 扩展,用来跟踪 malloc, mtrace 为内存分配函数(如 malloc, realloc, memalign, free)安装 hook 函数
dmalloc用于检查 C/C++ 内存泄漏的工具。检测程序运行结束时是否存在未释放的内存,以一个运行库V的方式发布。
memwatchdmalloc 一样,memwatch 也能检测未释放的内存,并记录内存的分配和释放情况,用于发现潜在的内存泄漏问题。
mpatrol一个跨平台的 C/C++ 内存泄漏检测器。
dbgmem可以与程序一起运行,并在程序结束时报告内存的使用情况。
Electric Fence通过引发段错误的方式,帮助开发者在调试时定位内存分配错误和内存泄漏问题。

在windows下使用第三方工具:

项目主页:Visual Leak Detector | Enhanced Memory Leak Detection for Visual C++

下载:https://github.com/KindDragon/vld/releases/download/v2.5.1/vld-2.5.1-setup.exe

其他工具:内存泄漏工具比较

2.4 如何避免内存泄漏 

1. 规范用法:

  • 使用智能指针

    • C++中使用智能指针如 std::unique_ptrstd::shared_ptr 来管理动态分配的内存。智能指针会在超出作用域时自动释放内存,避免忘记调用 delete 造成的内存泄漏。
  • 遵循RAII原则

    • 使用RAII技术,将资源管理交由对象的生命周期来控制。在对象的构造函数中分配资源,在析构函数中释放资源,确保资源在对象生命周期结束时自动释放。
  • 确保所有new对应delete

    • 每个 newmalloc 调用都应该有相应的 deletefree。对每个分配的内存都应有明确的释放策略。
  • 避免循环引用

    • 在使用智能指针时,注意避免循环引用问题。例如,std::shared_ptr 之间如果相互引用,将导致内存无法自动释放。可以使用 std::weak_ptr 解决此问题。
  • 定期检查与测试

    • 使用工具如 Valgrind、ASan(AddressSanitizer)等对程序进行内存泄漏检查,及时发现和修复潜在的内存泄漏问题。

2. 最后防线:

  • 使用内存泄漏检测工具

    • 定期使用内存泄漏检测工具,如 Valgrind,来扫描程序中可能存在的内存泄漏。对于复杂项目,持续集成环境中可以集成这些工具,以便及时发现和解决内存管理问题。

3. 智能指针的使用及原理

3.1 RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种通过对象生命周期来管理程序资源(如内存、文件句柄、网络连接、互斥量等)的技术。其核心思想是在对象构造时获取资源,并在对象析构时自动释放资源。

RAII的两大优势:

  1. 不需要显式释放资源

    • 通过将资源的管理交由对象的生命周期处理,避免了手动释放资源的繁琐和错误,降低了内存泄漏或资源泄漏的风险。
  2. 资源在对象生命期内始终有效

    • 在对象的整个生命周期中,资源始终保持有效,确保资源在使用过程中处于一个稳定和可用的状态。

通过RAII,程序设计更加简洁、可靠,减少了资源管理的复杂性。

 以下是使用RAII思想设计的SmartPtr类,并在MergeSort函数中使用该智能指针类的完整代码:

#include <iostream>
#include <vector>
#include <exception>

using namespace std;

template<class T>
class SmartPtr 
{
public:
    // 构造函数,接受一个原始指针
    SmartPtr(T* ptr = nullptr)
    : _ptr(ptr)
    {}

    // 析构函数,在对象销毁时自动释放内存
    ~SmartPtr() 
    {
        if (_ptr) 
        {
            delete _ptr;
        }
    }

private:
    T* _ptr; // 原始指针
};

void MergeSort(int* a, int n) 
{
    // 动态分配内存,并使用SmartPtr管理
    int* tmp = (int*)malloc(sizeof(int) * n);

    // 讲tmp指针委托给了sp对象
    SmartPtr<int> sp(tmp);

    // 假设这里有排序相关逻辑,比如 _MergeSort(a, 0, n - 1, tmp);

    // 这里假设处理了一些其他逻辑
    vector<int> v(1000000000, 10);
    // ...
}

int main() 
{
    try 
    {
        int a[5] = { 4, 5, 2, 3, 1 };
        MergeSort(a, 5);
    } 
    catch (const exception& e) 
    {
        cout << e.what() << endl;
    }

    return 0;
}

3.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;
    cout << *sp1 << endl;
    
    SmartPtr<Date> sparray(new Date);
    sparray->_year = 2018;
    sparray->_month = 1;
    sparray->_day = 1;
}

总结一下智能指针的原理: 1. RAII特性 2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3 auto_ptr

资源管理权转移

std::auto_ptr文档

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

// C++库中的智能指针都定义在memory这个头文件中
#include <memory>
 
class Date
{
public:
    Date() { cout << "Date()" << endl;}
    ~Date() { cout << "~Date()" << endl;}
 
    int _year;
    int _month;
    int _day;
};
 
int main()
{
    auto_ptr<Date> ap(new Date);
    auto_ptr<Date> copy(ap);
 
    // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
    // C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
    ap->_year = 2018;
 
    return 0;
}

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份AutoPtr来了解它的原理

// 模拟实现一份简答的AutoPtr,了解原理
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = NULL)
    : _ptr(ptr)
    {}
    
    ~AutoPtr()
    {
        if(_ptr)
            delete _ptr;
    }
    
    // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
    // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    AutoPtr(AutoPtr<T>& ap)
    : _ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }
    
    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        // 检测是否为自己给自己赋值
        if(this != &ap)
        {
            // 释放当前对象中资源
            if(_ptr)
                delete _ptr;
            
            // 转移ap中资源到当前对象中
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        
        return *this;
    }
    
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    
private:
    T* _ptr;
};

int main()
{
    AutoPtr<Date> ap(new Date);
    
    // 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
    // 通过ap对象访问资源时就会出现问题。
    AutoPtr<Date> copy(ap);
    ap->_year = 2018;
    
    return 0;
}

3.4 unique_ptr

C++11中开始提供更靠谱的unique_ptr

std::unique_ptr文档

int main()
{
 unique_ptr<Date> up(new Date);
 
 // unique_ptr的设计思路非常的粗暴-防拷贝,也就是不让拷贝和赋值。
 unique_ptr<Date> copy(ap);
 
 return 0;
}

 unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

template<class T> 
class UniquePtr
{
public:
    // 构造函数,接收一个原生指针,如果没有提供则默认为 nullptr
    UniquePtr(T * ptr = nullptr) 
    : _ptr(ptr)
    {}
    
    // 析构函数,删除指针所指向的对象
    ~UniquePtr() 
    {
        if(_ptr)
            delete _ptr;
    }
    
    // 重载解引用操作符,返回指向的对象引用
    T& operator*() {return *_ptr;}
    
    // 重载箭头操作符,返回指针
    T* operator->() {return _ptr;}
    
private:
    // C++98 防拷贝:将拷贝构造函数和拷贝赋值操作符声明为私有且不实现
    UniquePtr(UniquePtr<T> const &);
    UniquePtr & operator=(UniquePtr<T> const &);
    
    // C++11 防拷贝:使用 delete 关键字禁用拷贝构造函数和拷贝赋值操作符
    UniquePtr(UniquePtr<T> const &) = delete;
    UniquePtr & operator=(UniquePtr<T> const &) = delete;
    
private:
    T * _ptr; // 原生指针
};

3.5 shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

std::shared_ptr文档

int main()
{
     // shared_ptr通过引用计数支持智能指针对象的拷贝
     shared_ptr<Date> sp(new Date);
     shared_ptr<Date> copy(sp);
 
     cout << "ref count:" << sp.use_count() << endl;
     cout << "ref count:" << copy.use_count() << endl;
     
 return 0;
}

shared_ptr 的原理:

shared_ptr 是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。以下是其工作原理的详细说明:

  1. 引用计数shared_ptr 在其内部为每个资源维护一份计数,用来记录该资源被多少个对象共享。
  2. 引用计数减少:在对象被销毁时(也就是析构函数调用),表明自己不再使用该资源,对象的引用计数减一。
  3. 释放资源:如果引用计数为 0,就说明当前对象是最后一个使用该资源的对象,此时必须释放该资源。
  4. 引用计数不为 0:如果引用计数不是 0,就说明除了自己还有其他对象在使用该资源,不能释放该资源,否则其他对象就会成为野指针。

#include <thread>
#include <mutex>
#include <iostream>

template <class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr = nullptr)
        : _ptr(ptr)
        , _pRefCount(new int(1))
        , _pMutex(new std::mutex)
    {}

    ~SharedPtr() { Release(); }

    SharedPtr(const SharedPtr<T>& sp)
        : _ptr(sp._ptr)
        , _pRefCount(sp._pRefCount)
        , _pMutex(sp._pMutex)
    {
        AddRefCount();
    }

    // sp1 = sp2
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            // 释放管理的旧资源
            Release();

            // 共享管理新对象的资源,并增加引用计数
            _ptr = sp._ptr;
            _pRefCount = sp._pRefCount;
            _pMutex = sp._pMutex;

            AddRefCount();
        }

        return *this;
    }

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

    int UseCount() { return *_pRefCount; }
    T* Get() { return _ptr; }

    void AddRefCount()
    {
        // 加锁或者使用加1的原子操作
        _pMutex->lock();
        ++(*_pRefCount);
        _pMutex->unlock();
    }

private:
    void Release()
    {
        bool deleteflag = false;

        // 引用计数减1,如果减到0,则释放资源
        _pMutex->lock();
        if (--(*_pRefCount) == 0)
        {
            delete _ptr;
            delete _pRefCount;
            deleteflag = true;
        }
        _pMutex->unlock();

        if (deleteflag == true)
            delete _pMutex;
    }

private:
    int* _pRefCount;       // 引用计数
    T* _ptr;               // 指向管理资源的指针 
    std::mutex* _pMutex;   // 互斥锁
};

int main()
{
    SharedPtr<int> sp1(new int(10));
    SharedPtr<int> sp2(sp1);
    *sp2 = 20;
    std::cout << sp1.UseCount() << std::endl;
    std::cout << sp2.UseCount() << std::endl;

    SharedPtr<int> sp3(new int(10));
    sp2 = sp3;
    std::cout << sp1.UseCount() << std::endl;
    std::cout << sp2.UseCount() << std::endl;
    std::cout << sp3.UseCount() << std::endl;

    sp1 = sp3;
    std::cout << sp1.UseCount() << std::endl;
    std::cout << sp2.UseCount() << std::endl;
    std::cout << sp3.UseCount() << std::endl;

    return 0;
}

std::shared_ptr的线程安全问题

通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 引用计数的线程安全shared_ptr 的引用计数在多个智能指针对象之间共享,两个线程同时对引用计数进行增减操作可能导致计数错乱,因此引用计数的操作需要加锁或使用原子操作来保证线程安全。

  2. 资源访问的线程安全:智能指针管理的对象存放在堆上,两个线程同时访问这些对象会导致线程安全问题。

// 1.演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
// 2.演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,main函数的n改大一些概率就变大
//  了,就容易出现了。
// 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程安全问题,将代码中的SharedPtr
//  换成shared_ptr进行测试,可以验证库的shared_ptr,发现结论是一样的。
void SharePtrFunc(SharedPtr<Date>& sp, size_t n)
{
 cout << sp.Get() << endl;
 for (size_t i = 0; i < n; ++i)
 {
 // 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
 SharedPtr<Date> copy(sp);
 
 // 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但
是最终看到的结果,并一定是加了2n
 copy->_year++;
 copy->_month++;
 copy->_day++;
 }
}
 
int main()
{
 SharedPtr<Date> p(new Date);
 cout << p.Get() << endl;
 
 const size_t n = 100;
 thread t1(SharePtrFunc, p, n);
 thread t2(SharePtrFunc, p, n);
 
 t1.join();
 t2.join();
 
 cout << p->_year << endl;
 cout << p->_month << endl;
 cout << p->_day << endl;
 
 return 0;
}

循环引用分析:

  1. 智能指针和引用计数node1node2 是两个智能指针对象,最初它们的引用计数为 1,这意味着不需要手动 delete 来释放它们的内存。

  2. 指针设置node1_next 成员指向 node2,而 node2_prev 成员指向 node1。此时,它们的引用计数变为 2,因为每个节点都有一个智能指针引用它。

  3. 析构时的引用计数变化:当 node1node2 被析构时,它们的引用计数减少到 1。此时 _next 还指向下一个节点,但 _prev 还指向上一个节点。

  4. 析构影响:如果 _next 析构了,node2 会被释放;如果 _prev 析构了,node1 会被释放。

  5. 循环引用问题:由于 _nextnode1 的成员,而 _prevnode2 的成员,它们之间形成了循环引用。这种循环引用会导致这两个节点无法被释放,因为它们互相持有对方的引用。

  6. 循环引用的后果:在循环引用的情况下,两个节点由于互相持有对方的引用,它们的引用计数永远不会变为 0,因此它们的内存无法被释放。这就是循环引用的问题。

为了解决这种循环引用问题,可以使用 std::weak_ptr 来打破循环引用。std::weak_ptr 是一种不增加引用计数的智能指针,用于观察但不控制对象的生命周期。这样可以防止循环引用导致的内存泄漏。

 b73b4b50c7d7416b881c8d11a4773fd2.png

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
// node1和node2的引用计数。
struct ListNode
{
 int _data;
 weak_ptr<ListNode> _prev;
 weak_ptr<ListNode> _next;
 
 ~ListNode(){ cout << "~ListNode()" << endl; }
};
 
int main()
{
 shared_ptr<ListNode> node1(new ListNode);
 shared_ptr<ListNode> node2(new ListNode);
 cout << node1.use_count() << endl;
 cout << node2.use_count() << endl;
 
 node1->_next = node2;
 node2->_prev = node1;
 
 cout << node1.use_count() << endl;
 cout << node2.use_count() << endl;
 
 return 0;
}

3.6 weak_ptr 

支持管理计数,不支持RAII且不能访问资源。

weak_ptr& operator= (const weak_ptr& x) noexcept;template <class U> weak_ptr& operator= (const weak_ptr<U>& x) noexcept;
template <class U> weak_ptr& operator= (const shared_ptr<U>& x) noexcept;

 C++14多出可变模版参数的版本:

weak_ptr& operator= (weak_ptr&& x) noexcept;template <class U> weak_ptr& operator= (weak_ptr<U>&& x) noexcept;

std::weak_ptr::operator=

weak_ptr 赋值
对象成为 x 的拥有组的一部分,从而在其过期之前可以访问该对象的资源,但自身不获取所有权(并且不会增加其引用计数)。

如果 x 为空,则构造的 weak_ptr 也为空。

如果 x 是别名,则 weak_ptr 会保留被拥有的数据和存储的指针。

shared_ptr 对象可以直接赋值给 weak_ptr 对象,但若要将 weak_ptr 对象赋值给 shared_ptr,则必须通过成员函数 lock 来完成。

std::weak_ptr<ListNode> wp;
{
	std::shared_ptr<ListNode> sp(new ListNode);
	wp = sp;
	cout << sp.use_count() << endl;
	cout << wp.use_count() << endl;
	cout << wp.expired() << endl;
}
cout << wp.use_count() << endl;
cout << wp.expired() << endl;
同时指向统一资源,资源先被释放了,而对象wp生命周期还没结束。

 输出:

1
1
0
~ListNode()
0
1

std::weak_ptrlock 方法在 C++ 中有一个非常重要的作用,它用于从一个 std::weak_ptr 安全地获取一个 std::shared_ptr,从而访问指向的资源。

  • sp 在作用域内被创建时,它管理着 ListNode 资源。如果你调用 wp.lock()sp 继续管理这个资源,引用计数不变或增加。
  • 作用域结束后,sp 的生命周期会结束,如果没有其他 std::shared_ptr 持有该资源,资源会被释放。

 删除器(仿函数实现)

如果不是new出来的对象如何通过智能指针管理呢?

其实shared_ptr设计了一个删除器来解决这个问题 

#include <iostream>
#include <memory>
#include <cstdlib>  // For malloc and free

using namespace std;

// 自定义删除器 - 使用 free 函数释放内存
template<class T>
struct FreeFunc 
{
    void operator()(T* ptr)
    {
        cout << "free:" << ptr << endl;
        free(ptr);
    }
};

// 自定义删除器 - 使用 delete[] 释放内存
template<class T>
struct DeleteArrayFunc 
{
    void operator()(T* ptr)
    {
        cout << "delete[]" << ptr << endl;
        delete[] ptr;
    }
};

int main()
{
    // 使用 FreeFunc 作为删除器的 shared_ptr
    FreeFunc<int> freeFunc;
    shared_ptr<int> sp1((int*)malloc(4), freeFunc);

    // 使用 DeleteArrayFunc 作为删除器的 shared_ptr
    DeleteArrayFunc<int> deleteArrayFunc;
    shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

    return 0;
}

由循环引用引出weak_ptr,用另一个类保管对象的内容,但需要自己明确指定对象中的成员类型是否需要用weak_ptr管理(将资源中的指针套起来)

C++11和boost中智能指针的关系

1. C++ 98 中产生了第一个智能指针 auto_ptr

2. C++ boost给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr

3. C++ TR1,引入了 shared_ptr 等。不过注意的是TR1并不是标准版

4. C++ 11,引入了 unique_ptr 和 shared_ptr 和 weak_ptr(需要注意的是unique_ptr对应boost的 scoped_ptr

并且这些智能指针的实现原理是参考boost中的实现的。

RAII扩展学习 

RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。

#include <thread>
#include <mutex>

// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template<class Mutex>
class LockGuard
{
public:
	LockGuard(Mutex& mtx)
		:_mutex(mtx)
	{
		_mutex.lock();
	}

	~LockGuard()
	{
		_mutex.unlock();
	}

	LockGuard(const LockGuard<Mutex>&) = delete;

private:
	// 注意这里必须使用引用,否则锁的就不是一个互斥量对象
	Mutex& _mutex;
};

mutex mtx;
static int n = 0;

void Func()
{
	for (size_t i = 0; i < 1000000; ++i)
	{
		LockGuard<mutex> lock(mtx);
		++n;
	}
}

int main()
{
	int begin = clock();
	thread t1(Func);
	thread t2(Func);

	t1.join();
	t2.join();

	int end = clock();

	cout << n << endl;
	cout << "cost time:" << end - begin << endl;

	return 0;
}

今天分享就到这里,下期再见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值