C++ 内存管理 —— new/delete 到底比 malloc/free 多做了什么?

C++ 中自主内存管理 new/delete 与 malloc/free 完全详解 项目说明1new和delete要成对使用2new[]和delete[]要成对使用不是 delete!!!3不要混用malloc和delete,或new和free严重错误!4避免内存泄漏:申请的内存必须释放,否则内存泄漏5申请后立即检查返回值malloc返回 NULL,new可以捕捉异常6构造/析构问题malloc不负责对象构造,new是7自定义 new/delete:可以通过重载控制内存分配策略(如内存池优化)8异常安全:尽量使用智能指针(如)避免手动管理内存错误。 阅读详情

C++ 内存管理 —— new/delete 到底比 malloc/free 多做了什么?

编译参数:g++ -g -O0 -Wall -Wextra -std=c++17
说明:文中每一段"运行结果"都是本机实跑得到的,不是为了排版编的。


1. 一句话总结

new/delete = operator new/operator delete(只要空间、只还空间,底层就是 malloc/free)+ 构造函数/析构函数(管对象的生与死)。

malloc/free 只做了前半段。所以:

  • 对内建类型:new/delete 和 malloc/free 几乎是同一件事;
  • 对自定义类型:new/delete 多了一整条对象生命周期,这就是它存在的唯一理由。

2. 知识地图

C/C++ 内存管理
  ├── 内存分布(底层模型,回答"变量到底住在哪")
  │     ├── 栈:非静态局部变量 / 函数参数 / 返回值,向下增长
  │     ├── 内存映射段:共享库、大块动态申请走的 mmap
  │     ├── 堆:动态申请的空间,向上增长
  │     ├── 数据段(静态区):全局变量 + 静态变量
  │     └── 代码段(常量区):可执行代码 + 只读常量
  │
  ├── C 语言的方式:malloc / calloc / realloc / free
  │     └── 只分配空间,不初始化、不构造;失败返回 NULL
  │
  └── C++ 的方式:new / delete
        ├── 内置类型:new int、new int(10)、new int[3](+ delete / delete[])
        ├── 自定义类型:先调构造、先调析构
        ├── 底层函数:operator new / operator delete(重点)
        │     └── operator new 内部就是 while + malloc,失败抛 bad_alloc
        ├── 实现原理:new T[N] / delete[] 的四步流程
        └── 定位 new(placement new):在已有内存上"只调构造"

关系说明:这几块知识是层层往下钻的同一条线 —— 表面是"new 和 malloc 有什么区别"(用法层),往下是"new 失败为什么抛异常而 malloc 返回 NULL"(operator new 层),再往下是"new 申请空间和调构造是两个可以拆开的动作"(定位 new 层)。记住这条线,它们就不是零散知识点。


3. 核心概念深度解析

3.1 C/C++ 内存分布

这一节从一段最经典的代码开始:

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);
}

进程地址空间(高地址在上):

高地址
┌──────────────────────────────────┐
│  栈 stack(向下增长)              │  localVar / num1 / char2 / pChar3 / ptr1
├──────────────────────────────────┤
│  内存映射段(mmap)                │  共享库;大块 malloc 也走这里
├──────────────────────────────────┤
│  堆 heap(向上增长)               │  *ptr1 —— malloc/new 出来的空间
├──────────────────────────────────┤
│  数据段(静态区)                  │  globalVar / staticGlobalVar / staticVar
├──────────────────────────────────┤
│  代码段(可执行代码 + 只读常量)    │  函数指令、"abcd" 这类字面量
└──────────────────────────────────┘
低地址

先自己答一遍:globalVar / *ptr1 这些变量分别在哪?(A 栈 B 堆 C 数据段 D 代码段)

变量答案实测地址(macOS arm64,-O0)
globalVarC 数据段0x100bac000
staticGlobalVarC 数据段0x100bac008
staticVarC 数据段0x100bac004
localVarA 栈0x16f25afdc
num1A 栈0x16f25afe0
char2A 栈0x16f25afd4
*char2A 栈0x16f25afd4(和 char2 同一个地址)
pChar3A 栈0x16f25afc8
*pChar3D 代码段(常量区)0x100ba4814
ptr1A 栈0x16f25afc0
*ptr1B 堆0x100bd91d0

这张表里真正值钱的是这两组对比,把内存分布一次讲透:

① 三个"static/global"变量地址挤在一起(...c000、...c004、...c008) —— 它们确实住在数据段,staticVar 写在函数里也照样是静态存储期,函数退出不销毁。

② char2 和 *char2 是同一个地址,pChar3 和 *pChar3 完全不是一个地址。

char char2[] = "abcd";        // 栈上开 5 字节数组,把 'a','b','c','d','\0' 拷进去
     char2  ──→ 栈 0x16f25afd4   (数组名)
     *char2 ──→ 栈 0x16f25afd4   (就是数组第一个元素,也在栈上)

const char* pChar3 = "abcd";  // 栈上开 8 字节指针,指向代码段里的只读字面量
     pChar3  ──→ 栈 0x16f25afc8   (指针变量本身)
     *pChar3 ──→ 常量区 0x100ba4814(字面量"abcd",和函数代码住在同一段)

*pChar3 实测地址 0x100ba4814 与函数 Test 的地址 0x100ba4530 同属 0x100ba... 这一段 —— 这就是"代码段(常量区)"字面上的含义:只读字面量和指令放在一起,所以改它就是 UB(段错误)。

这五条按内存分配的顺序捋一遍就够了:

  1. 栈又叫堆栈,非静态局部变量/函数参数/返回值都在上面,向下增长;
  2. 内存映射段是高效的 I/O 映射方式,用来装载共享库,也能给进程间通信创建共享内存(Linux 内容,先了解);
  3. 堆用于动态内存分配,向上增长;
  4. 数据段存全局数据和静态数据;
  5. 代码段存可执行代码和只读常量。

3.2 C 语言的方式:malloc / calloc / realloc / free

C 这边一共四个函数,先看它们最典型的用法:

int* p2 = (int*)calloc(4, sizeof(int));            // 4 个 int,全部清零
int* p3 = (int*)realloc(p2, sizeof(int) * 10);     // 把这块调整成 10 个 int
// 这里需要 free(p2) 吗?  ← 先自己回答一遍
free(p3);

