本专栏目的

  • 更新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的作用是将mallocfree函数映射到它们的调试版本_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通信等)。
  • 结合计算机运行,在运用层抽象,就有:堆上分配的内存、文件句柄、线程、数据库连接、网络连接、网络套接字、互斥锁和内存等等,这些也分为系统资源

🔼

  • 资源不可能是取之不尽、用之不竭的,从抽象角度,可以归纳为: 获取资源—>使用资源—>释放资源(虽然第一看看起来可能是“废话”,但是随着学习深入,发现这些“废话”有时候却是很重要的)

C/C++语言基础--C++检测内存泄露方法、RALL思想模型-LMLPHP

案例展示

  • 我们来看一下这个案例:
#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;
}

🃏 :

C/C++语言基础--C++检测内存泄露方法、RALL思想模型-LMLPHP

  • 这里本人找了一个高端一点的例子:
#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的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源一定会被释放。

11-23 07:08