1.引入
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;
}
上面的代码存在内存泄漏的问题
2. 内存泄漏
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
3.智能指针的使用及原理
RAII
RAII的思想
RAII(Resource Acquisition Is Initialization)是一种常用的编程技术,主要在C++语言中使用,其核心思想是将资源的管理(如内存、文件句柄、网络连接等)与对象的生命周期绑定。以下是RAII思想的几个关键点:
资源封装:将资源封装在一个对象的构造函数中获取,并在析构函数中释放。这样,资源的生命周期就与对象的生存期同步。
对象生命周期管理:利用C++的对象生命周期机制(创建、使用、销毁)来管理资源。当对象创建时获取资源,当对象被销毁时释放资源。
异常安全:RAII提供了一种异常安全的编程方式。即使发生异常,C++的栈展开机制(stack unwinding)也会保证局部对象被正确地销毁,从而释放它们所管理的资源,防止资源泄漏。
简单性:RAII简化了资源管理,程序员不需要关注在程序的每一个退出点手动释放资源,减少了错误发生的可能性。
具体来说,RAII遵循以下原则:
构造函数获取资源:在对象的构造函数中,完成资源的分配和初始化。
析构函数释放资源:在对象的析构函数中,完成资源的释放和清理工作。
避免资源泄露:通过对象栈上的自动生命周期管理,或者智能指针等 RAII 类型的对象堆上的生命周期管理,确保资源总是被释放。
对象拷贝与资源管理:在实现拷贝构造函数和拷贝赋值运算符时,要特别注意资源的正确管理,通常采用“拷贝构造”或“引用计数”等技术。
RAII是C++中管理资源推荐的方式,与智能指针(如std::unique_ptr和std::shared_ptr)结合使用,可以大大提高程序的安全性和可靠性。
将指针交给智能指针之后,那么资源的释放就十分可靠
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
ShardPtr<int> sp1(new int);
ShardPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
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<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
智能指针的主要类型包括:
std::auto_ptr
(C++98中引入,C++17中已废弃)std::unique_ptr
(C++11中引入,用于独占资源所有权)std::shared_ptr
(C++11中引入,用于共享资源所有权)std::weak_ptr
(C++11中引入,用于解决shared_ptr
的循环引用问题)
下面我们依次介绍这些ptr类
auto_ptr
// C++98
// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
// 很多公司都明确规定了,不要用这个
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~auto_ptr()
{
delete _ptr;
_ptr = nullptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 拷贝构造函数(转移权限) //p1(p2)
auto_ptr(const auto_ptr<T>& p2)
: _ptr(p2._ptr)
{
p2._ptr = nullptr; //p2置空,但是保留p2的资源
}
private:
T* _ptr;
};
一句话总结auto_ptr:设计依托答辩,转移指针使得指针悬空,被业内人士喷烂了!
如何在CPP理解悬空
在C++中,"悬空"通常指的是指针悬空(Dangling Pointer)的情况,这是一个内存管理的问题。指针悬空发生在以下几种情况:
指向已删除的对象:当指针所指向的对象被删除后,如果没有重置指针,那么这个指针就变成了悬空指针。
指向超出作用域的对象:局部对象在离开其作用域后,指向它的指针也会悬空。
栈内存被释放:如果指针指向的栈内存被释放,但指针未置空,那么该指针也会悬空。
理解悬空的概念并避免它,是编写安全、可靠的C++代码的重要部分。以下是一些避免悬空指针的建议:
如何理解并避免悬空指针:
初始化指针:在声明指针时就初始化为nullptr,确保指针不会指向不确定的内存地址。
删除后置空:在删除一个对象后,立即将指针赋值为nullptr,这样可以避免悬空指针的问题。
作用域意识:确保在函数或代码块结束时,释放所有动态分配的资源,并将指针置为nullptr。
智能指针:使用C++的智能指针(如std::unique_ptr, std::shared_ptr)来自动管理内存,它们会在适当的时机自动释放所拥有的对象,并置空。
资源管理类:使用资源管理类(如RAII),在对象生命周期结束时自动释放资源。
避免野指针:不要使用未初始化的指针。
下面是一个简单的示例,展示了如何避免悬空指针:
#include <iostream>
int main() {
int* ptr = nullptr; // 初始化指针
{
int var = 5;
ptr = &var; // 指针指向局部变量
} // var离开作用域,其内存被释放
// ptr现在是悬空指针,因为var已经不再存在
// 应该避免使用ptr
ptr = nullptr; // 将指针置为nullptr,避免悬空
if (ptr == nullptr) {
std::cout << "指针现在是安全的,没有被悬空。" << std::endl;
}
return 0;
}
在这个例子中,通过在局部变量var作用域结束前将ptr置为nullptr,我们避免了悬空指针的问题。在实际编程中,应该始终警惕并妥善管理指针,以防止悬空指针引起的潜在错误和程序崩溃。
std::unique_ptr
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete->" << _ptr << endl;
delete _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 拷贝构造函数(转移权限) //p1(p2)
unique_ptr(const unique_ptr<T>& p2) = delete; //禁用拷贝构造时,最好将赋值重载也禁用
unique_ptr<T>& operater=(const unique_ptr<T>& p2) = delete;
private:
// C++98
// 1、只声明不实现
// 2、限定为私有
//unique_ptr(const unique_ptr<T>& up);
//unique_ptr<T>& operator=(const unique_ptr<T>& up);
private:
T* _ptr;
};
std::shared_ptr
不能用类内成员,也不可以用static成员来记录引用计数,否则会出现故障!
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1)) //内容初始化为1
{}
template<class D> //定制删除其他类型
shared_ptr(T* ptr, D deleter)
:_ptr(ptr)
,_pcount(new int(1))
,_deleter(deleter)
{}
~shared_ptr()
{
release();
}
void release()
{
if (--(*_pcount) == 0) //先--
{
_deleter(_ptr); //再调用删除器(不定制删除器时,默认调用_deleter)
delete _pcount; //最后释放计数器(_pcount不应该由外界传入释放器,应该内部维护)
}
}
//sp1(sp2)
share_ptr(const shared_ptr<T>& sp2)
:_ptr(sp2._ptr)
,_pcount(sp2._pcount)
,_deleter(sp2._deleter)
{
*_pcount += 1; //计数器加1
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp2)
{
if (sp2._ptr == _ptr) //避免自我赋值
return *this;
release(); //先对自身进行一次release
_ptr = sp2._ptr;
_pcount = sp2._pcount;
_deleter = sp2._deleter;
*_pcount += 1; //计数器加1
return *this;
}
// 像指针一样( = 、 * 、 -> )
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //应该额外在堆区开一块空间,否则新建一个智能指针,计数器就不对了(不能将计数器属于单独的类,而是属于一个资源块)
function<void(T*)> _deleter = [](T* ptr) { delete ptr; }; //自定义删除器
}; //捕捉 参数列表 函数体
使用:
n1->next 本质去调用n1这个智能指针的operator->,得到了一个_ptr(n1的成员变量,存储了传入类型new之后的地址)的地址,所以->本质还是对传入类型的指针(原生指针)进行的解引用操作。
即智能指针类对原生指针进行了封装,当智能指针对象进行解引用操作时,本质还是元省指针类型的解引用操作。
所以智能指针可以对任意类型的原生指针进行封装,智能指针进行任意指针操作,功能等都与原生指针一致。
因此,智能指针的行为与原生指针非常相似,但具有额外的内存管理和异常安全性功能。
因此,shared_ptr<//类型>可以理解为对于类型的指针 ,后面的名字可理解为指针的名字
为了解决循环引用,引入了weak_ptr
shared_ptr的循环引用
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_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;
}
解决方案:在循环引用的智能指针处,将对应的智能指针改成weak_ptr,进行弱引用。
/*
为什么程序能正常结束,但是没有打印~ListNode。(如果一直持续在循环中,按理说程序无法正常结束)
回归本质:从底层看引用循环的真正原因
N1和n2都是封装了ListNode的智能指针
最终结果只能是,引用--,而永远无法执行delete ptr,而ptr才是真正指向结构的指针,所以无法打印~ListNode
而不只是简单的:
N1的next是n2,n2的prev是n1.
想要彻底析构n2,那就得将n2的prev析构,即需要先析构n1
想要彻底析构n1,那就的将n1的next析构,即需要先析构n2
*/
weak_ptr的实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_sp(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp) //拷贝和operater=并没有增加引用计数
:_sp(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_sp = sp.get();
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _sp; //弱引用,指向shared_ptr的指针,不能改变shared_ptr的引用计数
};
可以看到weak_ptr在 赋值和拷贝处并没有增加引用,因此不会出现循环引用!
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;
}
对于ListNode实例化的node1 node2正常用shared_ptr维护。只不过在引用计数发生的关键指针
_prev和_next处,采用weak_ptr进行维护。
shared_ptr是可以赋值给weak_ptr的!
Weak_ptr不支持RAII
weak_ptr 本身并不直接支持 RAII(Resource Acquisition Is Initialization)原则,这是因为它不拥有它所监视的资源。RAII 是一种 C++ 资源管理策略,它通过对象的生命周期来管理资源,确保资源在对象析构时被正确释放。
以下是 weak_ptr 与 RAII 的关系:
资源管理:weak_ptr 不直接管理资源。它只是提供了一个观察 shared_ptr 管理的资源的手段。因此,它不需要在析构时释放资源,因为它不拥有资源。
使用场景:weak_ptr 通常用于解决 shared_ptr 可能引起的循环引用问题。当 weak_ptr 观察的对象不再被任何 shared_ptr 所拥有时,即使 weak_ptr 存在,对象也会被销毁。
RAII 实践:尽管 weak_ptr 本身不直接实现 RAII,但它可以与 shared_ptr 一起使用来实现 RAII。shared_ptr 负责资源的获取和释放,而 weak_ptr 可以用来打破循环引用,从而确保 shared_ptr 能够正确释放资源。
转换:当需要操作 weak_ptr 观察的资源时,可以通过调用 lock() 方法将 weak_ptr 转换为 shared_ptr。如果资源仍然存在,lock() 将返回一个有效的 shared_ptr,这样就可以安全地访问资源。如果资源已经被销毁,lock() 将返回一个空的 shared_ptr。
总结来说,虽然 weak_ptr 本身不支持 RAII,但它与 shared_ptr 结合使用,可以维护 RAII 原则,并帮助解决智能指针在复杂场景下可能遇到的资源管理问题。
在上述的代码例子中:
node1->_next = node2;
node2->_prev = node1;
weak_ptr维护的_next 和_prev并不会设计RAII,但是node1 node2是被shared_ptr所维护的,RAII由对应的shared_ptr实现
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题
template<class D> //定制删除其他类型
shared_ptr(T* ptr, D deleter)
:_ptr(ptr)
,_pcount(new int(1))
,_deleter(deleter)
{}
我们内部实现了定制删除器,当不传入时,调用默认的定制删除器(用function包装的一个lambda表达式),完成资源的清理。
当外界传入定制删除器时,传入的ptr将交给这个定制删除器。
问题:
如果是new出来的指针,那么shared_ptr可以自动在内部将指针delete,如果不是new出来的指针(比如malloc),那么就需要进行定制删除器去释放空间吗?(也就是说shared_ptr会把维护的指针交给定制删除器去释放吗?)
是的,shared_ptr 默认情况下会使用 delete 操作符来释放它所管理的对象。这是因为在 C++ 中,new 操作符与 delete 操作符是一对,用来分配和释放动态内存。
当你使用 new 来分配内存时,shared_ptr 可以确保当引用计数变为零时,使用 delete 来正确地释放内存。然而,如果你使用 malloc 来分配内存,那么你应该使用 free 来释放它,而不是 delete。在这种情况下,你需要提供一个定制删除器(custom deleter)给 shared_ptr,以确保内存被正确地释放。
#include <memory>
#include <cstdlib> // for free
// 自定义删除器,使用 free 释放内存
void customDeleter(void* ptr) {
free(ptr);
}
int main() {
// 使用 malloc 分配内存
int* rawPtr = static_cast<int*>(malloc(sizeof(int)));
*rawPtr = 10;
// 创建 shared_ptr,并传递自定义删除器
std::shared_ptr<int> sharedPtr(rawPtr, customDeleter);
// 使用 shared_ptr
std::cout << *sharedPtr << std::endl;
// 当 sharedPtr 离开作用域或被 reset 时,customDeleter 会被调用,从而使用 free 释放内存
return 0;
}
在上面的代码中,我们定义了一个 customDeleter 函数,它接受一个 void* 类型的指针,并使用 free 来释放它。然后,我们创建了一个 shared_ptr,并将 rawPtr 和 customDeleter 作为参数传递给它。当 sharedPtr 的引用计数变为零时,它会调用 customDeleter 来释放内存。
注意,如果你使用 malloc 分配内存,并试图通过 shared_ptr 使用 delete 来释放它,这将导致未定义行为,因为 delete 期望的是通过 new 分配的内存。因此,使用正确的定制删除器是非常重要的。
定制删除器:可以用lambda表达式、可以用仿函数(可见是function<void(T*)>包装器发力了)