第一个高频面试题:malloc / calloc / realloc 的区别?

函数作用是否初始化说明
malloc(size)申请 size 字节否(内容是脏的)返回 void*,失败返回 NULL
calloc(n, size)申请 n * size 字节是,全部置 0按"n 个元素"给,适合数组
realloc(p, size)把 p 指向的块调整成 size 字节新增部分不保证可能原地扩、可能搬家、可能失败返回 NULL

“这里需要 free(p2) 吗?”——不需要,而且不能 free。 原因:

realloc 成功的两种情况:
  ① 原地扩展:p2 的地址没变,但"所有权"已经交给 p3 了
  ② 搬家:realloc 内部已经 free 了旧块,返回新地址

所以 realloc 成功后,p2 就是一个"已失效的指针":
  free(p2) → double free(同一块内存释放两次)
  free(p3) → 正确(就是上面代码干的事)

但 realloc 失败时返回 NULL,原块还活着:
  p3 = NULL 时,p2 仍然有效,这时候 p2 才需要你 free

正确的写法(把"失败"这条路径也处理掉):

int* tmp = (int*)realloc(p2, sizeof(int) * 10);
if (tmp == NULL)
{
    // p2 依然有效,需要自己决定是继续用还是 free 掉
    // ...
}
else
{
    p2 = tmp;   // 只有成功才覆盖原指针
}
free(p2);

第二个高频面试题:malloc 的实现原理?glibc 里的 malloc 是怎么做的?

要点(面试够用):glibc 的 malloc 用 ptmalloc(源自 Doug Lea 的 dlmalloc)。

  • 小块内存:通过 brk/sbrk 抬高 heap 边界,在连续堆里切 chunk;
  • 大块内存:直接 mmap 一块独立的区域(默认阈值 128KB,可用 mallopt 调);
  • 空闲的 chunk 按大小分箱管理(tcache / fastbin / smallbin / unsorted bin / largebin),所以频繁 malloc/free 小对象不一定每次都进内核;
  • 每个 chunk 都有自己的头部(大小、标志位),这就是"malloc 怎么知道 free 该还多少"的答案。

macOS 上不是这一套(Apple 用自己的 zone allocator),所以我在 Mac 上实测出的地址分布规律不能直接套到 Linux 的结论上 —— 后端主战场是 Linux,glibc 的那套才是要背的。

3.3 C++ 的方式:new / delete

C 那套在 C++ 里能继续用,但**“只能用不能忍”**的两点:不能初始化(除了 calloc 的清零)、对自定义类型不调构造/析构。所以 C++ 给了自己的方案。

3.3.1 内置类型

申请、初始化、释放的三种典型写法放在一起看:

int* p0 = new int;                 // 一个 int,未初始化
int* p1 = new int[10];             // 10 个 int,未初始化
delete p0;                         // 配对 delete
delete[] p1;                       // 配对 delete[](数组必须用 delete[])

int* p2 = new int(0);              // 一个 int,初始化为 0(圆括号)
int* p3 = new int[10]{0};          // 10 个 int,全部初始化为 0
int* p4 = new int[10]{1,2,3,4,5};  // 前 5 个 1..5,其余补 0
delete p2;
delete[] p3;
delete[] p4;

实测(*p2 = 0, p3[0..2] = 000, p4[0..6] = 1234500):

  • new int(0) 是"申请 + 给初值",new int[10]{0} 是"申请 10 个 + 全部给 0";
  • new int[10]{1,2,3,4,5} 只写了 5 个初始值,剩下的 5 个按 C++ 规则补 0,不是随机值;
  • 从语法就能看出和 malloc 的差别:new 后面跟类型,编译器自己算大小;malloc 后面跟字节数,要你自己 sizeof 再强转。

3.3.2 自定义类型(分水岭)

一句话就是全部:申请自定义类型的空间时,new 会调用构造函数,delete 会调用析构函数,而 malloc 与 free 不会。

对照着看最直观:

A* p1 = (A*)malloc(sizeof(A));   // 只拿到 sizeof(A) 字节,_a 是垃圾值,"还不算一个对象"
A* p2 = new A(1);                // 拿空间 + 调 A(1),这才是一个对象
free(p1);                        // 只还内存,不调 ~A()
delete p2;                       // 先 ~A(),再还内存

A* p5 = (A*)malloc(sizeof(A)*10);  // 10 份空间,0 次构造
A* p6 = new A[10];                 // 10 份空间,10 次构造
free(p5);
delete[] p6;                       // 10 次析构

一句话:malloc/free 是"内存级"的,new/delete 是"对象级"的。

3.4 new 的底层:operator new / operator delete(本节是重点)

这里最关键的一句话:new 和 delete 是操作符,operator new 和 operator delete 是系统提供的全局函数。new 在底层调用 operator new 申请空间,delete 在底层通过 operator delete 释放空间。

分清楚这两个名字,很多"到底调了谁"的问题就没了:

new / delete      → 语言层面的操作符,不能重载(可以重载的是 operator new)
operator new      → 全局函数,干的事只有一个:申请 size 字节
operator delete   → 全局函数,干的事只有一个:释放这块空间

MSVC 的 operator new 实现长这样:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    void *p;
    while ((p = malloc(size)) == 0)          // ① 底层还是 malloc
        if (_callnewh(size) == 0)            // ② 失败就问"空间不足应对措施"
        {
            static const std::bad_alloc nomem;
            _RAISE(nomem);                   // ③ 没人管,就抛 bad_alloc
        }
    return (p);
}

流程画出来:

            new T
              │
              ▼
      operator new(size)
              │
      ┌───────▼────────┐
      │  p = malloc(size) │
      └───────┬────────┘
              │
      ┌───────┴────────┐
    成功│              │失败(malloc 返回 NULL)
      ▼                ▼
   返回 p      是否设置了"空间不足应对措施"(new_handler)?
                       │               │
                     有│               │没有
                       ▼               ▼
                 执行它并再试 ──→ 抛 std::bad_alloc 异常

