本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- C++是面向对象的语言,但是更像是面向内存的语言,内存问题一直是C++程序员一直要注意的问题,本篇文章简单的介绍了一下检测内存泄露方法、RALL思想等
- 本篇文章也是为了后面更新指针指针做铺垫;
- C语言后面也会继续更新知识点,如内联汇编;
- 欢迎收藏 + 关注,本人将会持续更新。
VS中内存泄漏检测方法
首先来看一下内存泄漏的案例,以及如何检测内存泄漏。
1、实质:
🍸: 内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费;
⏰ :这句话的以上,看一个程序就明白了:
void test()
{
int* a = (int*)malloc(sizeof(int)); // 申请了一块内存
*a = 10; // 赋值
}
- 假设:在test作用域中没有释放申请的内存,这个时候我们在后面就无法操作这个申请内存的空间了,但是由于这块内存一直在,没有释放,他就会一直占有这块内存空间,这样就导致了内存泄露。
2、原理:
内存泄露的关键就是跟踪每一块内存的生命周期,记录分配的内存和释放内存的操作,看看能不能匹配,
3、方法:不同开发环境有不同的检测方法,在VS中使用时,需加上
#define _CRTDBG_MAP_ALLOC
#include<crtdbg.h>
crtdbg.h
的作用是将malloc
和free
函数映射到它们的调试版本_malloc_dbg
和_free_dbg
,这两个函数将跟踪内存分配和释放。
_CrtDumpMemoryLeaks(); //内存检测函数
注意:要点击调试才能看出
⚓️
//Vs中检测内存泄漏代码,C/C++通用
#ifdef _DEBUG
#ifdef __cplusplus
#include<iostream>
#define new new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define _CRTDBG_MAP_ALLOC
#include<malloc.h>
#include<crtdbg.h>
#include<stdlib.h>
#endif // __cplusplus
#else
#include<malloc.h>
#endif
//用于atexit注册,会在程序退出时自动调用
void Exit()
{
_CrtDumpMemoryLeaks();
}
atexit 函数
- atexit()用来设置一个程序正常结束前调用的函数
测试代码
int main()
{
atexit(Exit);
int* p = new int[5];
//delete[] p;
return 0;
}
/* 输出窗口中显示:
Detected memory leaks! // 内存泄露
Dumping objects ->
{157} normal block at 0x00C5E690, 20 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
*/
RAII
什么是RAII
RAII (Resource Acquisition Is Initialization),也称为**“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法**。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。
🛰 :RAII
就是在创建出一个对象后,保证这个对象在他的有效生命周期中一直存在,一旦离开了他的生命周期,就会释放掉,结合类的构造和析构函数,==可以简要的说:==在构造函数中申请的内存没在析构函数中释放⚜️⚜️⚜️.
🍇
RAII技术被认为是C++中管理资源的最佳方法,进一步引申,使用RAII技术也可以实现安全、简洁的状态管理,编写出优雅的异常安全的代码,它利用栈对象在离开作用域后自动析构的语言特点,将受限资源的生命周期绑定到该对象上,当对象析构时以达到自动释放资源的目的。
资源管理问题
- 小编认为主要归纳为两=三个:算力(CPU、GPU等),存储(文件,磁盘,寄存器,内存等),网络通信(基于socket通信等)。
- 结合计算机运行,在运用层抽象,就有:堆上分配的内存、文件句柄、线程、数据库连接、网络连接、网络套接字、互斥锁和内存等等,这些也分为系统资源。
🔼
- 资源不可能是取之不尽、用之不竭的,从抽象角度,可以归纳为: 获取资源—>使用资源—>释放资源(虽然第一看看起来可能是“废话”,但是随着学习深入,发现这些“废话”有时候却是很重要的)
案例展示
- 我们来看一下这个案例:
#include <iostream>
using namespace std;
const int N = 100;
int main()
{
int* arr = new int[N];
/*
……………………………… 干活
*/
delete[] arr; // 释放
arr = nullptr;
return 0;
}
👁🗨 这个案例是一个很简单的一个例子,申请了一块内存,然后释放,但是如果“干活那段代码”
很多,那我们是很容易忘记释放内存的❓❓❓
- 再来一个例子(官方的案例):
#include <iostream>
using namespace std;
bool OperationA();
bool OperationB();
int main()
{
int *testArray = new int [10];
// Here, you can use the array
if (!OperationA())
{
// If the operation A failed, we should delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
if (!OperationB())
{
// If the operation A failed, we should delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
// All the operation succeed, delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
bool OperationA()
{
/*Do some operation, if the operate succeed, then return true, else return false */
return false ;
}
bool OperationB()
{
/*Do some operation, if the operate succeed, then return true, else return false*/
return true ;
}
👓 上面的案例是一个比较常用的逻辑架构,在主函数main
中,当我们对不同情况做不同处理的时候,根据实际场景去释放 testArray 这个数组这块内存,如果所有实际场景都没有满足,则最后释放,从逻辑的角度上说,这个没有任何问题,但是从代码角度上说,不够简介,比较臃肿,
如何使用RAII
RALL
核心思想:在使用的时候申请资源,并且保证在他的有效作用域中资源不被释放,但是一旦离开了这个作用域,资源就会立刻释放。
🏗 ,使用RALL
思想,主要可以靠这两个地方实现:
- 当我们在一个函数内部使用局部变量,利用栈的特性实现。
- 当这个变量是类对象时,利用构造和析构函数特性实现。
:
#include <iostream>
using namespace std;
template <class T>
class Array
{
public:
// 构造函数中申请内存
Array(T n)
:m_n(n)
{
m_data = new T[n];
}
// 析构函数中释放内存
~Array()
{
delete[] m_data;
cout << __FUNCTION__ << endl;
}
private:
int m_n;
T* m_data;
};
int main()
{
Array<int> array(10);
return 0;
}
🃏 :
- 这里本人找了一个高端一点的例子:
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
CRITICAL_SECTION cs;
int gGlobal = 0;
class MyLock
{
public:
MyLock()
{
EnterCriticalSection(&cs);
}
~MyLock()
{
LeaveCriticalSection(&cs);
}
private:
MyLock( const MyLock &);
MyLock operator =(const MyLock &);
};
void DoComplex(MyLock &lock )
{
}
unsigned int __stdcall ThreadFun(PVOID pv)
{
MyLock lock;
int *para = (int *) pv;
DoComplex(lock);
for (int i = 0; i < 10; ++i)
{
++gGlobal;
cout<< "Thread " <<*para<<endl;
cout<<gGlobal<<endl;
}
return 0;
}
int main()
{
InitializeCriticalSection(&cs);
int thread1, thread2;
thread1 = 1;
thread2 = 2;
HANDLE handle[2];
handle[0] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread1, 0, NULL );
handle[1] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread2, 0, NULL );
WaitForMultipleObjects(2, handle, TRUE , INFINITE );
return 0;
}
这个例子可以说是实际项目的一个模型,当多个进程访问临界变量时,为了不出现错误的情况,需要对临界变量进行加锁;上面的例子就是使用的Windows的临界区域实现的加锁。但是,在使用CRITICAL_SECTION时,EnterCriticalSection和LeaveCriticalSection必须成对使用,很多时候,经常会忘了调用LeaveCriticalSection,此时就会发生死锁的现象。当我将对CRITICAL_SECTION的访问封装到MyLock类中时,之后,我只需要定义一个MyLock变量,而不必手动的去显示调用LeaveCriticalSection函数。
总结
RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源一定会被释放。