【逐步剖C++】-第三章-C++内存管理

一、C/C++内存分布

C/C++的内存分布主要分为如下这么四个区域:

1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2、 堆区(heap):一般由程序员申请分配与释放, 若程序员不释放,程序结束时可能由操作系统回收 。

3、数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。

4、代码段:存放函数体(类成员函数和全局函数)的二进制代码

这里可通过代码和相关问题来认识一下各类型数据在内存中的分布,请看:

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

(1)判断变量的存储位置
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?C
staticGlobalVar在哪里? C
staticVar在哪里?C
localVar在哪里? A
num1 在哪里?A
char2在哪里?A
*char2在哪里?A
pChar3在哪里?A
*pChar3在哪里?D
ptr1在哪里?A
*ptr1在哪里?B

这里的易错点是const char*类型的指针变量pChar,看到const可能就想着其会在常量区,其实不然;其本质还是一个局部变量,所以其存储位置在栈区上;实际上,加不加const都不影响变量本身的存储位置,加const只是为了“适配”const指向的内容,如const int a = 1;,a仍存储在栈区。

二、C++内存管理

1、C内存管理

关于C内存管理大家可以阅读笔者的这一篇文章:【逐步剖C】-第十一章-动态内存管理,这里不再具体说明啦。

2、C++内存管理

C++内存管理是在C内存管理基础上的更新,以便适应C++面向对象编程的特点。C++通过newdelete两个操作符进行动态内存管理。
newdelete对内置类型和自定义类型进行操作时的行为不同:
(1)对内置类型的操作

  • 动态申请一个int类型的空间和释放
int* ptr = new int;
delete ptr;
  • 动态申请一个int类型的空间并初始化为10和释放
int* ptr = new int(10);
delete ptr;
  • 动态申请10个int类型的空间和释放
int* ptr = new int[10];
delete[] ptr;

使用注意事项:申请和释放单个元素的空间时,使用newdelete操作符;申请和释放连续的空间,使用new[]delete[],要匹配起来成对使用;

(2)对自定义类型的操作
与内置类型不同的是,new在为自定义类型开空间时还会调用其构造函数,相应地,delete调用其析构函数

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};

int main()
{
    //动态申请和释放一个A类对象大小的空间并将其成员_a初始化为1
    A* ptra = new A(1);
    delete ptra;

    return 0;
}

运行结果:
【逐步剖C++】-第三章-C++内存管理_第1张图片

三、new和delete本质

1、operator new 和 operator delete函数

operator newoperator delete是系统提供的全局函数new操作符在底层调用operator new全局函数来申请空间,delete操作符在底层通过operator delete全局函数来释放空间。

(1)operator new
该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行用户设置的空间不足应对措施,若成功执行则继续申请;否则抛异常(PS:抛异常是面向对象语言处理错误的方式

(2)operator delete
该函数最终会调用一个名为_free_dbg的函数进行空间释放,而free函数本身其实也是这个函数的宏定义函数

相关源码如下,供参考:

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
    void* p;
    while ((p = malloc(size)) == 0)

        if (_callnewh(size) == 0)
        {
            // report no memory
            // 如果申请内存失败了,这里会抛出 bad_alloc 类型异常
            static const std::bad_alloc nomem;
            _RAISE(nomem);
        }
    return (p);
}

void operator delete(void* pUserData)
{
    _CrtMemBlockHeader* pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK); /* block other threads */
    __TRY
        /* get a pointer to memory block header */
        pHead = pHdr(pUserData);
    /* verify block type */
    _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
    _free_dbg(pUserData, pHead->nBlockUse);		//释放空间
    __FINALLY
        _munlock(_HEAP_LOCK); /* release other threads */
    __END_TRY_FINALLY
        return;
}

//free的实现,本质为宏函数

#define free(p) _free_dbg(p, _NORMAL_BLOCK)

2、new和delete的实现原理

(1)对内置类型
newmalloc类似,而deletefree类似,不同的地方是:
操作符new/delete申请和释放的是单个元素的空间,若申请的是连续空间需要用操作符new[]delete[];但这两组操作符的本质都是调用了operator newoperator delete函数

(2)对自定义类型

  • new的原理
    调用operator new函数先进行空间的申请
    接着在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
    先在空间上执行析构函数,完成对象中资源的清理工作
    接着调用operator delete函数释放对象的空间(归还使用权)
  • new[]的原理
    调用operator new[]函数,而实际在operator new[]中实际又调用operator new函数先完成N个对象空间的申请
    接着在申请的空间上执行N次构造函数
  • delete[]的原理
    先在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    接着调用operator delete[]释放空间,而实际在operator delete[]中又调用operator delete来释放空间