这个"空间不足应对措施",在标准库里就是 std::set_new_handler 注册的回调函数(void(*)()):malloc 失败时 operator new 会反复调用它,希望它释放点内存出来(典型用法:释放缓存、记日志、然后决定抛异常)。注意它是回调 —— 又是"控制反转"那套思路:库不决定释放什么,由你自己决定。

operator delete 那一半同样直白:

void operator delete(void *pUserData)
{
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK);
    // ... 校验块合法性(Debug 版)
    _free_dbg( pUserData, pHead->nBlockUse );   // 最终还是 free
    _munlock(_HEAP_LOCK);
    return;
}

// free 的 Debug 版就是一个宏
#define free(p)  _free_dbg(p, _NORMAL_BLOCK)

所以结论是:operator new/operator delete 本质就是 malloc/free 的包装,多做的事是"加异常语义 + 留可替换的钩子"。 这就是为什么面试问"new 和 malloc 的区别",答到最后一定会落到异常 vs NULL 上。

3.5 new / delete 的实现原理

内置类型:new/malloc、delete/free 基本类似,三个差别:

  1. new/delete 处理单个元素,new[]/delete[] 处理连续空间;
  2. new 申请失败抛异常,malloc 失败返回 NULL;
  3. new 后跟类型(自己算大小、能初始化),malloc 要自己算字节数。

自定义类型(四步流程,背下来):

new T     : operator new    ──→ 构造函数(1 次)
new T[N]  : operator new[]  ──→ operator new ──→ 构造函数(N 次,依次构造)

delete p    : 析构函数(1 次) ──→ operator delete
delete[] p  : 析构函数(N 次) ──→ operator delete[] ──→ operator delete

文字版:

  • new T:① 调 operator new 申请空间 → ② 在空间上执行构造函数;
  • new T[N]:① 调 operator new[](内部调 operator new)申请 N 个对象空间 → ② 执行 N 次构造函数;
  • delete p:① 执行析构函数清理资源 → ② 调 operator delete 释放空间;
  • delete[] p:① 执行 N 次析构函数 → ② 调 operator delete[](内部调 operator delete)释放空间。

注意顺序:new 是"先要空间再构造",delete 是"先析构再还空间"。 顺序反了就没法解释"为什么 new[] 必须先构造完才能给你指针"。

3.6 定位 new(placement new)

用途:在已经分配好的原始内存上,只调用构造函数,不申请空间。

new (place_address) type;                          // 默认构造
new (place_address) type(initializer-list);        // 带参构造

place_address 必须是指针,需要 #include <new>。完整用法是这样:

A* p1 = (A*)malloc(sizeof(A));   // p1 只是一段和 A 一样大的空间,还不是对象(没构造)
new (p1) A;                      // 显式调构造函数 —— 到这一步它才成为对象
p1->~A();                        // 手动调析构(因为它不是 new 出来的,没人替你调)
free(p1);

A* p2 = (A*)operator new(sizeof(A));  // 直接用 operator new 拿裸空间
new (p2) A(10);                       // 显式构造,带参要在这里传
p2->~A();
operator delete(p2);                  // 配 operator delete 释放

为什么要用它:内存池。 内存池一次申请一大块,切出来的内存没有初始化、也没有构造,所以自定义类型的对象必须用定位 new 显式调构造来初始化,用完后手动调析构再把内存还回池子。

这段代码还顺手揭示了 3.4 的结论:"申请空间"和"调构造函数"本来就是两个可以分开的动作 —— new 只是把这两个动作打包成一个操作符而已。

3.7 malloc/free 与 new/delete 的区别

共同点:都是从堆上申请空间,都需要用户手动释放。

#维度malloc / freenew / delete
1本质是函数是操作符
2初始化不初始化可以初始化(new int(10))
3大小手动 sizeof 算字节数并传参后跟类型即可;多个对象用 [] 给个数
4返回值void*,必须强转不需要,类型已由 new 后的类型决定
5失败返回 NULL,必须判空抛异常,必须 try/catch(不判空)
6自定义类型只开辟空间,不调构造/析构申请后调构造,释放前调析构

(还有一条别漏:malloc 配 free,new 配 delete,new[] 配 delete[]。)


4. 实测:new/delete 到底调了谁

4.1 探针类 A

要看清编译器到底调了谁,最有效的办法是写一个"探针类":每个特殊成员函数里都打印一句话,让它们自己报数。

class A
{
public:
    A(int a1 = 0, int a2 = 0) : _a1(a1), _a2(a2) { cout << "A(int a1 = 0, int a2 = 0)" << endl; }
    A(const A& aa) : _a1(aa._a1)                  { cout << "A(const A& aa)" << endl; }   // 只拷了 _a1!
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;
        if (this != &aa) { _a1 = aa._a1; }        // 也只赋了 _a1!
        return *this;
    }
    ~A() { cout << "~A()" << endl; }              // 若类里出现指针成员,这里必须 delete _ptr;
    void Print() { cout << "A::Print->" << _a1 << endl; }
    A& operator++() { _a1 += 100; return *this; } // 前置 ++,返回引用(支持 ++++a)
private:
    int _a1 = 1;      // 类内默认值
    int _a2 = 1;
};

三个值得注意的设计:

  1. 构造函数带默认参数(a1 = 0, a2 = 0)→ 它既是默认构造,又是"收一个 int 就能用"的转换构造,所以 A a = 1; 这种写法合法(见 4.4);
  2. ~A() 现在什么都不用做:当前 A 只有两个 int 成员,没有指针,析构里没有资源要清理 —— 但记住这条规则:一旦类里出现 new 出来的指针成员,析构函数里就必须有对应的 delete,同时必须写深拷贝的拷贝构造和赋值重载(三法则);
  3. Print() 只打印 _a1 —— 也就是说 _a2 从头到尾没被任何代码读过。直接这么编译(-Wall -Wextra)会实打实地报一条 private field '_a2' is not used;为了观察 _a2 的实际取值,我在 Print() 里把它也打印了出来(下面的实测输出会标出来)。这条警告和 5.3 踩坑 1 直接相关,别错过。

4.2 内置类型的 new / delete

先看最基本的一组:一个 int、一个 int 数组,以及带初值的申请。

