智能指针(RAII)
一、内存泄漏
1、介绍
- 内存泄漏(Memory Leak)是计算机科学中的一个常见问题,指的是程序在运行过程中,未能释放那些已经不再使用的内存空间。随着时间的推移,这些未释放的内存会逐渐累积,最终导致程序可用的内存越来越少,可能引发程序运行缓慢、崩溃或者系统资源耗尽等问题。
2、原因
- 申请内存成功,相关工作结束后不释放这些内存,这是最常见的内存泄漏原因。
- 错误的内存管理逻辑,如重复释放内存(double free)、未检查内存是否分配成功就使用等等。
- 缓存策略不当,在某些情况下,程序会缓存数据以提高性能,但如果缓存策略不好,如没有适当的清理机制,就可能导致内存泄漏。
- 使用的第三方库或框架可能存在内存泄漏的问题。
- 作用域问题,如果局部变量(如动态分配的对象)的作用域过大,且未在使用完毕后及时释放,可能导致内存泄漏。
3、泄漏的内存类型分类
- 堆内存泄漏:堆内存指的是程序执行过程中通过malloc、calloc、realloc(C语言)或new(C++语言)等函数从堆上分配的内存。如果这部分内存没有被正确地释放(如忘记调用free或delete),就会导致堆内存泄漏。
- 系统资源泄漏:程序使用系统分配的其他资源,如文件描述符、套接字、数据库连接等等。如果这些资源在使用完毕后没有被正确地释放,如忘记关闭文件、断开网络连接等等,就会导致系统资源泄漏。
二、RAII
1、介绍
- RAII(Resource Acquisition Is Initialization)是一种在C++编程中广泛使用的资源管理技术。它基于C++的构造函数和析构函数的调用机制,确保资源(动态分配的内存、文件句柄、网络连接等等)在对象生命周期内被正确地获取和释放。
2、基本思想
- 资源的获取(Acquisition) 与对象的初始化(Initialization) 绑定在一起。这意味着,当对象被创建时,它会自动获取所需的资源。
- 资源的释放(Release) 与对象的析构(Destruction) 绑定在一起。这意味着,当对象的生命周期结束时,例如,对象被销毁或作用域结束,它的析构函数会自动释放它所管理的资源。
3、优点
- 自动管理资源:RAII通过自动调用构造函数和析构函数来管理资源,减少了手动管理资源的复杂性和出错的可能性。
- 异常安全:在C++中,异常可能会导致函数提前退出。使用RAII,即使发生异常,对象的析构函数也会被调用,从而确保资源被正确地释放。
- 减少内存泄漏:由于资源在对象析构时自动释放,因此可以显著减少内存泄漏的风险。
4、实现方式
- RAII通常通过封装资源的类来实现。这些类通常包含以下部分。
- 构造函数:在构造函数中,类会获取所需的资源。
- 析构函数:在析构函数中,类会释放它所管理的资源。
- 拷贝构造函数和赋值运算符重载(可选,但通常需要禁用或适当实现,以防止资源被错误地复制或赋值):如果对象包含独占资源(如文件句柄),则应该禁用拷贝构造函数和赋值运算符重载,或者实现深拷贝以确保资源被正确管理。
- 关于对应函数的介绍参见类与对象(中)。
三、unique_ptr
1、介绍
- unique_ptr 是C++11引入的一个智能指针,用于管理动态分配的内存,确保资源(如动态分配的对象、数组等)在不再需要时能够被自动释放。
- 与shared_ptr不同,unique_ptr拥有其所指对象的唯一所有权,即两个unique_ptr不能同时指向同一个对象。
2、主要特性
- 唯一所有权:unique_ptr不允许复制(copy),但支持移动(move),这保证了在任何时候只有一个unique_ptr实例拥有对资源的所有权。
- 自动释放:当unique_ptr的实例被销毁时(例如,离开作用域时),它会自动调用其指向对象的析构函数(如果是对象的话),并释放分配的内存。
- 自定义删除器:unique_ptr允许指定一个自定义的删除器,该删除器将在unique_ptr被销毁时调用,以执行资源的释放逻辑。
- 数组支持:unique_ptr有两个特化版本,一个用于单个对象,另一个用于对象数组。对于数组版本,unique_ptr会调用delete[]而不是delete来释放内存。
3、注意事项
- 由于unique_ptr不支持复制,因此不能直接将一个unique_ptr赋值给另一个同类型的unique_ptr,否则会导致编译错误。但是,可以使用move来转移所有权。
- 当使用unique_ptr管理动态分配的数组时,需确保使用unique_ptr<T[]>的特化版本,以便正确调用delete[]而不是delete。
- unique_ptr是轻量级的,并且通常比shared_ptr有更好的性能,因为它不涉及额外的开销(如控制块)来管理共享所有权。然而,这也意味着它不能用于需要在多个所有者之间共享资源的场景。
4、unique_ptr类
5、示例代码
class Test_unique_ptr_Class {
public:
Test_unique_ptr_Class() { std::cout << "MyClass created\n"; }
~Test_unique_ptr_Class() { std::cout << "MyClass destroyed\n"; }
void Func() { std::cout << "Doing something...\n"; }
};
void Test_unique_ptr1()
{
std::unique_ptr<Test_unique_ptr_Class> ptr = std::make_unique<Test_unique_ptr_Class>();
ptr->Func();
}
void Test_unique_ptr2()
{
std::unique_ptr<int[]> arr = std::make_unique<int[]>(4);
for (int i = 0; i < 4; ++i) {
arr[i] = i * i;
}
}
int main()
{
Test_unique_ptr1();
Test_unique_ptr2();
return 0;
}
6、运行结果
7、简单实现
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "~unique_ptr()" << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
四、shared_ptr
1、介绍
- shared_ptr是C++标准库中的一个智能指针模板类,用于自动管理具有共享所有权的动态分配的对象。通过使用shared_ptr可以避免手动管理内存(如使用new和delete),从而减少内存泄漏的风险。
2、主要特点
- 共享所有权:多个shared_ptr实例可以指向同一个对象。当最后一个指向该对象的shared_ptr被销毁或重置时,对象会被自动删除。
- 引用计数:shared_ptr使用一个内部计数器来跟踪有多少个shared_ptr实例指向某个对象。每当一个新的shared_ptr被创建并指向该对象时,计数器递增;每当一个shared_ptr被销毁或重置时,计数器递减。当计数器达到零时,对象被删除。
- 线程安全:shared_ptr的引用计数操作是线程安全的,允许多个线程安全地共享对象。但是,这并不意味着指向的对象本身是线程安全的;如果多个线程需要修改对象的状态,则需要额外的同步机制。
3、注意事项
- 性能开销:虽然shared_ptr提供了方便的内存管理,但它也引入了额外的性能开销(如引用计数的维护)。在性能敏感的应用中,需要权衡使用shared_ptr的利弊。
- 自定义删除器:shared_ptr允许指定一个自定义的删除器,用于在对象被销毁时执行特定的清理操作。这可以通过传递一个删除器给shared_ptr的构造函数来实现。
4、shared_ptr类
5、简单实现
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new atomic<int>(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_del(del)
, _pcount(new atomic<int>(1))
{}
void Release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
Release();
}
T* GetPtr() const
{
return _ptr;
}
int GetUseCount() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
五、weak_ptr
1、介绍
- weak_ptr是C++11引入的一种智能指针,它用于解决shared_ptr之间的循环引用问题。当两个或多个shared_ptr相互引用时,就会形成一个循环引用,导致这些shared_ptr所指向的对象无法被正确删除,因为每个shared_ptr都认为至少还有一个其他shared_ptr指向该对象。而weak_ptr的设计就是为了解决这个问题。
- weak_ptr不拥有其所指向的对象,它不会增加对象的所有权计数,即shared_ptr中的引用计数。相反,weak_ptr提供了一种方式来观察shared_ptr所管理的对象,但不阻止该对象被销毁。
- weak_ptr可以从shared_ptr或另一个weak_ptr中创建,但不能直接用来访问对象;它必须首先被锁定为一个shared_ptr,这通过调用weak_ptr的lock()方法实现。如果原始shared_ptr已经被销毁,lock()将返回一个空的shared_ptr。
2、weak_ptr类
3、shared_ptr的循环引用问题
- 在C++中,当使用shared_ptr时,一个常见的问题是循环引用(circular reference),这发生在两个或多个shared_ptr实例相互引用对方。由于每个shared_ptr都认为至少有一个其他shared_ptr正在引用其管理的对象,因此它们的引用计数永远不会降为零,这导致内存无法被正确释放,从而产生内存泄漏。
4、循环引用示例代码
class B;
class A {
public:
//std::shared_ptr<B> bPtr;
std::weak_ptr<B> bPtr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> aPtr;
~B() { std::cout << "B destroyed\n"; }
};
void Test_shared_ptr5()
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 创建循环引用
a->bPtr = b;
b->aPtr = a;
}
- 上方代码为循环引用问题解决后的代码,循环引用问题的代码为将A类中的bPtr改为shared_ptr类型。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