【C++】C&C++内存管理--之内存分布,operatenew/new,operate/delete的底层原理.

一.C/C++内存分布

我们先来看下面的一段代码和相关问题,引入话题:

cpp 复制代码
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);
}

通过一下问题来说明情况:

选择题:

选项: 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__

填空题:

  • sizeof(num1) = 40__
  • sizeof(char2) = 5
  • strlen(char2) = 4__
  • sizeof(pChar3) = 4或者8____
  • strlen(pChar3) = 4__
  • sizeof(ptr1) = 4或者8_
  • sizeof(*ptr1) = 4
  • sizeof(ptr2) = 4或8_
  • sizeof(ptr3) = ___ 4或8___

选择题解析:

1.int globalVar = 1;

位置:数据段(静态区)

原因: 全局变量,在整个程序运行期间存在,不在栈也不在堆。

  1. static int staticGlobalVar = 1;

位置:数据段(静态区)

原因: 全局静态变量,作用和全局变量类似,也存放在静态区。static 限制的是链接属性只可以在当前文件可见,不影响存储位置。

  1. static int staticVar = 1;(在函数内部的)

位置:数据段(静态区)

原因: 局部静态变量,虽然作用域在函数内,但生命周期是整个程序。它只被初始化一次,不会随函数调用结束而销毁,所以也放在静态区。

  1. int localVar = 1;

位置:栈

原因: 普通局部变量,在函数调用时创建,函数结束时销毁,存放在栈上。

  1. int num1[10] = { 1, 2, 3, 4 };

位置:num1在 栈

原因: num1 是局部数组,数组名代表栈上这块连续内存的地址。整个数组在栈上。

  1. char char2[] = "abcd";

位置:char2在 栈, *char2(即数组内容)也在栈

原因: char2 是局部字符数组,"abcd" 会被拷贝到栈上的数组中,而不是指向常量区。可以修改 char2[0] 验证。

  1. const char* pChar3 = "abcd";

位置:pChar3 在栈,所以 *pChar3(即指向的字符串常量)在代码段(常量区)

原因: pChar3 本身是一个指针变量,存放在栈上。它指向一个字符串常量 "abcd",这个常量存放在代码段的常量区,不能修改

  1. int* ptr1 = (int*)malloc(sizeof(int) * 4);

位置:ptr1 在 栈 *ptr1(即 malloc 分配的内存在堆)

原因: ptr1 是指针变量,在栈上。它指向的内存是通过 malloc 在堆上动态分配的。

  1. int* ptr2 = (int*)calloc(4, sizeof(int));

位置:ptr2 在栈, *ptr2 在堆,原因同8

  1. int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);

位置:ptr3 在哪里栈,*ptr3 在哪里堆,原因同8

注意: realloc 可能会移动内存地址,但始终在堆上,就是说,你用 realloc 想扩大或缩小一块内存,系统会先看看原来那块地方后面有没有足够的空位。如果有,就直接在原地方扩大,地址不变;如果没有,系统就会重新找一块足够大的新地方,把原来的数据拷过去,然后释放掉旧的那块,最后返回新地址。但是无论地址变不变,这块内存始终在堆上,不会跑到栈上去。

填空题解析:

sizeof(num1) = 40

答:10个int,每个4字节,一共40字节。

sizeof(char2) = 5

答:数组char2包含了"abcd"和结尾的\0,一共5个字符。

strlen(char2) = 4

答:strlen只计算到\0之前,所以是4。

sizeof(pChar3) = 4(32位)或8(64位)

答:pChar3是一个指针,大小由系统位数决定。

strlen(pChar3) = 4

答:pChar3指向字符串常量"abcd",长度为4,所以一个为一个字节大小为4。

sizeof(ptr1) = 4或8

答:ptr1是指针,大小由系统位数决定。

sizeof(*ptr1) = 4

答:*ptr1int类型,4字节。

sizeof(ptr2) = 4或8

答:ptr2是指针,由系统位数决定。

sizeof(ptr3) = 4或8

答:ptr3是指针,由系统位数决定。

方便各位理解:如果用图片对应一下,如下:

【说明】

  • 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  • 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
  • 创建共享共享内存,做进程间通信。(后面Linux课程我会给咱更新)
  • 堆用于程序运行时动态内存分配,堆是可以上增长的。
  • 数据段--存储全局数据和静态数据
  • 代码段--可执行的代码/只读常量

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

cpp 复制代码
void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}

解析如下:

realloc(p2, sizeof(int)*10) 的作用是:基于 p2 指向的内存,给p2指向的地方重新分配一块大小为 10 * sizeof(int) 的内存。

如果 realloc 在原地址后面有足够空间,就直接扩展,返回的地址还是 p2 原来的地址。

