目录
[1. 为什么需要智能指针?](#1. 为什么需要智能指针?)
[2. 什么是内存泄漏?](#2. 什么是内存泄漏?)
[3.回忆 this](#3.回忆 this)
[二. 原理](#二. 原理)
[1. RAII 资源获取即初始化](#1. RAII 资源获取即初始化)
[三. 使用](#三. 使用)
[1. 问题: string 的浅拷贝](#1. 问题: string 的浅拷贝)
[自定义 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
操作时抛出异常(例如内存不足),没有捕获到的 p1
和 p2
将导致内存泄漏。
例如:
抛异常后,会直接跳转到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++程序中,内存泄漏主要分为两类:
- 堆内存泄漏(Heap leak)
堆内存是通过malloc
/calloc
/realloc
/new
等分配的内存,用完后必须通过free
或者delete
释放。若程序设计错误导致内存没有释放,则会产生堆内存泄漏,导致内存浪费。 - 系统资源泄漏
程序使用的系统资源(如套接字、文件描述符、管道等)若未使用相应的函数释放,会导致系统资源浪费,严重时可能会导致系统性能下降或不稳定。
如何检测内存泄漏
- Linux下检测
使用Linux平台的内存泄漏检测工具。 - Windows下检测
使用第三方工具,例如VLD(Visual Leak Detector)。 - 其他工具
可以使用内存泄漏工具对比不同检测工具的优劣,选择合适的工具。
如何避免内存泄漏
- 良好的设计规范
在工程的前期,遵循良好的编码规范,确保所有申请的内存都能被及时释放。 - RAII思想与智能指针
使用RAII(Resource Acquisition Is Initialization)思想或智能指针来管理内存与资源,避免手动释放内存时的遗漏和错误。 - 私有内存管理库
有些公司会开发自己的私有内存管理库,这些库通常带有内存泄漏检测功能。 - 使用内存泄漏检测工具
在出问题时,使用内存泄漏工具进行检测和调试,确保问题能及时修复。
总结:
- 事前预防:使用智能指针等技术。
- 事后检测:使用内存泄漏检测工具。
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
类有两个方法setFont
和setSize
,它们都接受参数并修改对象的内部状态。每个方法在修改状态后都返回this
指针的引用,这样就可以在调用一个方法后立即调用另一个方法,形成一个链式调用。
这里是详细解释:
setFont
方法接受一个字符串参数font
,并将其存储在私有成员变量font
中。之后,它返回*this
,即对象自身的引用。setSize
方法接受一个整数参数size
,并将其存储在私有成员变量size
中。同样,它返回*this
。- 在
main
函数中,我们创建了一个Printer
对象,并通过链式调用的方式设置了字体和大小,然后调用print
方法来打印结果。
通过返回this
指针,我们能够构建出流畅的接口,让代码更加易读和易用。
二. 原理
⭕ 智能指针的原理:
- RAII特性 :智能指针通过构造和析构自动管理资源,避免手动释放内存。
- 重载操作符 :通过重载
*
和->
操作符,智能指针能够像普通指针一样使用,具备解引用和访问对象成员的能力。
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_ptr
、shared_ptr
和weak_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
-
sp1._pcount--
-
赋值
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; }
特例的解决:
- sp3=sp3 会出现野指针
- sp4 =sp5
在最开始的时候,最好用资源来判定一下
⭕ shared_ptr 的赋值 sum
- 避免对同一份资源拷贝,进行一下判断要
- 左边资源要注意是不是最后一个了
- 最后 赋值拷贝
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;
}
- 我们创建了两个节点 n1 和 n2,并且设置了 _next 和 _prev 的链接。
- 通过使用 weak_ptr::lock() 来安全地获取 shared_ptr,避免直接使用 weak_ptr 导致的问题。
- 在退出 main 函数前,n1 和 n2 会被销毁,因为没有引用计数,所以内存都可以正常释放。
解决原理:
使 _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_ptr
、shared_ptr
和 weak_ptr
等不同类型的智能指针提供了丰富的功能,以应对不同的场景需求。理解并合理使用智能指针,是现代 C++ 编程的基本功之一。