1. C/C++内存分布

C/C++中内存区域划分:   

静态变量都是放在数据段,也可以叫静态区,静态区还可以划分为“初始化区”和“未初始化区”;“代码段”也叫“常量区”

前三个变量的生命周期,在整个程序运行之间都在,区别就在于前两个在程序运行到main之前就初始化,而第三个要走到那一行才初始化

局部变量存在栈

char2是一个栈上的数组,数组名是首元素的地址,所以*char2相当于解引用数组的首个元素,数组在栈上,所以首元素也在栈上

指针变量是存在栈上的,但是*pChar3指向的是字符串,是一个常量,所以应该在常量区,也就是代码段

而ptr1依旧是局部变量的指针,还是在栈上,而它指向的内容才是malloc划分出来的,所以是堆

局部的静态变量。生命周期是全局,但是只在其声明那一行才开始构造和初始化

局部变量先析构,静态对象和全局对象后析构,而后定义的先析构

2. C语言中动态内存管理方式:malloc/calloc/realloc/free

malloc/calloc/realloc 的区别:

malloc

void* malloc (size_t size) //size 为开辟字节数

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针

  • 如果开辟成功,则返回一个指向开辟好空间的指针。

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查

  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定

  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

calloc

void* calloc (size_t num, size_t size);

函数的功能是为 num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0

  • 与函数 malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0

realloc

  • realloc函数的出现让动态内存管理更加灵活。

  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,

我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。函数原型如下:

void* realloc (void* ptr, size_t size);

  • ptr 是要调整的内存地址

  • size 调整之后新大小

同时,realloc 还可以实现 malloc 的作用:

int *p = realloc(NULL, 40);//等价于 malloc(40)

realloc 的使用注意事项:

1.当地址指向的内存空间之后有足够的内存空间可以追加时,在原来内存空间基础上追加空间,并返回旧地址

2.当后续内存空间不够追加时,realloc 会重新找一个新的内存区域一次性开辟一个新的足够大小的空间,并将原来空间上的内容拷贝过去,返回新的空间的地址,然后将旧空间释放掉

3.当需要 realloc 函数开辟的空间过大时,realloc 会返回空指针,而此时如果拿原指针接收会导致原指针指向区域丢失,所以一般会用一个新的指针变量接收返回值,检测是否开辟失败,确认开辟成功过后再将返回值赋给原指针。(如下代码)

4.当需要用 realloc 缩小动态内存块时,原内存块中被缩小部分的数据将会丢失。因此,在缩小内存前,确保不再需要那些数据,或者已经将其复制到其他地方。

int* ptr = realloc(p, 40);
if (ptr != NULL)
{
        p = ptr;
}

3. C++内存管理方式

Q:既然C语言的内存管理方式C++也可以用,那么C++为什么还要再使用一套新的内存管理方式呢“?

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理

new/delete操作内置类型

void Test()
{
    // 动态申请一个 int 类型的空间(未初始化)
    int* ptr4 = new int;
    
    // 动态申请一个 int 类型的空间并初始化为 10
    int* ptr5 = new int(10);
    
    // 动态申请 10 个 int 类型的空间(未初始化)
    int* ptr6 = new int[10];
    
    // 错误写法:不能这样初始化数组
    // int* ptr7 = new int[10](10); // 编译错误
    
    // 释放内存
    delete ptr4;
    delete ptr5;
    delete[] ptr6;
}

使用须知:

对于内置类型,new/deletemalloc/free 效果是一样的,对于自定义类型,效果就不一样。

  • 单个元素使用 newdelete,数组使用 new[]delete[]

3.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()
{
    // malloc/free 不会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A)); // 只申请空间
    free(p1); // 只释放空间
    
    // new/delete 会自动调用构造函数和析构函数
    A* p2 = new A(1); // 申请空间 + 调用构造函数
    delete p2; // 调用析构函数 + 释放空间
    
    // 数组操作
    A* p5 = (A*)malloc(sizeof(A) * 10); // 只申请空间
    free(p5); // 只释放空间
    
    A* p6 = new A[10]; // 申请空间 + 调用10次构造函数
    delete[] p6; // 调用10次析构函数 + 释放空间
    
    return 0;
}

