C++内存管理分析

malloc/calloc/realloc的区别?

1. malloc
函数原型:
void *malloc(size_t size);
函数功能:
malloc()在内存的动态存储区中分配一块长度为size字节的连续区域。参数size为需要的内存空间的长度,返回该区域的地址。
区别:
malloc不能初始化所分配的内存空间,需要用memset,而函数calloc能初始化。如果这部分内存曾经被分配过,则其中可能遗留各种各样的数据。
2. calloc
函数原型:
void *calloc(size_t nmemb, size_t size);
函数功能:
calloc()与malloc()相似,参数size为申请地址的单位元素长度,nmemb为参数个数。
区别:
calloc会将所分配的空间中的每一位都初始化为零。
3. realloc
函数原型:
void *realloc(void *ptr, size_t size);
函数功能:
realloc()是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址空间。
区别:
realloc可以对给定的指针所指向的空间进行扩大或缩小,原有的内存中的内容将保持不变。realloc并不保持调整后的内存空间和原来的内存空间保持同一内存地址,返回的指针很可能指向新的地址。

内存泄漏?如何检测内存泄漏?

C++中的内存泄露一般指堆中的内存泄露。堆内存是我们手动malloc/realloc/new申请的,程序不会自动回收,需要调用free或delete手动释放,否则就会造成内存泄露。

内存泄露的关键就是记录分配的内存和释放内存的操作,看看能不能匹配。跟踪每一块内存的生命周期。
例如:每当申请一块内存后,把指向它的指针加入到List中,当释放时,再把对应的指针从List中删除,到程序最后检查List就可以知道有没有内存泄露了。Window平台下的Visual Studio调试器和C运行时(CRT)就是用这个原理来检测内存泄露。
在VS中使用时,需加上

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
...
_CrtDumpMemoryLeaks();

crtdbg.h的作用是将malloc和free函数映射到它们的调试版本_malloc_dbg和_free_dbg,这两个函数将跟踪内存分配和释放(在Debug版本中有效)。

_CrtDumpMemoryLeaks函数将显示当前内存泄露,也就是说程序运行到此行代码时的内存泄露,所有未销毁的对象都会报出内存泄露,因此要让这个函数尽量放到最后。

举例如下:

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    char *str1 = NULL;
    char *str2 = NULL;
    str1=new char[100];
    str2=new char[50];

    delete str1;

    _CrtDumpMemoryLeaks();

    return 0;

}

上述代码中,我们只释放了一块内存,运行调试,会在output窗口输出:
这里写图片描述

可以看到检测到了内存泄露。
但是并没有检测到泄露内存申请的位置,我们已经加了宏定义#define _CRTDBG_MAP_ALLOC。原因是申请内存用的是new,而刚刚包含头文件和加宏定义是重载了malloc函数,并没有重载new操作符,所以要自己定义重载new操作符才能检测到泄露内存的申请位置。修改如下:

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG    //重载new
#define new  new(_NORMAL_BLOCK, __FILE__, __LINE__)  
#endif
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    char *str1 = NULL;
    char *str2 = NULL;
    str1=(char*)malloc(100);
    str2=new char[50];

    _CrtDumpMemoryLeaks();

    return 0;
}

我们再看调试结果:
这里写图片描述

源.cpp()括号里面的数字就是泄露内存的起始位置。
后面的{144} normal block at 0x0361B1C0, 50 bytes long代表什么呢?

大括号{}里面的数字表示第几次申请内存操作;0x0361B1C0表示泄露内存的起始地址,CD CD表示泄露内存的内容。

调用long _CrtSetBreakAlloc(long nAllocID)可以再第nAllocID次申请内存时中断,在中断时获取的信息比在程序终止时获取的信息要多,你可以调试,查看变量状态,对函数调用调试分析,解决内存泄露。

block分为3中类型,此处为normal,表示普通,此外还有client表示客户端(专门用于MFC),CRT表示运行时(有CRT库来管理,一般不会泄露),free表示已经释放掉的块,igore表示要忽略的块。

在上面程序中,调用_CrtDumpMemoryLeaks()来检测内存泄露,如果程序可能在多个地方终止,必须在多个地方调用这个函数,这样比较麻烦,可以在程序起始位置调用_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF| _CRTDBG_LEAK_CHECK_DF),这样无论程序何时终止,都会在终止前调用_CrtDumpMemoryLeaks()。