如果没有足够空间,realloc 会找一块新的足够大的内存,把 p2 里的数据拷过去,然后自动释放掉 p2 原来的内存,再返回新地址。但结果是无论哪种情况,原来的 p2 都不需要你再手动 free。你只需要 free realloc 返回的那个指针(这里是 p3)。给大家通俗点说就是:

realloc 自己会把旧房子(旧)内存的事情处理好(该搬就搬,该退就退),你不用操心。你只需要最后退掉(free)它给你的那间房(p3)就行。如果再加上 free(p2),那就是重复释放内存了,程序会出问题。

两个问题:

1.malloc/calloc/realloc的区别?

malloc:只给你一块指定大小的内存,但里面可能脏乱差,有之前留下的垃圾数据。你需要自己清理。

calloc :给你一块内存,还会帮你全部清零。适合用来存放数组,因为每个元素默认就是0。

realloc:调整你已有内存的大小。可以改大也可以改小。如果原来那块后面有位置就直接扩;如果后面没位置了,就找一块新地方,把你的数据搬过去,再把原来的地方自动释放掉。但realooc资金会搬家,它可能会搬家,但不用你自己搬。

简单理解:malloc 只管给,calloc 给完还打扫,realloc 帮你换房搬家。

2. malloc的实现原理?

malloc 的实现原理是:简单来说就是,不是每次你问它要内存,它都去找操作系统要。那样太慢了。它会提前多批发一些存着,你要的时候先从库存里拿。

三.C++内存管理方式

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

3-1new/delete操作内置类型

cpp 复制代码
void Test()
{
 // 动态申请一个int类型的空间
 int* ptr4 = new int;
 
 // 动态申请一个int类型的空间并初始化为10
 int* ptr5 = new int(10);
 
 // 动态申请10个int类型的空间
 int* ptr6 = new int[10];
 delete ptr4;
 delete ptr5;
 delete[] ptr6;
}

解析:在Test函数中,首先通过new int在堆上申请了一个int大小的空间,这块内存没有初始化,里面是随机值,ptr4指针指向它;

接着通过new int(10)在堆上申请了一个int大小的空间,并且初始化为10,ptr5指针指向它;

然后通过new int3在堆上申请了3个int大小的连续空间,也就是一个数组,但同样没有初始化,值是随机的,ptr6指针指向数组的第一个元素。

释放时,ptr4和ptr5指向的是单个int,所以用delete释放。ptr6指向的是数组,必须用delete\[\]释放。

这里有配对规则是:new和delete配对,new int(值)和delete配对,new\[\]和delete\[\]配对。如果你不小心混用使用/或者配对错误,会导致未定义行为,可能内存泄漏或程序崩溃。

另外注意代码中注释写的是申请10个int,但实际写的是new int3,只申请了3个,这里存在笔误

注意1: 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new\[\]和delete\[\]

注意2:二者匹配起来使用。

3-2 new和delete操作自定义类型

cpp 复制代码
class A
{
public:
    A(int a = 0)
    : _a(a)
{
    cout << "A():" << this << endl;
}
~A()
{
    cout << "~A():" << this << endl;
}
private:
    int _a;
};
int main()
{
       // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
   // 还会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A));
    A* p2 = new A(1);
    free(p1);
    delete p2;


// 内置类型是几乎是一样的
    int* p3 = (int*)malloc(sizeof(int)); // C
    int* p4 = new int;
    free(p3);
    delete p4;


    A* p5 = (A*)malloc(sizeof(A)*10);
    A* p6 = new A[10];
    free(p5);
    delete[] p6;
return 0;
}

C 语言:用 malloc / calloc / realloc 申请内存,用 free 释放。

C++ 语言:既可以用 malloc / free,也可以用 new / delete。但推荐用 new / delete

区别:

malloc / free 只分配和释放内存,不调用构造函数和析构函数。

new / delete 除了分配释放内存,还会调用对象的构造函数和析构函数。

所以对于 C++ 中的自定义类型(比如类对象) ,必须用 new / delete,否则构造函数和析构函数不会执行,容易出问题。


四. operator new与operator delete函数(重点)

这里很重要,我会很仔细的讲解:

4-1 operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete

系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间(这句话很重,要记在脑子里)

operator deletefree 宏的实现

cpp 复制代码
/*operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。*/
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);
}
/*operator delete: 该函数最终是通过free来释放空间的*/
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)

我大致解析说明一下底层的代码:你传一个指针(pUserData)进来,如果是空指针(NULL)就直接返回。不是空指针就先给堆加个锁(_mlock(_HEAP_LOCK)),防止多个线程同时操作搞乱套;

然后从这个指针往前找,找到这块内存的"头信息"(_CrtMemBlockHeader,用 pHdr 拿到 pHead),里面记着这块内存多大、什么类型(nBlockUse)。找到之后检查一下这个头信息合不合法(_ASSERTE(_BLOCK_TYPE_IS_VALID(...))),防止你乱传指针。