int * p0  = new int;
int * p1  = new int[10];
delete p0;
delete[] p1;

int * p2 = new int(0);
int * p3 = new int[10]{0};
int * p4 = new int[10]{1,2,3,4,5};
delete p2;
delete[] p3;
delete[] p4;

实测输出:

*p2 = 0, p3[0..2] = 000, p4[0..6] = 1234500

三条结论(都对上 3.3.1):

  • new int(0)、new int[10]{0}、new int[10]{1,2,3,4,5} 都真的初始化了,这是 malloc 给不了的;
  • p4 只写了 5 个初值,p4[5]、p4[6] 是 0(补零,不是随机值);
  • 申请数组用 new[],释放就必须用 delete[],配对使用。

4.3 自定义类型:Listnode 链表

struct Listnode{
    int _val;
    Listnode * next;
    Listnode(int val = 1): _val(val),next(nullptr) { }
};

用 new 手搭一条链表,验证"构造是不是真的被调了":

Listnode * p1 = new Listnode();
Listnode * p2 = new Listnode(2);
Listnode * p3 = new Listnode(3);
Listnode * p4 = new Listnode(4);
p1->next = p2;  p2->next = p3;  p3->next = p4;  p4->next = nullptr;

给 Listnode 的构造和析构各加一句打印,实测输出:

Listnode(int val = 1):0x104d751d0
Listnode(int val = 1):0x104d750a0
Listnode(int val = 1):0x104d750b0
Listnode(int val = 1):0x104d750c0
1 2 3 4
~Listnode():0x104d751d0
~Listnode():0x104d750a0
~Listnode():0x104d750b0
~Listnode():0x104d750c0

验证了三件事:

  1. 每个 new Listnode(...) 都调了一次构造函数,new Listnode() 走的是默认参数 val = 1;
  2. delete 时每个节点都调了一次析构 —— malloc 做这件事是完全不可能的;
  3. 4 个节点的地址在堆上连成一片(0x104d75xxx),和 3.1 里 *ptr1 的堆地址规律一致。

⚠️ 但上面这段代码没有 delete:4 个节点 new 出来就再也没还过。演示可以,真实代码里这就是 4 处内存泄漏(见第 6 节修正)。

4.4 对象的拷贝发生在什么时候

这部分本来属于拷贝构造,但放在内存视角下看特别有说服力(栈上的临时对象也是"内存管理")。

① 隐式类型转换

A aa1 = 1;
const A& aa2 = 1;

实测:

A(int a1 = 0, int a2 = 0)      ← A aa1 = 1;      int → A 隐式转换构造
A(int a1 = 0, int a2 = 0)      ← const A& aa2 = 1; 临时对象绑定到常引用

A aa1 = 1; 之所以合法,是因为构造函数"给两个默认参数后只传一个 int 也能调" —— 编译器拿它当作单参构造,允许隐式转换。第二行是常引用绑定临时对象:临时对象本来该在这一行结束就销毁,绑定到 const A& 后生命周期被延长到引用离开作用域。

想禁止这种隐式转换就写 explicit A(int a1 = 0, int a2 = 0);。这是 C++ 里"能编译通过但不是你想写的代码"的经典来源。

② 按值传参:void f1(A aa)

A aa1(1);
f1(aa1);      // 传左值
f1(A(1));     // 传匿名对象
f1(1);        // 传 int,隐式转换出匿名对象

实测输出:

A(int a1 = 0, int a2 = 0)        ← A cc(3, 5);
A(const A& aa)                   ← f1(cc);   传左值 → 实打实的拷贝构造
A::Print->_a1=3 _a2=1            ← 拷进来的对象 _a2 丢了,用了类内默认值 1
~A()                             ← 形参 aa 出作用域析构
--
A(int a1 = 0, int a2 = 0)        ← f1(A(1)); 直接构造,没有拷贝
A::Print->_a1=1 _a2=0
~A()
--
A(int a1 = 0, int a2 = 0)        ← f1(1);  同样只有一次构造,没有拷贝
A::Print->_a1=1 _a2=0
~A()

结论:

实参形式普通构造 A(int,int)拷贝构造 A(const A&)说明
f1(aa1)(左值实参)01按值传参会真的拷贝一份,改形参不影响实参
f1(A(1))(匿名对象)10临时对象直接在形参位置构造(拷贝被省略)
f1(1)(隐式转换)10同上,int 先转成匿名 A

这就是 -Wall -Wextra 也报不出来、但性能上实实在在的拷贝 —— 结论:只读参数一律写 const A&(第 6 节修正)。

③ 返回局部对象:A f2()

A f2() { A aa(1); ++aa; return aa; }

实测:

A(int a1 = 0, int a2 = 0)        ← f2() 里的 aa 直接在调用方要用的位置上构造
A::Print->_a1=101 _a2=0          ← 前置 ++ 生效(1 + 100)
~A()                             ← 只在 main 结束时析构一次

即使 -O0,return aa; 也没有输出 A(const A& aa) —— 编译器把局部对象直接构造在调用方的目标位置(返回值优化 NRVO)。所以"返回对象会多一次拷贝"在今天的编译器上通常不成立,不要为了省这次拷贝而返回指针(返回局部对象的地址才是真错误)。

4.5 new 失败会发生什么

这里可以做两套实验:

实验一:一次性申请三个 1GB

void * p1 = new char [1024*1024*1024];
void * p2 = new char [1024*1024*1024];
void * p3 = new char [1024*1024*1024];

实测:三次全部成功(0x79ae400000、0x79ee400000、0x7a2e400000)。

原因不是"内存够",而是 macOS 和 Linux 一样超售(overcommit)虚拟内存:new 拿到的是虚拟地址,只有真正写入页面时才会分配物理内存。所以"申请 1GB 没崩"不代表你占了 1GB。

实验二:死循环申请 1MB(func())

void func()
{
    int n = 1;
    while(1)
    {
        void * p1 = new char [1024*1024];
        cout << p1 << "->" << n << endl;    // 既没 delete,也不需要 p1,纯粹观察
        ++n;
    }
}

int main()
{
    try { func(); }
    catch(const exception & e) { cout << e.what() << endl; }
    return 0;
}

跑 4 秒后手动 kill,实测结果:

0x7b53400000->1
0x7b53500000->2
0x7b53600000->3
...
0x1245e00000->721387
共 721387 行,约 700 GB 虚拟地址

三点观察:

  1. 地址单调递增(不是每次正好 +1MB,因为 malloc 是成块向系统要大区域再切分)—— 这是"堆向上增长"在真实程序里的样子;
  2. 4 秒就吃到 700GB 虚拟空间还没有抛异常:在 overcommit 的机器上,这个 while(1) 可能跑很久都等不到 bad_alloc,中途只会把地址打满屏。这段代码的真正价值是演示意图,不是演示结果;
  3. catch(const exception & e) 的写法是对的(按引用捕获,避免对象切片),但它在这台机器上基本轮不到执行。

那怎么可靠地看到 bad_alloc? 实测两个办法:

// ① 申请超过用户态虚拟地址空间能给的量:直接抛 bad_alloc
try { void* p = new char[64ull*1024*1024*1024*1024]; }   // 64TB
catch (const std::exception& e) { cout << e.what() << endl; }
// 实测输出:std::bad_alloc

// ② Linux 上限制进程虚拟内存(后端主战场,推荐)
// $ ulimit -v 524288        # 512MB
// $ ./a.out                 # 很快就会打印 std::bad_alloc

(顺带实测:macOS 的 bash 直接 ulimit -v 会报 Invalid argument,setrlimit(RLIMIT_AS) 也返回 -1 —— 这不是命令写错了,是 Darwin 不支持这两种限制方式。)


5. 注释提炼

5.1 知识注解类 —— 把容易滑过去的那句话抠细

“new 后面跟类型就行,不用自己算大小”

写惯 C 之后,手会先去找 sizeof。但看这一行:new 后面直接跟类型名就够了,编译器负责算大小、返回正确类型的指针。

A* p1 = new A();                                  // 类型即大小,不需要 sizeof,不需要强转
A* p2 = (A*)malloc(sizeof(A));                    // 大小自己算,返回 void* 自己强转

差别不只在于"少写几个字":malloc 那行万一 sizeof 写错(比如写成 sizeof(A*),指针大小),编译器不会拦你;new A 大小写错了根本编译不过。类型安全不是风格问题,是 bug 数量问题。

为什么说"内置类型几乎和 malloc 一样"

要理解这句"几乎":内置类型没有构造/析构,所以 new 相对 malloc 只多做了两件事 —— 初始化(可选)和异常语义。真正拉开差距的是自定义类型。

为什么 catch 要写 const exception&

catch(exception e) 是按值捕获:异常对象会被拷贝一份,如果抛的是 bad_alloc 而按 exception 值捕获,多态信息就被"切"没了(对象切片)。写 const std::exception& 才能拿到 what() 里真正的内容(实测打印出 std::bad_alloc 就是靠它)。

5.2 个人理解类

我的理解:new 的本质是"把两件事打包"。

把 new 拆开看就一目了然 —— operator new 负责"要空间",构造函数负责"变成对象",定位 new 就是把这两件事拆开单独用的工具(3.6)。所以 new 不是"更高级的 malloc",它是语言为"对象"这个概念特意做的语法糖:因为 C 里根本没有"对象",所以 C 的 malloc 也不需要调用构造。

我的理解:operator new 的 while 循环揭示了"内存不足"是可以协商的。

malloc 失败就返回 NULL,调用方(库)没有任何办法;operator new 在失败和抛异常之间插了一个"回调你自己注册的处理函数"的环节。这就是把"策略"交给使用者:库负责机制(申请不到就问你),你负责策略(丢缓存还是直接崩)。这个套路(机制在内、策略在外)在后端里到处都是:缓存淘汰策略、线程池拒绝策略、连接池超时策略,全是同一个形状。

5.3 踩坑感悟

踩坑 1:拷贝构造和赋值重载漏了 _a2,编译器一声不吭

A(const A& aa) : _a1(aa._a1) { }                     // _a2 没拷
A& operator=(const A& aa) { if (this != &aa) _a1 = aa._a1; return *this; }  // _a2 没赋

实测证据(A s1(7, 8); A s2(s1); A s3(9,9); s3 = s1;):

A::Print->_a1=7 _a2=1     ← 拷贝出来的 s2:_a2 掉回类内默认值 1(不是 8!)
A::Print->_a1=7 _a2=9     ← 赋值后的 s3:_a2 保持原值 9(不是 8!)

为什么这个 bug 阴险:_a2 有类内默认值 = 1,所以"没被初始化"变成了"被默认值初始化",运行时不崩、不报错,只是数据悄悄丢了。如果 _a2 没有默认值,反而会踩到"未初始化成员是随机值"的老问题。

那编译器警告能兜住吗?——不完全能。 直接 -Wall -Wextra 编译,确实会报一条 private field '_a2' is not used,但理由是"没人读 _a2",不是"拷贝时漏了 _a2"。我把它加进 Print() 之后,这条警告立刻消失,而漏拷贝的 bug 一点变化都没有。也就是说:-Wunused-private-field 只能帮你判断"这个成员是不是多余",检测不出"这个成员忘了拷" —— 真实项目里 _a2 只要被任何一处读到,警告就没了,问题却还在。

这条坑的教训不是"记得多写一行",而是:写拷贝构造/赋值前,先把类的成员一个一个数一遍。 成员一多就该考虑 = default(编译器生成的版本会老老实实拷所有成员)或拷贝再交换(copy-and-swap)的写法。

踩坑 2:new[] 配 delete(以及 new 配 delete[])

int* p = new int[10];
delete p;       // 错!数组要用 delete[]

对内置类型可能"看起来没事",对自定义类型(比如 4.3 的 Listnode)就是只析构第一个对象、堆结构被破坏。规则:new 配 delete,new[] 配 delete[],一个字都不能省。

踩坑 3:realloc 之后再去 free 原指针

前面那段 Test() 里 free(p3) 是对的,free(p2) 就是 double free。详见 3.2 —— realloc 成功时旧指针已经失效,失败时(返回 NULL)旧指针才还需要你 free。

踩坑 4:只读参数写成按值传参