除此之外,还可以在某时刻设置检查点,获取当时内存状态的快照。比较不同时刻内存状态的差异。

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG //重载new
#define new  new(_NORMAL_BLOCK, __FILE__, __LINE__)  
#endif
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    _CrtMemState s1, s2, s3;
    char *str1 = NULL;
    char *str2 = NULL;
    str1=(char*)malloc(100);
    _CrtMemCheckpoint( &s1 );//记录内存快照
    _CrtMemDumpStatistics( &s1 );//输出
    str2=new char[50];
    _CrtMemCheckpoint( &s2 );
    _CrtMemDumpStatistics( &s2 );

    if ( _CrtMemDifference( &s3, &s1, &s2) )//比较s1和s2,把比较结果输出到s3
    _CrtMemDumpStatistics( &s3 );// dump 差异结果

    return 0;
}

调试结果:
这里写图片描述

new/delete操作符

我们都知道,new/delete是对于单个对象空间,new[]/delete[]是对于连续空间,且必须配对使用。
先看几个C++语言标准库的库函数:

void *operator new(size_t);     //allocate an object
void *operator delete(void *);    //free an object

void *operator new[](size_t);     //allocate an array
void *operator delete[](void *);    //free an array

看一个例子:

class A
{
public:
    A(int v) : var(v)
    {
        fopen_s(&file, "test", "r");
    }
    ~A()
    {
        fclose(file);
    }

private:
    int var;
    FILE *file;
};

上面类A中有两个私有成员,有一个构造函数和一个析构函数,构造函数中初始化私有变量 var 以及打开一个文件,析构函数关闭打开的文件。

我们使用class A *pA = new A(10);来创建一个类的对象,返回其指针 pA。

new 背后完成的工作:
  1. 首先需要调用上面提到的 operator new 标准库函数,传入的参数为 class A 的大小,这里为 8 个字节。
  2. 上面分配的内存是未初始化的,也是未类型化的,第二步就在这一块原始的内存上对类对象进行初始化,调用的是相应的构造函数,这里是调用 A::A(10); 这个函数,var=10, file 指向打开的文件。
  3. 最后一步就是返回新分配并构造好的对象的指针,这里 pA 就指向这块内存,pA 的类型为类 A 对象的指针。

那么 delete 都干了什么呢?还是接着上面的例子,如果这时想释放掉申请的类的对象怎么办?我们使用下面的语句来完成:
delete pA;

delete 所做的事情:
  1. 调用 pA 指向对象的析构函数,对打开的文件进行关闭。
  2. 通过上面提到的标准库函数 operator delete 来释放该对象的内存,传入函数的参数为 pA 的值。
如何申请和释放一个数组?

我们经常要申请一个数组,如下:

string *psa = new string[10];      //array of 10 empty strings
int *pia = new int[10];           //array of 10 uninitialized ints

上面在申请一个数组时都用到了new[]这个表达式来完成:
第一个数组是 string 类型,分配了保存对象的内存空间之后,将调用 string 类型的默认构造函数依次初始化数组中每个元素;
第二个是申请具有内置类型的数组,分配了存储 10 个 int 对象的内存空间,但并没有初始化。

如果我们想释放空间了,可以用下面两条语句:

delete []psa;
delete []pia;

都用到 delete[]表达式,注意这地方的 [] 一般情况下不能漏掉。
我们也可以想象这两个语句分别干了什么:第一个对 10 个 string 对象分别调用析构函数,然后再释放掉为对象分配的所有内存空间;
第二个因为是内置类型不存在析构函数,直接释放为 10 个 int 型分配的所有内存空间。

我们如何知道 psa 指向对象的数组的大小?怎么知道调用几次析构函数?
C++ 的做法是在分配数组空间时多分配了4个字节的大小,专门保存数组的大小,然后才是对象数组指针的地址,在delete[]时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

我们需要注意:
  1. 调用析构函数的次数是从数组对象指针前面的 4 个字节中取出;
  2. 传入 operator delete[] 函数的参数不是数组对象的指针p,而是 p 的值减 4。
malloc/free&&new/delete的区别
  1. malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符
  2. new能够自动分配空间大小
  3. 对于用户自定义的对象而言,用maloc/free无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
    因此C++需要一个能对对象完成动态内存分配和初始化工作的运算符new,以及一个能对对象完成清理与释放内存工作的运算符delete

猜你喜欢

转载自blog.csdn.net/baidu_37964071/article/details/81334497
今日推荐