检查通过后就调用 _free_dbg(pUserData, pHead->nBlockUse) 真正释放内存。最后解锁(_munlock(_HEAP_LOCK)),就结束了到这里;

而咱们平时写的**free(p),在 Debug 模式下其实被偷偷换成了 _free_dbg(p, _NORMAL_BLOCK) ,只是多告诉系统一声"这是个普通内存块"。C++ 的 delete 底层干的就是上面这套活,本质上还是调用了 freeDebug 模式多加了几道保险(加锁、校验、记信息),帮你查内存问题。Release 模式**就,直接释放,追求速度。

这说明:C++ 的 new / delete 底层本质上还是 C 的 malloc / free
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果

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

4-2重载operator new与operator delete

这里我给大家大致说一下过程直接大家了解一下多学一点是一点:

默认情况一般是:你写 new int,编译器会调用全局的 operator new,这个函数底层再去调用 malloc。你写 delete ptr,编译器调用全局的 operator delete,底层调用 free

这些全局版本是 C++ 标准库自带的,你不用管它怎么实现,直接用就行。

**重载的意思:**你自己写一个同名的 operator newoperator delete,替换掉默认的。这样每次 newdelete 就会先执行你写的代码,然后你再去调用 mallocfree 干正事。

**得到的结论就是:**正常情况下你不需要动它们。只有在申请或释放内存时想干点额外的事,才需要重载。最常见的就是打印日志,帮你排查内存泄漏。


五.new和delete的实现原理

5-1 内置类型

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

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

代码如下:

cpp 复制代码
#include <iostream>
#include <cstdlib>
using namespace std;

int main()
{
    // malloc / free
    int* p1 = (int*)malloc(sizeof(int));
    if (p1 == NULL) {
        cout << "malloc 失败" << endl;
    }
    *p1 = 10;
    free(p1);

    // new / delete
    int* p2 = new int(10);
    delete p2;

    // new[] / delete[]
    int* p3 = new int[10];
    delete[] p3;

    return 0;
}

5-2 自定义类型

  • new的原理
  1. 调用operator new函数申请空间

  2. 在申请的空间上执行构造函数,完成对象的构造

  • delete的原理
  1. 在空间上执行析构函数,完成对象中资源的清理工作

  2. 调用operator delete函数释放对象的空间

  • new TN的原理
  1. 调用operator new\[\]函数,在operator new\[\]中实际调用operator new函数完成N个对象空间的申请

  2. 在申请的空间上执行N次构造函数

  • delete\[\]的原理
  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

  2. 调用operator delete\[\]释放空间,实际在operator delete\[\]中调用operator delete来释放空间


六.定位new表达式(placement-new) (了解)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

  • 使用格式:

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

place_address必须是一个指针,initializer-list是类型的初始化列表

  • 使用场景:

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

cpp 复制代码
class A
{
public:
    A(int a = 0)
    : _a(a)
{
    cout << "A():" << this << endl;
}
~A()
{
    cout << "~A():" << this << endl;
}
private:
    int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    A* p1 = (A*)malloc(sizeof(A));
    new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
    p1->~A();
    free(p1);
    A* p2 = (A*)operator new(sizeof(A));
    new(p2)A(10);
    p2->~A();
    operator delete(p2);
 return 0;
}

理解定位 new 就是:内存你自己搞定,它只负责在上面调用构造函数

正常情况下,你用 new A(),C++ 会帮你干两件事:找一块内存(调用 malloc),然后在这块内存上构造对象。定位 new 把这两件事拆开了------内存你自己申请(用 malloc、operator new、或者从内存池里拿),然后你告诉定位 new 这块内存的地址,它只负责在上面调用构造函数。

我的代码里:A* p1 = (A*)malloc(sizeof(A)); 只是用 malloc 申请了一块原始内存,里面是还垃圾数据,还不能算是一个 A 对象,因为构造函数没执行过。

new(p1)A; 就是定位 new,在 p1 指向的内存上调用 A 的构造函数,把它变成真正的对象。

用完要手动调析构 p1->~A();,然后手动释放内存 free(p1);p2同理,只不过换成了 operator newoperator delete

定位 new 主要配合内存池用。内存池提前申请一大块内存,每次你需要对象时从池子里拿一块现成的内存,但这块内存不是用 new 申请的,构造函数不会自动调用,所以需要你手动用定位 new 来调构造函数。

简单点来说就是:定位 new 就是在你指定的内存地址上"盖房子",内存你自己准备,它只管把对象造出来。


七.malloc/free和new/delete的区别

7-1.相同点:

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

7-2.不同点:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,\[\]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放

八.内存泄漏:

8-1定义:

程序向系统申请了一块内存,用完之后忘了归还,而且把指向这块内存的地址也弄丢了,导致这块内存既没有被使用,也没法被释放,就一直占着地方浪费掉。它不是内存变少了或者物理上消失了,而是程序失去了对它的控制权,系统也没法把它重新分配给别的代码使用。

8-2危害:

内存泄漏的危害:

长期运行的程序(如操作系统、服务器、后台服务)如果存在内存泄漏,可用内存会越来越少,程序响应越来越慢,最终卡死或崩溃。

8-3分类:

内存泄漏主要分两类:

1. 堆内存泄漏

通过 malloc、calloc、realloc、new 等从堆上申请的内存,用完后没有用 free 或 delete 释放。这是最常见的内存泄漏。

2. 系统资源泄漏

程序使用了系统分配的资源,比如套接字(socket)、文件描述符(file descriptor)、管道(pipe)、数据库连接等,用完后没有调用对应的释放函数(如 close、fclose 等)。这类资源泄漏可能导致系统效能下降,甚至系统不稳定。

8-4如何检验内存分类:

1.VS 下自带的检测

在 main 函数末尾加上 _CrtDumpMemoryLeaks(),程序退出时会在输出窗口打印泄漏信息。只能看到泄漏了多少字节,看不到具体位置。

2. 使用内存泄漏检测工具

Linux:Valgrind 是最常用的,能定位到具体哪一行代码泄漏

Windows:Visual Leak Detector(VLD)、Dr.Memory、Deleaker

跨平台:AddressSanitizer(ASan,编译器自带,需要加编译选项)

3. 自己实现简单的检测(小项目可以这样用因为简单)

重载 operator new/delete,记录每次申请的内存地址和文件行号,程序结束时打印未释放的地址。

我拿第一个举个例子代码如下:

cpp 复制代码
#include <iostream>
#include <crtdbg.h>

int main()
{
    int* p1 = new int[10];      // 申请了没释放
    int* p2 = (int*)malloc(40); // 申请了没释放

    _CrtDumpMemoryLeaks();  // 检测内存泄漏
    return 0;
}
cpp 复制代码
运行结果:
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
{78} normal block at 0x00EC5F10, 40 bytes long.

最后分配内存使用完毕后一定要记得释放内存


8-5预防内存泄露:

如何避免内存泄漏

1. 写好代码的规矩

从一开始就养成好习惯:在哪里申请的内存,就在哪里释放。写 new 的时候顺手把 delete 写上,写 malloc 的时候顺手把 free 写上。别等后面补,很容易忘。不过这只是理想状态。代码里一旦有异常或者提前 return,释放的那行代码可能根本走不到,还是会漏。

2. 用 RAII 或智能指针

让对象自己管理自己的内存。对象活着内存就在,对象死了内存自动还回去。智能指针(比如 unique_ptr、shared_ptr)就是干这个的。用上它们,就算函数中间抛异常,内存也能正常释放。这是目前最靠谱的办法。

3. 用公司内部的内存库

很多公司自己封装了一套内存管理库,Debug 模式下能记录每次申请和释放,程序结束的时候自动打印没释放的地址。这种库用起来省心,但不是每个公司都有。

4. 出问题了用工具查

泄漏已经发生了,就用工具定位。Linux 下用 Valgrind,Windows 下用 Visual Leak Detector 或者 VS 自带的检测功能。不过这些工具要么不够准(误报漏报),要么好用的要花钱。

简单说就是两种思路:

  • 事前防:用智能指针、RAII,从源头上减少泄漏的可能。

  • 事后查:用工具去定位泄漏的代码位置。

更多内容,下节继续,下节会讲解模板,尽请期待

相关推荐
大白话_NOI2 小时前
【洛谷 P1480】A/B Problem(高精度除法 Ⅰ)详细题解
c++
拂拉氏2 小时前
【项目分享-知识讲解】 C++标准库 list类的模拟实现
开发语言·c++·list·封装·stl标准库
十月的皮皮2 小时前
C语言学习笔记20260603-打印整数(32位)二进制的奇数位和偶数位(2种方法)
c语言·笔记·学习
刃神太酷啦2 小时前
MySQL 库表操作 +数据类型+ 基础概念全梳理----《Hello MySQL!》(2)
java·c语言·数据库·c++·vscode·mysql·adb
L_090710 小时前
【C++】异常
开发语言·c++
liulilittle10 小时前
关于拥塞控制的几点思考
网络·c++·tcp/ip·计算机网络·信息与通信·tcp·通信
QT-Neal12 小时前
C++ 编码规范
c++
啦啦啦啦啦zzzz13 小时前
数据结构:红黑树理论
数据结构·c++·红黑树
Yolo_TvT13 小时前
C++:默认构造函数
c++