void f1(A aa) 每调一次就拷贝一次对象(实测输出里的 A(const A& aa))。对象一大(里面还有 vector、string、数组),这个开销直接进热路径。只读 → const A&;要改副本 → 按值,但自己知道在拷贝什么。

踩坑 5:func() 那种写法在真机上几乎等不到 bad_alloc

while(1) + 每次 1MB + 不 delete,在 overcommit 的系统上 4 秒能刷 72 万行、吃 700GB 虚拟空间(实测),程序既不崩也不进 catch。想验证"new 失败抛异常",要么申请一个明显超过地址空间大小的块(new char[64TB],实测抛 std::bad_alloc),要么在 Linux 上用 ulimit -v 把地址空间限死。 靠"慢慢把内存耗光"这个思路在 macOS 上不成立。

踩坑 6:中文注释在编码混编下会悄悄变成乱码

比如注释里本该写"匿名对象",存盘之后却变成了一个奇怪的字符:

写下的:  // 匿名对象
存盘后:  // Ż

这是编码问题留下的疤。GBK 和 UTF-8 混编时,中文注释就是这样悄悄坏掉的。养成习惯:C++ 源文件统一 UTF-8,编辑器关掉自动猜编码 —— 注释坏了比代码错了更难发现,因为它不影响编译。

5.4 对比总结

按存储位置选"把数据放哪":

数据放哪何时申请谁负责释放
生命周期明确的小对象栈(局部变量)进作用域编译器(离开作用域)
需要跨函数存活 / 大小运行时才定堆(new / malloc)你说了算你(delete / free)
全程序共享、只一份数据段(全局/静态)程序启动程序结束
只读文本代码段(字面量)编译期不用管,也别改(改就是 UB)

四组 API 的关系一图流:

                         new T / new T[N]

       ①申请空间  ◄──────────────────────────────────►  ②构造对象
            │                                              │
            ▼                                              ▼
   operator new / operator new[]                 T()  /  T[N] 个构造函数
            │
            ▼
      malloc(size)  ── 失败 ──→ std::bad_alloc(C 里是返回 NULL)

        delete p / delete[] p :析构(1 次 / N 次)──→ operator delete ──→ free

面试一句话版本:new = operator new(底层 malloc,失败抛 bad_alloc)+ 构造函数;delete = 析构函数 + operator delete(底层 free)。所以对内置类型二者等价,对自定义类型差一个完整的对象生命周期。


6. 代码修正

修正 1:拷贝构造补齐 _a2(真实 bug)

// 修改前:_a2 掉回类内默认值 1
A(const A& aa) : _a1(aa._a1) { cout << "A(const A& aa)" << endl; }

// 修改后:成员初始化列表补齐,顺序与声明顺序一致(_a1 在前)
A(const A& aa)
    : _a1(aa._a1)
    , _a2(aa._a2)
{
    cout << "A(const A& aa)" << endl;
}

修正 2:赋值重载补齐 _a2

// 修改前
A& operator=(const A& aa)
{
    if (this != &aa) { _a1 = aa._a1; }   // _a2 丢了
    return *this;
}

// 修改后:两个成员都要赋值,返回值支持连续赋值 a = b = c
A& operator=(const A& aa)
{
    if (this != &aa)
    {
        _a1 = aa._a1;
        _a2 = aa._a2;
    }
    return *this;
}

如果类里将来真的出现指针成员,这两处必须改成深拷贝(_ptr = new int(*aa._ptr);)+ 析构里 delete _ptr,也就是让 ~A() 里真的写上 delete _ptr; —— 三法则缺一不可。

修正 3:只读参数用常引用,别按值拷贝

// 修改前:每次调用拷贝一个对象
void f1(A aa) { aa.Print(); }

// 修改后:不拷贝;函数内确实要改就自己拷一份局部变量
void f1(const A& aa) { aa.Print(); }

注意:Print() 目前是非 const 成员函数,改成 const A& 后必须把它也改成 void Print() const,否则编译不过。这也是为什么"成员函数该加 const 就加 const"是习惯问题,不是洁癖。

顺带一提:f1 是空函数体,直接编译还会报 unused parameter 'aa' —— 这里写它只是为了验证"什么时候发生拷贝",形参压根没用,所以这个警告本身是合理的。

修正 4:链表节点必须释放

// 修改前:4 个节点 new 出来就不管了
// Listnode* p1 = new Listnode();
// ...

// 修改后:谁申请谁释放,遍历释放
Listnode* p1 = new Listnode();
Listnode* p2 = new Listnode(2);
Listnode* p3 = new Listnode(3);
Listnode* p4 = new Listnode(4);
p1->next = p2; p2->next = p3; p3->next = p4; p4->next = nullptr;

for (Listnode* cur = p1; cur != nullptr; )
{
    Listnode* next = cur->next;   // 先存下一个节点,再 delete 当前节点
    delete cur;
    cur = next;
}

顺序很重要:delete cur 之后再读 cur->next 就是访问已释放内存。真实工程里更该做的是把"申请/释放"绑在一起(unique_ptr + 自定义链表析构,属于下一讲 RAII 的内容)。

修正 5:func() 的内存耗尽实验改成能收场的写法

// 修改前:无限循环 + 每次 1MB 从不释放,且 catch 基本等不到
void func()
{
    int n = 1;
    while(1)
    {
        void * p1 = new char [1024*1024];
        cout << p1 << "->" << n << endl;
        ++n;
    }
}

// 修改后:① 要观察"堆向上增长",就跑有限次并及时释放
void func(int times)
{
    for (int n = 1; n <= times; ++n)
    {
        void* p1 = new char[1024 * 1024];
        cout << p1 << "->" << n << endl;
        delete[] p1;              // 观察完就还回去,不泄漏
    }
}

// ② 要观察 bad_alloc,直接申请一个超大的块(不要靠"慢慢耗")
int main()
{
    try
    {
        void* p1 = new char[64ull * 1024 * 1024 * 1024 * 1024];   // 64TB
        cout << p1 << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;     // 实测输出:std::bad_alloc
    }
    return 0;
}

new char[...] 的返回值直接丢给 void* 是没问题的(new 返回的就是对应类型的指针);但不 delete 就一定会泄漏,哪怕你手里只有 void*,delete[] (char*)p1 一样能还回去 —— 类型信息用来决定"怎么还",不是"能不能还"。