可通过反汇编进行简单的验证:
用于验证的代码:

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};


int main()
{
    A* a = new A(1);
    delete a;

    return 0;
}

验证结果如下图:

  • 对于new
    【逐步剖C++】-第三章-C++内存管理_第2张图片
  • 对于delete
    【逐步剖C++】-第三章-C++内存管理_第3张图片
    这里编译器对其做了进一步的封装,F11跳转进去后:
    【逐步剖C++】-第三章-C++内存管理_第4张图片

对new和delete行为补充理解:
若有这么一个类,在初始化时需为其成员进行内存资源的分配,如:

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        _pa = new int[5];        //为pa成员申请5个整型大小的空间
        cout << "A():" << this << endl;
    }
    ~A()
    {
        delete pa;
        cout << "~A():" << this << endl;
    }
private:
    int _a;
    int* _pa;
};

int main()
{
    A* a = new A(1);
    delete a;

    return 0;
}

那么要先为整个类的对象开空间,才能通过类的构造函数为该成员开空间;
同理,在对象销毁时,需先调用析构函数把为成员开的空间资源进行清理释放后,才释放整个对象的空间。若先释放了空间,那么申请的资源就会“丢了”,即会造成内存泄漏

这里也能侧面说明malloc要配合free使用,new要配合delete使用的原因;从上面的代码来看,如果用malloc给A类对象申请空间,而用delete来进行空间释放就会出现问题:malloc函数仅进行空间分配,不会调用构造函数,即A类对象的成员_pa所指向的空间是未定义(随机)的,此时用delete进行空间释放时,会先调用析构函数来回收和释放_pa成员所指向的空间,如此一来就造成了非法的内存访问

3、定位new

由于定位new在学习阶段用得不多,所以这里仅进行一定简单的说明
(1)定义
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:

new (place_address) type 
或 
new (place_address) type(initializer-list)

其中place_address必须是一个指针initializer-list是类型的初始化列表(PS:可以理解为类似初始化数组用的{}
使用示例:

class A
{
public:
    A(int a1 = 1, int a2 = 2)
        : _a1(a1)
        ,_a2(a2)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a1;
    int _a2;
};

int main()
{
    A* p0 = new A;      //new:开空间+构造

    A* p1 = (A*)malloc(sizeof(A)); 
    //p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    new(p1)A{1,2};
    //通过定位new调用其构造函数,{1,2}即为初始化列表,当然也可写为new(p1)A(1, 2);
    p1->~A();           //显示调用析构,然后再free,即和delete操作符执行的顺序保持一致
    free(p1);           //不能直接delete,因为分配空间用的是malloc,要配套使用

    A* p2 = (A*)operator new(sizeof(A));        
    //operator new函数本质调用malloc开空间,并不会调用构造
    new(p2)A(10);
    p2->~A();
    operator delete(p2);

    return 0;
}

(2)使用场景
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

关于池化技术,大家可以阅读一下本站的这篇文章:什么是池化技术

四、C/C++内存管理常见问题

1、malloc、free和new、delete的区别

malloc/free和new/delete的共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:

  • malloc和free是函数newdelete操作符
  • malloc仅能申请空间无法满足初始化的功能;而new有手动初始化的功能
  • malloc申请空间时,需要计算空间大小并传递;new只需在其后跟上空间的类型即可,
    如果是多个对象,在new[][]中指定对象个数即可
  • malloc的返回值为void*, 在使用时必须强转new不需要,因为new后直接跟了空间的类型
  • malloc申请空间失败时,返回的是NULL,因此使用时必须判空new不需要,但是new
    要捕获异常
  • 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数;而new
    申请空间后会调用构造函数完成对象的初始化delete先调用析构函数完成空间中资源的清理然后释放空间

2、关于内存泄漏

(1)内存泄漏的定义
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

(2)内存泄漏的分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定

(3)内存泄漏的检测与预防
关于内存泄漏的检测与预防,这里放上几篇优质文章供大家阅读:
Linux下几款C++程序中的内存泄露检查工具
内存泄露检测工具比较

本章完。

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!

你可能感兴趣的:(逐步剖C++,c++,开发语言,学习)