C++ 内存管理详解:从 malloc/free 到 new/delete

🎬 个人主页 :道尔柯南
❄专栏传送门 :《C语言》《C++》《Linux操作系统》

昙花一现,却等待了整个白昼;蝉鸣一夏,却蛰伏了好几个四季。

文章目录

    • [一、为什么要学习 C++ 内存管理?](#一、为什么要学习 C++ 内存管理?)
    • [二、C/C++ 程序的内存分布](#二、C/C++ 程序的内存分布)
      • [2.1 程序运行时,内存是如何划分的?](#2.1 程序运行时,内存是如何划分的?)
      • [2.2 栈区:存放普通局部变量等数据](#2.2 栈区:存放普通局部变量等数据)
      • [2.3 堆区:存放动态申请的内存](#2.3 堆区:存放动态申请的内存)
      • [2.4 数据段:存放全局变量和静态变量](#2.4 数据段:存放全局变量和静态变量)
      • [2.5 代码段与只读常量](#2.5 代码段与只读常量)
    • 三、通过代码练习判断变量存储位置
    • [四、C 语言的动态内存管理:malloc、calloc、realloc 和 free](#四、C 语言的动态内存管理:malloc、calloc、realloc 和 free)
      • [4.1 malloc:申请动态内存](#4.1 malloc:申请动态内存)
      • [4.2 calloc:申请内存并清零](#4.2 calloc:申请内存并清零)
      • [4.3 realloc:调整动态内存大小](#4.3 realloc:调整动态内存大小)
      • [4.4 free:释放动态内存](#4.4 free:释放动态内存)
      • [4.5 四个函数的区别](#4.5 四个函数的区别)
    • [五、C++ 的动态内存管理:new 和 delete](#五、C++ 的动态内存管理:new 和 delete)
      • [5.1 new:动态申请并初始化对象](#5.1 new:动态申请并初始化对象)
      • [5.2 new\[\]:申请连续的多个对象](#5.2 new[]:申请连续的多个对象)
      • [5.3 new 和 delete 如何处理自定义类型?](#5.3 new 和 delete 如何处理自定义类型?)
      • [5.4 malloc/free 与自定义类型](#5.4 malloc/free 与自定义类型)
    • [六、operator new 与 operator delete:理解底层原理](#六、operator new 与 operator delete:理解底层原理)
      • [6.1 operator new 是什么?](#6.1 operator new 是什么?)
      • [6.2 operator new 与 malloc 的关系](#6.2 operator new 与 malloc 的关系)
      • [6.3 operator delete 与 free 的关系](#6.3 operator delete 与 free 的关系)
    • [七、new 和 delete 的实现原理](#七、new 和 delete 的实现原理)
      • [7.1 内置类型的实现原理](#7.1 内置类型的实现原理)
      • [7.2 自定义类型的 new 原理](#7.2 自定义类型的 new 原理)
      • [7.3 自定义类型的 delete 原理](#7.3 自定义类型的 delete 原理)
      • [7.4 new\[\] 和 delete\[\] 的原理](#7.4 new[] 和 delete[] 的原理)
    • [八、定位 new 表达式:在已有内存中构造对象](#八、定位 new 表达式:在已有内存中构造对象)
      • [8.1 定位 new 的基本语法](#8.1 定位 new 的基本语法)
      • [8.2 定位 new 示例](#8.2 定位 new 示例)
      • [8.3 定位 new 有什么应用场景?](#8.3 定位 new 有什么应用场景?)
    • [九、malloc/free 与 new/delete 的区别总结](#九、malloc/free 与 new/delete 的区别总结)
    • 十、常见错误与避坑指南
    • [十一、总结:建立完整的 C++ 内存管理知识体系](#十一、总结:建立完整的 C++ 内存管理知识体系)
    • 十二、课后练习
  • 结尾

一、为什么要学习 C++ 内存管理?

学习 C++ 时,我们经常会看到这样的代码:

复制代码
int a = 10;

int* p = new int(20);

delete p;

这段代码看起来很简单,但它涉及几个重要的问题:

  • 变量 a 和指针 p 存储在哪里?
  • new 申请的内存来自哪里?
  • new 和 malloc 有什么区别?
  • 为什么使用 new 申请的内存要使用 delete 释放?
  • 如果申请内存后忘记释放,会发生什么?

这些问题都属于 C++ 内存管理 的范畴。

简单来说,内存管理就是:了解程序运行时内存的组织方式,并正确地申请、使用和释放内存。

掌握这部分知识,不仅有助于理解 C++ 的运行机制,也是学习类与对象、指针、构造函数、析构函数以及面试题的重要基础。

二、C/C++ 程序的内存分布

2.1 程序运行时,内存是如何划分的?

程序运行时,并不是所有变量都存放在同一个地方,可以把相关内存区域概括为以下几部分:

栈区(Stack)

局部变量、函数参数等

堆区(Heap)

程序运行时动态申请的内存

数据段(静态区)

全局变量、静态变量

代码段(常量区)

可执行代码、只读常量等

内存映射区域

可用于共享库映射、共享内存等场景

实际进程的内存布局由操作系统、编译器和平台决定。

下面分别介绍这些区域。

2.2 栈区:存放普通局部变量等数据

例如:

复制代码
void Test()
{
    int a = 10;
    int b = 20;
}

这里的 a 和 b 是普通局部变量。

在常见的 C++ 实现中,它们的自动存储期对象通常由函数调用所使用的栈来承载。函数执行结束后,这些局部变量的生命周期也随之结束。

栈的特点:

  • 主要用于函数调用相关的数据和自动存储期对象。
  • 一般不需要程序员手动释放局部变量占用的存储空间。
  • 栈空间通常有限,递归过深或局部对象过大可能导致栈溢出。

初学者可以先记住:普通局部变量通常具有自动存储期,不需要使用 delete 释放。

2.3 堆区:存放动态申请的内存

如果程序需要在运行过程中申请内存,就可以使用动态内存分配。

例如:

复制代码
int* p = new int(10);

delete p;
p = nullptr;

这里需要区分两个概念:

  • p 是一个指针变量,保存动态分配对象的地址。
  • new int(10) 创建了一个动态分配的 int 对象,并将其初始化为 10。

在常见实现中,指针变量 p 如果是普通局部变量,通常存放在栈上;它所指向的动态分配对象则通常位于堆上。

栈区:指针变量 p

保存对象的地址

p 指向

动态分配对象:int(10)

保存整数值 10

图 2:指针变量与动态分配对象是两个不同的东西。

注意:动态分配的对象不会因为指针变量离开作用域就自动释放。使用普通裸指针管理动态内存时,需要正确释放资源,否则可能造成内存泄漏。

2.4 数据段:存放全局变量和静态变量

例如:

复制代码
int globalVar = 1;

static int staticGlobalVar = 2;

void Test()
{
    static int staticVar = 3;
}

这里的三个变量都具有静态存储期。

  • globalVar:全局变量。
  • staticGlobalVar:文件作用域的静态变量。
  • staticVar:函数内部的静态局部变量。

它们的生命周期贯穿程序的整个运行期间,但作用域和可见性并不完全相同。

需要注意: static 不一定代表变量只能在某个函数内使用。变量定义的位置不同,作用域也会不同。

2.5 代码段与只读常量

例如:

复制代码
const char* p = "abcd";

这段代码中:

  • p 是一个指针变量。
  • "abcd" 是字符串字面量。
  • p 指向字符串字面量的首字符。

在常见实现中,字符串字面量通常位于只读存储区域;但局部指针变量 p 本身并不会因为指向字符串字面量,就自动进入只读区域。

此外,const char* 表示不能通过该指针修改所指字符,并不意味着指针变量自身不能改变指向。

例如:

复制代码
const char* p = "abcd";

p = "hello";   // 可以:改变指针指向
// p[0] = 'A'; // 不可以:不能通过 p 修改字符

三、通过代码练习判断变量存储位置

综合示例:

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

对于这段代码,可以这样理解:

变量或对象 常见存储位置 说明
globalVar 数据段 全局变量
staticGlobalVar 数据段 静态全局变量
staticVar 数据段 静态局部变量
localVar 栈区 普通局部变量
num1 栈区 局部数组本身
char2 栈区 局部字符数组本身
*char2 栈区中的数组元素 第一个字符 'a'
pChar3 栈区 普通局部指针变量
*pChar3 通常在只读存储区域 指向字符串字面量的首字符
ptr1 栈区 普通局部指针变量
*ptr1 堆区 动态分配的整数空间中的第一个元素

表格中的"栈区""堆区"是常见实现下的便于理解的描述,并非 C++ 标准对所有对象物理地址的统一规定。

这一部分最重要的结论是:判断内存位置时,要区分指针变量本身与它所指向的对象。

四、C 语言的动态内存管理:malloc、calloc、realloc 和 free

C 语言提供了四个常用的动态内存管理函数。C++ 也可以继续使用它们。

4.1 malloc:申请动态内存

malloc 用于申请指定字节数的内存空间。

语法:

复制代码
void* malloc(size_t size);

示例:

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

int main()
{
    int* p = (int*)malloc(sizeof(int));

    if (p == nullptr)
    {
        cout << "内存申请失败" << endl;
        return 1;
    }

    *p = 10;
    cout << *p << endl;

    free(p);
    p = nullptr;

    return 0;
}

运行逻辑如下:

  1. sizeof(int) 计算一个 int 所占的字节数。
  2. malloc 申请对应大小的内存。
  3. 检查返回的指针是否为空。
  4. 通过指针写入并读取数据。
  5. 使用 free 释放内存。

需要注意,malloc 申请的空间不会自动将内容初始化为 0,因此不能直接假设其中已经存放了有效的整数值。

4.2 calloc:申请内存并清零

calloc 的作用是申请指定数量、指定大小的内存,并将申请到的内存字节初始化为零。

语法:

复制代码
void* calloc(size_t num, size_t size);

示例:

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

int main()
{
    int* p = (int*)calloc(4, sizeof(int));

    if (p == nullptr)
    {
        return 1;
    }

    for (int i = 0; i < 4; i++)
    {
        cout << p[i] << " ";
    }

    free(p);
    p = nullptr;

    return 0;
}

在常见平台上,这个程序会输出:

复制代码
0 0 0 0

calloc(4, sizeof(int)) 表示申请能够容纳四个 int 的空间,并将这些空间的字节清零。

对于常见的整数类型,这样得到的值为 0。但要注意,将内存字节清零并不等同于适用于所有 C++ 对象的通用初始化方式,尤其不能用它替代对象构造函数。

4.3 realloc:调整动态内存大小

当原先申请的空间不够用时,可以使用 realloc 调整空间大小。

复制代码
void* realloc(void* ptr, size_t new_size);

例如:

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

int main()
{
    int* p = (int*)malloc(4 * sizeof(int));

    if (p == nullptr)
    {
        return 1;
    }

    for (int i = 0; i < 4; i++)
    {
        p[i] = i + 1;
    }

    int* tmp = (int*)realloc(p, 10 * sizeof(int));

    if (tmp == nullptr)
    {
        // realloc 失败时,原来的 p 仍然有效
        free(p);
        return 1;
    }

    p = tmp;

    for (int i = 0; i < 4; i++)
    {
        cout << p[i] << " ";
    }

    free(p);
    p = nullptr;

    return 0;
}

这里有一个特别重要的细节:不要直接把 realloc 的返回值覆盖到原指针上。

不推荐这样写:

复制代码
p = (int*)realloc(p, 10 * sizeof(int));

如果 realloc 失败并返回空指针,原来的地址就可能丢失,导致原内存无法正常释放。

使用临时指针 tmp,能够在调整失败时保留原指针。

此外,realloc 可能在原地址调整空间,也可能分配新空间并搬移原有数据。成功后,应当使用它返回的新指针。

4.4 free:释放动态内存

free 用于释放由 C 动态内存分配函数申请的空间。

复制代码
int* p = (int*)malloc(sizeof(int));

if (p != nullptr)
{
    *p = 100;
    free(p);
    p = nullptr;
}

使用 free 后,原来指向该空间的指针不会自动变为空指针。因此,将其设置为 nullptr 是一个有用的习惯。

但是要记住:free 不会自动修改其他指向同一内存的指针。 如果还有别的指针指向已经释放的空间,就需要避免继续通过它们访问该空间。

4.5 四个函数的区别

函数 主要作用 初始化特点
malloc 申请指定字节数的内存 不初始化
calloc 申请指定数量的内存单元 将申请到的字节清零
realloc 调整已有动态内存的大小 保留原有数据中能够保留的部分
free 释放动态内存 不负责对象的析构

这四个函数的使用规则可以概括为:申请、使用、释放。 申请成功后,必须确保程序在适当的时候释放内存,并正确处理失败的情况。

五、C++ 的动态内存管理:new 和 delete

C++ 保留了 C 的动态内存管理函数,同时提供了更符合面向对象编程特点的 new 和 delete。

5.1 new:动态申请并初始化对象

先看一个简单的例子:

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

int main()
{
    int* p1 = new int;
    int* p2 = new int(10);

    cout << *p2 << endl;

    delete p1;
    delete p2;

    return 0;
}

其中:

复制代码
int* p1 = new int;

表示动态分配一个 int 对象。对于这种没有显式初始化的写法,不能假设它的值为 0。

而:

复制代码
int* p2 = new int(10);

则表示动态分配一个 int 对象,并将其初始化为 10。

这里可以看到,new 不仅涉及内存分配,还可以完成对象初始化。

5.2 new\[\]:申请连续的多个对象

如果希望申请一组连续的整数,可以使用 new[]:

复制代码
int* p = new int[5];

for (int i = 0; i < 5; i++)
{
    p[i] = i + 1;
}

for (int i = 0; i < 5; i++)
{
    cout << p[i] << " ";
}

delete[] p;
p = nullptr;

输出:

复制代码
1 2 3 4 5

注意下面两个写法的区别:

复制代码
int* p1 = new int;     // 一个 int 对象
int* p2 = new int[5];  // 五个 int 对象组成的数组

对应的释放方式也不同:

复制代码
delete p1;
delete[] p2;

申请与释放必须匹配:

  • new 对应 delete。
  • new[] 对应 delete[]。

不能把 new[] 申请的数组交给普通的 delete 释放,否则会产生未定义行为。

5.3 new 和 delete 如何处理自定义类型?

这是 C++ 内存管理中非常重要的知识点。

假设我们定义一个类:

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

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

    ~A()
    {
        cout << "~A():" << this << endl;
    }

private:
    int _a;
};

这个类包含:

  • 构造函数 A():创建对象时初始化对象。
  • 析构函数 ~A():对象生命周期结束时执行清理工作。

使用 new 和 delete:

复制代码
int main()
{
    A* p = new A(10);

    delete p;
    p = nullptr;

    return 0;
}

程序执行时,主要经历两个过程:

第一步:new A(10)

申请足够的存储空间,并在该空间中构造 A 对象,将成员变量 _a 初始化为 10。

第二步:delete p

调用对象的析构函数,执行对象的清理工作,然后释放该对象所占用的动态存储空间。

图 3:new/delete 对自定义类型的基本处理过程。

需要特别注意:析构函数负责清理对象管理的资源,但不意味着它会自动释放所有外部资源。比如类中持有的裸指针,如果其指向的资源需要释放,仍然需要编写相应的清理逻辑。

5.4 malloc/free 与自定义类型

对比下面两段代码:

复制代码
A* p1 = (A*)malloc(sizeof(A));
free(p1);

以及:

复制代码
A* p2 = new A(10);
delete p2;

第一段代码仅仅申请和释放原始存储空间,不会自动调用 A 的构造函数和析构函数。

第二段代码则会在对象创建时调用构造函数,在对象销毁时调用析构函数。

因此,对于需要执行构造和析构逻辑的 C++ 自定义类型,通常应该使用 new 和 delete,或者更推荐使用标准库的智能指针及容器管理资源。

还要注意,malloc 得到的内存并不会因为强制转换成 A* 就自动变成一个已经构造好的 A 对象。

六、operator new 与 operator delete:理解底层原理

前面我们知道,new 和 delete 可以创建和销毁动态对象。那么,它们底层是如何完成内存分配的呢?

接下来介绍了两个重要的全局函数:

  • operator new
  • operator delete

6.1 operator new 是什么?

先区分两个概念:

名称 作用
new 表达式 申请存储空间,并初始化对象
operator new 函数 负责提供所需的原始存储空间

例如:

复制代码
int* p = new int(10);

可以从概念上理解为:

  1. 调用适当的 operator new 分配存储空间。
  2. 在分配的空间中初始化 int 对象。
  3. 返回指向该对象的指针。

需要注意,这是一种帮助理解的流程描述;实际调用会受到重载、分配形式等因素影响。

6.2 operator new 与 malloc 的关系

常见实现中,operator new 会通过 malloc 申请内存。

如果申请成功,就返回相应的地址;如果申请失败,则可能按照相应的处理机制继续尝试,或者抛出 std::bad_alloc 异常。

示意代码如下:

复制代码
void* operator new(size_t size)
{
    void* p = malloc(size);

    if (p == nullptr)
    {
        throw std::bad_alloc();
    }

    return p;
}

这段代码只是用于理解原理的简化示例,并不是完整的标准库实现。实际的全局 operator new 还涉及分配失败处理机制等细节。

6.3 operator delete 与 free 的关系

同样,在常见实现中,operator delete 最终会通过 free 释放相应的存储空间。

可以用下面的示意代码理解:

复制代码
void operator delete(void* p) noexcept
{
    free(p);
}

这同样是简化的原理示例,不应直接当作完整的标准库实现。

这里最重要的结论是:

new 表达式不等于 operator new 函数,delete 表达式也不等于 operator delete 函数。

new 和 delete 还承担着对象初始化、对象销毁等工作,而底层分配函数主要负责原始存储空间的获取与释放。

七、new 和 delete 的实现原理

掌握前面的内容后,我们可以进一步理解普通类型、数组以及自定义类型的内存管理流程。

7.1 内置类型的实现原理

对于 int 等内置类型:

复制代码
int* p = new int(10);

delete p;

可以理解为:

new int(10)

申请存储空间

将对象初始化为 10

delete p

释放动态存储空间

对于内置类型,new 和 malloc、delete 和 free 在基本用途上存在相似之处,但它们并不能随意混用。

另外,分配失败时的处理方式也不同:普通的 new 通常抛出 std::bad_alloc,而 malloc 返回空指针。

7.2 自定义类型的 new 原理

对于前面定义的 A 类:

复制代码
A* p = new A(10);

其基本过程为:

  1. 调用适当的分配函数申请存储空间。
  2. 在该空间上执行构造过程。
  3. 完成 A 对象的初始化。

其中,构造函数会执行成员变量初始化以及编写在构造函数中的其他操作。

7.3 自定义类型的 delete 原理

当执行:

复制代码
delete p;

基本过程为:

  1. 调用对象的析构函数,完成资源清理。
  2. 调用相应的释放函数,释放对象占用的存储空间。

这也是为什么 new/delete 比简单的内存申请和释放更适合管理具有复杂生命周期的 C++ 对象。

7.4 new\[\] 和 delete\[\] 的原理

如果要创建多个对象:

复制代码
A* p = new A[3];

delete[] p;

创建数组时,需要为数组中的对象申请存储空间,并完成各个元素的构造。

释放数组时,需要销毁相应的元素,再释放数组所占用的存储空间。

一般情况下,数组元素的析构顺序与构造顺序相反。

可以将整个过程总结为:

new A3

申请数组所需的存储空间

构造 A0、A1、A2

delete\[\] p

销毁数组元素,再释放存储空间

图 4:自定义类型数组的创建与销毁流程。

注意:new[] 和 delete[] 是一组匹配的操作。对于需要执行析构函数的对象数组,使用正确的释放方式尤其重要。

八、定位 new 表达式:在已有内存中构造对象

它主要用于:在一块已经分配好的原始存储空间中,构造一个对象。

普通的 new 通常负责申请空间并创建对象,而定位 new 使用调用者提供的地址,不负责重新申请一块独立的存储空间。

8.1 定位 new 的基本语法

复制代码
new (place_address) type;

也可以带初始化参数:

复制代码
new (place_address) type(initializer);

其中,place_address 是提供给定位 new 使用的地址。

8.2 定位 new 示例

继续使用前面的 A 类:

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

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

    ~A()
    {
        cout << "~A()" << endl;
    }

private:
    int _a;
};

int main()
{
    // 先申请原始存储空间
    void* buffer = malloc(sizeof(A));

    if (buffer == nullptr)
    {
        return 1;
    }

    // 在已有空间中构造 A 对象
    A* p = new (buffer) A(10);

    // 显式调用析构函数
    p->~A();

    // 释放原始存储空间
    free(buffer);

    return 0;
}

理解这段代码时,要注意三个步骤:

  1. malloc 只负责申请原始存储空间。
  2. new (buffer) A(10) 在这块空间中构造对象。
  3. p->~A() 显式调用析构函数,随后使用 free 释放原始存储空间。

特别注意:定位 new 创建的对象,不能简单地使用普通的 delete p 来释放。 因为这块存储空间不是通过普通 new 表达式获得的。这里应该先结束对象的生命周期,再通过与原始内存分配方式相匹配的方式释放空间。

8.3 定位 new 有什么应用场景?

定位 new 通常与内存池配合使用。

内存池可以预先申请一大块存储空间,在需要创建对象时,再从中划分出合适的区域。

由于这些区域最初可能只是原始存储空间,因此需要通过定位 new 构造相应的 C++ 对象。

这有助于理解 C++ 中存储空间的管理和对象生命周期管理是两个相关但不同的概念。

九、malloc/free 与 new/delete 的区别总结

这是本章最值得掌握的一组对比,也是常见的 C++ 面试知识点。

对比维度 malloc/free new/delete
本质 函数 运算符及表达式
申请大小 需要指定字节数 根据类型确定
初始化 malloc 不初始化 可以在创建时初始化
返回值 void* 返回相应类型的指针
申请失败 malloc 返回空指针 普通 new 通常抛出异常
构造函数 不会自动调用 创建类对象时会调用
析构函数 不会自动调用 销毁类对象时会调用
数组操作 申请合适的总字节数 使用 new[] 和 delete[]
释放匹配 malloc 对应 free new 对应 delete

补充两点:

  • 如果使用 new (std::nothrow),分配失败时可以返回空指针,而不是抛出分配失败异常。
  • new 和 delete 的调用规则还会受到自定义分配函数、重载以及对象类型等因素影响。上表概括的是初学阶段最常见的情况。

初学者应该优先掌握什么?

建议记住以下四条:

  1. malloc 申请原始存储空间,free 释放这类空间。
  2. new 创建对象,delete 销毁对象并释放相应存储空间。
  3. 自定义类型通常需要通过构造函数初始化,通过析构函数清理资源。
  4. new[] 必须与 delete[] 匹配,不能随意混用。

在现代 C++ 项目中,还应优先考虑 std::vector、std::string、std::unique_ptr 等资源管理工具,尽量避免手动管理裸指针带来的错误。

十、常见错误与避坑指南

错误一:申请后忘记释放

复制代码
int* p = new int(10);

// 忘记 delete

如果程序反复执行类似操作,就可能发生内存泄漏。

建议: 让动态资源具有明确的所有权和生命周期,优先使用智能指针或标准容器。

错误二:重复释放同一块内存

复制代码
int* p = new int(10);

delete p;
// delete p; // 错误:重复释放

重复释放同一块已经释放的内存会产生未定义行为。

将 p 置为 nullptr 有助于避免某些误用,但不能替代对所有权的正确管理。

错误三:释放后继续访问

复制代码
int* p = new int(10);

delete p;

// cout << *p; // 错误:访问已释放的对象

释放对象后,原指针可能变成悬空指针。继续解引用会产生未定义行为。

错误四:混用申请和释放方式

复制代码
int* p = new int[10];

// delete p; // 错误
delete[] p;

数组应该使用 delete[] 释放。

类似地,使用 malloc 申请的空间应当使用相匹配的释放方式,而不是直接交给普通 delete。

错误五:认为所有指针都在堆上

复制代码
void Test()
{
    int a = 10;
    int* p = new int(20);
}

这里,a 是普通局部变量;p 是局部指针变量;new int(20) 创建的是动态分配对象。

在常见实现中,a 和 p 通常由栈承载,而动态分配对象通常位于堆上。Test() 返回后,动态对象不会因为 p 消失就自动释放,因此这段代码存在内存泄漏风险。

十一、总结:建立完整的 C++ 内存管理知识体系

通过本章学习,我们可以把 C++ 内存管理串联成一条完整的知识链。

第一步:理解内存分布

栈区、堆区、数据段、代码段

第二步:掌握 C 动态内存函数

malloc、calloc、realloc、free

第三步:掌握 C++ 动态内存操作

new、delete、new\[\]、delete\[\]

第四步:理解底层机制

operator new、operator delete

第五步:理解对象生命周期

构造、析构、定位 new、资源释放

图 5:C++ 内存管理知识体系。

对于刚开始学习 C++ 的同学来说,不需要一开始就研究复杂的内存分配器实现。先理解不同内存区域的用途,熟练掌握 malloc/free 和 new/delete 的区别,再逐步理解构造函数、析构函数和底层分配机制,就能建立扎实的知识基础。

最后送给初学者一句话:内存管理不仅是申请和释放空间,更重要的是理解对象的生命周期,并确保资源能够被正确管理。

把这句话真正理解透彻,后续学习 C++ 类与对象、智能指针、容器以及资源管理时,就会轻松很多。

十二、课后练习

练习 1:下面哪种方式适合释放 new int[10] 申请的数组?

A. free(p)

B. delete p

C. delete[] p

练习 2:使用 malloc 申请 A 类对象的空间,会自动调用 A 的构造函数吗?

A. 会

B. 不会

练习 3:int* p = new int(10); 中,指针 p 与它指向的整数对象有什么区别?

A. 指针和整数对象是同一个东西

B. 指针保存地址,整数对象保存值 10

C. 指针一定在堆上,整数对象一定在栈上


结尾

uu们,本文的内容到这里就全部结束了,道尔在这里再次感谢您的阅读!

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 道尔柯南 C/C++ & Linux 底层探索者 | 一个正在努力学习的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日目标达成!" |

结语:希望对学习C++相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

相关推荐
東隅已逝,桑榆非晚1 小时前
AVL数实现
c++·笔记·学习·算法
CoderYanger1 小时前
Java EE 进阶:5. Spring IoC&DI(第三弹):Spring DI
java·开发语言·spring boot·spring·面试·java-ee·mvc
思想觉悟1 小时前
SEAndroid实战
android·linux·c++
.YM.Z1 小时前
优选算法——【双指针算法2】C++实现
开发语言·c++·算法
思想觉悟1 小时前
Aosp开发磨刀石之ADB速查
android·c++
小此方2 小时前
C++ 工程组件与基础设施(三)详解RPC服务的概念与安装,图片介绍Brpc服务器和客户端的搭建过程
服务器·c++·rpc
qq_406981032 小时前
什么是顶层 const什么是底层const
开发语言·c++
zwc32104320552 小时前
第四周第五天——重构成类
c++·人工智能·opencv·算法·计算机视觉
凉茶钱3 小时前
多态:从虚函数语法到虚表寻址,一次讲透动态绑定
开发语言·c++·多态