7. 小结

这一块的主线只有一条:C 的内存管理停在了"空间"这一层,C++ 往上加了"对象"这一层。

  • 内存分布在讲"空间":栈、堆、静态区、常量区,char2 和 *char2 同地址、pChar3 和 *pChar3 不同地址,这一组对比就是全部考点;
  • malloc/calloc/realloc 在讲"空间":给字节数、拿 void*、失败给 NULL、要你自己算大小;
  • new/delete 在讲"对象":给类型、拿指针、失败抛异常、自动调构造和析构;
  • operator new/delete 是这两层之间的桥:它证明 new 底下的"申请空间"就是 malloc,多出来的是异常语义和可替换的钩子;
  • 定位 new 则是最后一块拼图:它证明"申请空间"和"构造对象"本来就是两个动作,new 只是把它们打包了。

把这几层顺序理顺,再把上面那些实验自己写一遍、跑一遍(对着打印看谁被调用了),这一块就不会再有"背了但不确定"的地方。

文中所有运行结果均由 g++ -g -O0 -Wall -Wextra -std=c++17 在本机(macOS arm64)实跑得到;涉及平台差异(overcommit、ulimit -v、malloc 实现)的地方已单独标注。

Lumerical FDTD新手必看:从零开始搭建光子晶体仿真环境(附完整脚本) 本文为Lumerical FDTD新手提供光子晶体仿真环境的完整搭建指南,涵盖从基础参数设置到脚本编写的全流程。通过典型光子晶体带隙分析案例,详细讲解结构建模、仿真参数配置及后处理技巧,并附赠完整MATLAB脚本,帮助用户快速掌握FDTD仿真核心技术。 阅读详情

相关推荐

突破III-V/硅混合集成的挑战:UTC光电探测器能带工程仿真实践

本文探讨了UTC光电探测器在III-V/硅混合集成中的能带工程仿真实践,通过Ansys Lumerical平台实现多物理场仿真,解决晶格失配和能带不连续性等关键挑战。文章详细介绍了FDTD光学仿真和CHARGE电学仿真方法,优化器件性能,提升响应度和带宽,为高速光通信和量子计算提供技术支持。

weixin_29219539的博客 296

C++中new/delete 和malloc/free的区别

1、new、delete是C++中的操作符,而malloc和free是标准库函数。2、对于非内部数据对象来说,只使用malloc是无法完成动态对象要求的,一般在创建对象时需要调用构造函数,对象消亡时,自动的调用析构函数。而malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而NEW在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对...

gaosaixing的博客 1万+

关于 Python PyQt5 界面运行时提示无法初始化Qt平台的解决方案

关于 Python PyQt5 界面运行时提示无法初始化Qt平台的解决方案

smart_num_1的博客 6883

new/delete相较于malloc/free的区别

一篇博客搞懂malloc/free和new/delete的区别

shylyly_的博客 1526

51单片机STC8G1K08驱动ws2812记录

使用51单片机控制17个2812灯,这里选择STC8芯片,最大频率可以达到35M,参考网上的各种文章案例,使用定时器做的时候实测做不到规格书要求的245-345ns,STC8G1K08不带PWM,所以只能使用nop来做,好在单片机在这里只有做编程灯的功能,省略测试过程,下面是程序,使用的是24MHz内部晶振 //设置一个灯珠 void SendOnePix(unsigned char *ptr) { unsigned char i,j; unsigned char temp; fo...

的博客 7320

避坑指南:C#用S7NetPlus读写西门子PLC字符串(String/WString)的完整方案

本文详细解析了C#通过S7NetPlus读写西门子PLC字符串(String/WString)的完整方案,包括内存布局、编码差异和字节序处理等关键技术细节。提供了实战优化的代码示例和常见问题排查指南,帮助开发者避免乱码、截断等常见陷阱,提升工业自动化项目的开发效率。

weixin_32956805的博客 320

C++内存管理——malloc/free和new/delete

C++ 提供了多种内存管理方式,包括传统的 C 风格的 malloc和 free,以及 C++ 专用的 new和 delete。理解这些内存管理方法对于编写高效、安全的 C++ 程序至关重要。一、了解内存分区自动管理内存内存分配和释放速度快,但空间有限动态分配与释放灵活但易出错(如内存泄漏、悬挂指针)。程序启动时分配,程序结束时释放static存储常量,不可修改,程序结束时释放存储程序的二进制指令,只读。

2401_84073201的博客 1403

电子硬件仿真:设计一个频率范围为10k—50k的带通滤波器

带通滤波器是一种常用的滤波器类型,具有选择特定频率范围内信号的能力,并在该范围内提供一定的增益。它允许通过一定范围内的频率信号,并在该范围内提供一定的增益,同时在该范围之外的频率上提供较高的衰减。这意味着在通带内(10kHz至50kHz),滤波器的增益应为10dB,而在每个十倍频的频率点上,滤波器的增益应降低40dB。设计滤波器:使用滤波器设计工具(如MATLAB、Python的scipy库、Tina软件等),根据选定的滤波器类型(例如巴特沃斯滤波器),中心频率和通带宽度,设计一个4阶带通滤波器。

走向CTO的路上... 579

c++内存管理——malloc calloc realloc free new delete operator new operator delete

new delete 和 malloc free 最大的区别是new除了开空间还会调用构造函数,delete除了释放空间还会调用析构函数。(对内置类型基本一样)1.调用operator new函数申请空间。2. 在申请的空间上执行构造函数,完成对象的构造。

shsbyshdbdhjsk的博客 1102

cannot import name ‘BUFSIZE‘ from ‘numpy‘ 解决方案

本文主要介绍了cannot import name ‘BUFSIZE’ from ‘numpy’ 解决方案,希望能对使用numpy的同学们有所帮助。 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 22万+

采样率转化代码C++(有理倍数,升采样、降采样均可)

【代码】采样率转化(有理倍数,升采样、降采样均可)

goudahai的专栏 547

C++ malloc/free/new/delete详解(内存管理)