区别:

  • new 会调用构造函数,delete 会调用析构函数

  • mallocfree 只负责内存分配和释放

  • 对于自定义类型,必须使用 new/delete

    3.3 new/delete

    现在我们用创建链表节点来看看C语言和C++内存管理方式具体有什么不同

  • #include <iostream>
    using namespace std;
    
    struct ListNode
    {
        int _val;
        struct ListNode* _next;
        struct ListNode* _prev;
    };
    
    typedef struct ListNode ListNode_t;
    
    ListNode_t* CreatListNode(int val)
    {
        ListNode_t* node_c = (ListNode_t*)malloc(sizeof(ListNode_t));
        node_c->_val = val;
        node_c->_next = NULL;
        node_c->_prev = NULL;
        return node_c;
    }
    
    struct ListNode_CPP
    {
        int _val;
        struct ListNode_CPP* _next; //兼容C struct 的用法
        ListNode_CPP* _prev; //CPP中,struct已经可以认为是类,和class一样,区别是默认访问限定符
    
        ListNode_CPP(int val = 0)
            : _val(val)
            , _next(nullptr)
            , _prev(nullptr)
        {
        }
    };
    int main()
    {
        // C 语言方式:需要单独初始化
        ListNode_t* node1 = CreatListNode(1);
    
        // C++ 方式:定义即初始化
        ListNode_CPP* node2= new ListNode_CPP; // 使用默认参数
        ListNode_CPP* node3 = new ListNode_CPP(5); // 使用指定参数
    
        // 释放内存
        delete node2;
        delete node3;
    
        return 0;
    }

    使用不同:

    在C语言中,每创建一个对象,都要再手动调用一个自定义的函数来进行初始化,但是在c++中,有了构造函数的概念,可以将构造函数封装进结构体中,在定义的时候就可以直接初始化,比C语言方便快捷不少。

    总结:

    C++的使用诠释了面向对象的思想,定义即是初始化,使代码的封装更加完整简洁,使用更加方便,不需要单独的函数来初始化,可以自动调用构造/析构函数,更加安全

    4. operator new与operator delete函数

    基本概念

  • new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete是系统提供的全局函数。

  • new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

  • operator new 的用法和 malloc 一模一样,operator delete 和 free 的用法一模一样。

    class A
    {
    public:
            A(){cout << "A()" << endl;}
            ~A(){cout << "~A()" << endl;}
    private:
            // ...
    };
    
    int main()
    {
            // 下面两个都不会调用构造函数
            A* p1 = (A*)malloc(sizeof(A));
            A* p3 = (A*)operator new(sizeof(A));
            return 0;
    }

    由于C语言和C++的性质不一样,一个是面向过程,一个是面向对象,所以在申请内存失败时,其处理的方式也不一样

      void* p4 = malloc(2 * 1024 * 1024 * 1024);
      
      void* p5 = operator new(2* 1024 * 1024 * 1024);

    用malloc申请内存失败是返回的是0 , 而用operator申请失败时,会弹出异常,程序报错。

    operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的

    5. new和delete的实现原理

    operator new = malloc + 失败抛异常实现

    new = operator new + 构造函数

    new 对比 malloc :1、调用构造函数初始化。2、失败了抛异常

    delete 对比 free :1.多一个调用析构函数清理。(一般free不会失败,因为free是释放空间,除非你释放空间的位置不对,一般释放空间失败会导致进程直接终止)

    5.1 内置类型

    如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:

    new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

    5.2 自定义类型

    new 的原理

  • 调用 operator new 函数申请空间

  • 在申请的空间上执行构造函数

        delete 的原理

  • 在空间上执行析构函数

  • 调用 operator delete 函数释放空间

        数组操作原理        

  • new T[N]:调用 operator new[],在其中调用 operator new 完成 N 个对象空间的申请,然后申请的空间上执行 N 次构造函数

  • delete[]:执行 N 次析构函数,然后调用 operator delete[] 释放空间

6. 内存泄漏

6.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务 等等,出现内存泄漏会导致响应越来越慢,最终卡死。

6.2 内存泄漏分类(了解)

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

6.3 如何检测内存泄漏(了解)

在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。

在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客

在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客

其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园

6.4 如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。

ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

  1. 采用RAII思想或者智能指针来管理资源。

  2. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

  3. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下:

内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

        

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