new是动态分配内存的运算符,自动计算需要分配的空间,在C++中,它属于重载运算符,可以对多种数据类型形式进行分配内存空间,比如int型、char型、结构体型和类等的动态申请的内存分配,分配类的内存空间时,同时调用类的构造函数,对内存空间进行初始化,即完成类的初始化工作。delete与new通常配对使用,与new的功能相反,可以对多种数据类型形式的内存进行撤销,包括类,撤销类的内存空间时,它要调用其析构函数,完成相应的清理工作,收回相应的内存资源。这告诉我们,可以认为new就是malloc的封装。

qq_41897304的博客 1612

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

Robot_PLC_HMI_Automation 6100

C++ malloc & free、new & delete

malloc函数用于动态分配内存,它的原型为。它返回一个指向连续内存块的指针,该内存块的大小至少为size字节,如果无法分配请求的大小,则返回NULL。free函数用于释放动态分配的内存,它的原型为。它将指向动态分配的内存块的指针ptr作为参数,并将该内存块释放,使其可供后续的动态分配。

qq_45780653的博客 1714

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

new/delete和malloc/free的区别联系

1.先来谈谈new和delete 在C++中堆内存的分配和释放是通过new和delete来操作的,它们和C语言的malloc和free有什么区别呢? (1)new的底层也是通过malloc来开辟内存的 new比malloc多一项功能,就是开辟完内存,还可以进行初始化操作 如下: int *p = new int(10); 上面是new的基本操作,10代表堆上开辟的整形内存的初始值;如果是自定义类类型的话,如下: Test *p = new Test(); // Tset是一个实现定义好的类 这个语句不

qq_37535749的博客 1389

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

【C++】new/delete 还是 malloc/free?C++内存管理的“世纪抉择

文章系统讲解了C/C++内存管理机制。首先分析了程序内存布局,包括栈、堆、静态区等区域的特性及变量存储位置。重点对比了C语言(malloc/free)和C++(new/delete)动态内存管理方式:new/delete会调用构造/析构函数且类型安全,而malloc/free需要手动计算大小和类型转换。文章详细剖析了new/delete的底层实现原理(通过operator new/delete调用malloc/free),并介绍了定位new表达式等高级用法。最后总结了两种方法的区别。

w37773698的博客 435

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

【C/C++内存管理】关于 new / delete 和 malloc / free

new。

qq_73758702的博客 756

new、delete和malloc、free的区别

malloc、free和new、delete的区别

qq_50993361的博客 929

C/C++内存管理 new/delete、malloc/realloc/calloc/free详解

new的原理:(1)调用operator new函数申请空间(2)在申请的空间上执行构造函数,完成对象的构造delete的原理(1)在空间上执行析构函数,完成对象中资源的清理工作(2)调用operator delete函数释放对象的空间new T[N]的原理(1)调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请(2)在申请的空间上执行N次构造函数delete[]的原理。...

博客记录学习轨迹,方便日后复习 945

C++内存管理:new/delete与malloc/free核心差异与实战指南

内存管理是编程语言的核心基础,它决定了程序如何高效、安全地使用系统资源。在C++中,动态内存分配涉及两个关键概念:C语言风格的malloc/free函数和C++特有的new/delete操作符。从原理上看,malloc/free是纯粹的内存分配器,只负责原始内存块的申请与释放;而new/delete则是对象生命周期管理器,在分配内存的同时自动调用构造函数和析构函数,实现了C++的RAII(资源获取即初始化)核心机制。这种设计差异带来了类型安全、异常处理、与面向对象特性深度协同等技术价值,使得new/dele

weixin_30776545的博客 455

【C++】new、delete,malloc/free 与new/delete区别

【C++】new、delete,malloc/free 与new/delete区别

小何同学的博客 656

C++中有了malloc/free 为什么还要new/delete

malloc 与free 是C++/C 语言的标准库函数,new/delete 是C++的运算符。它们都可用于申请动态内存和释放内存。 对于非内部数据类型(内部数据类型是编译器本来就认识的,不需要用户自己定义。 非内部数据类型不是编译器本来就认识的,需要用户自己定义才能让编译器识别。)的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象在消亡

liu_qiqi的专栏 1275

malloc/free与new/delete

一、基本概念 malloc/free: 1、函数原型及说明: void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。 void free(void *FirstByte): 该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。 2.内存操作: malloc函数的参数是接受需要分配的内存字节数,如果内存能够满足请求量,那么将会返回:指向被

cz的博客 1019

【C++】详谈malloc/free和new/delete的区别

new = 全局operator new开内存(先) + 构造函数(后) operator new = malloc ,若开辟空间失败,会抛异常 delete = 析构函数(先) + 全局operator delete释放内存(后) operator delete = free 这体现了封装 operator new/delete 不是运算符重载,是调用全局函数,而函数里面包含了malloc/free 此外,operator new和operator delete能够进行专属类重载,也就是说,.

DanteKing的博客 1047

C++内存管理:从malloc/free到new/delete的转换

本文介绍了C++内存管理方式,重点对比了new/delete与malloc/free的区别。主要内容包括:1) new/delete操作内置类型时与malloc/free功能类似;2) 对于自定义类型,new会调用构造函数,delete会调用析构函数,而malloc/free不会;3) operator new和operator delete是系统提供的全局函数,new/delete底层调用这些函数进行内存分配和释放;4) 使用时要匹配new/delete和new[]/delete[]。文章通过代码示例展示

2601_95035594的博客 215

C++中动态内存管理,用malloc/free模拟实现new/new[],delete/delete[]

     C++中动态内存管理的方式与C语言有所不同,在c++中通过new和delete运算符进行动态内存管理,通过深入剖析new/new[]和delete/delete[]来对c++的内存管理有更深入的了解C语言堆上使用malloc/calloc/realloc/free进行动态内存管理, malloc/calloc/realloc用来在堆上开辟空间,free将申请的空间释放掉面试题:mallo...

zimituanzi_的博客 1000
上一篇: C++ 类与对象终极解析:从“能跑“到“能上工程“
下一篇: C++ 模板初阶 —— 让编译器帮你写“同一套逻辑、不同类型“的代码
DDXYcoder
博客等级 码龄1年 140粉丝 29原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值