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

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


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


目录

  • [C++ 内存管理详解:从内存分区、malloc/free 到 new/delete](#C++ 内存管理详解:从内存分区、malloc/free 到 new/delete)
    • [一、C/C++ 程序的内存区域划分](#一、C/C++ 程序的内存区域划分)
      • [1.1 为什么要先理解内存分区?](#1.1 为什么要先理解内存分区?)
      • [1.2 常见内存区域](#1.2 常见内存区域)
    • 二、栈区:局部变量主要生活的地方
      • [2.1 栈区存什么?](#2.1 栈区存什么?)
      • [2.2 栈区的特点](#2.2 栈区的特点)
    • 三、堆区:动态内存申请的地方
      • [3.1 堆区存什么?](#3.1 堆区存什么?)
      • [3.2 堆区的特点](#3.2 堆区的特点)
    • 四、数据段和代码段
      • [4.1 数据段 / 静态区](#4.1 数据段 / 静态区)
      • [4.2 代码段 / 常量区](#4.2 代码段 / 常量区)
    • 五、一道经典内存分布题
      • [5.1 全局和静态变量](#5.1 全局和静态变量)
      • [5.2 普通局部变量和数组](#5.2 普通局部变量和数组)
      • [5.3 指针变量和字符串常量](#5.3 指针变量和字符串常量)
      • [5.4 指针变量和堆空间](#5.4 指针变量和堆空间)
    • [六、C 语言动态内存管理:malloc/calloc/realloc/free](#六、C 语言动态内存管理:malloc/calloc/realloc/free)
      • [6.1 malloc](#6.1 malloc)
      • [6.2 calloc](#6.2 calloc)
      • [6.3 realloc](#6.3 realloc)
      • [6.4 realloc 的安全写法](#6.4 realloc 的安全写法)
      • [6.5 free](#6.5 free)
    • [七、malloc、calloc、realloc 的区别](#七、malloc、calloc、realloc 的区别)
    • [八、C++ 为什么还要引入 new/delete?](#八、C++ 为什么还要引入 new/delete?)
      • [8.1 malloc/free 在 C++ 中还能用吗?](#8.1 malloc/free 在 C++ 中还能用吗?)
      • [8.2 new/delete 的基本用法](#8.2 new/delete 的基本用法)
    • [九、new/delete 操作自定义类型](#九、new/delete 操作自定义类型)
      • [9.1 malloc/free 只管空间](#9.1 malloc/free 只管空间)
      • [9.2 new/delete 会调用构造和析构](#9.2 new/delete 会调用构造和析构)
      • [9.3 数组对象也一样](#9.3 数组对象也一样)
    • [十、operator new 和 operator delete 是什么?](#十、operator new 和 operator delete 是什么?)
      • [10.1 new 和 operator new 不是一回事](#10.1 new 和 operator new 不是一回事)
      • [10.2 delete 和 operator delete 也不是一回事](#10.2 delete 和 operator delete 也不是一回事)
      • [10.3 operator new 底层通常还是 malloc](#10.3 operator new 底层通常还是 malloc)
    • [十一、new/delete 的底层原理](#十一、new/delete 的底层原理)
      • [11.1 new 单个对象的过程](#11.1 new 单个对象的过程)
      • [11.2 delete 单个对象的过程](#11.2 delete 单个对象的过程)
      • [11.3 new\[\] 的过程](#11.3 new[] 的过程)
      • [11.4 delete\[\] 的过程](#11.4 delete[] 的过程)
    • [十二、定位 new:在已有空间上构造对象](#十二、定位 new:在已有空间上构造对象)
      • [12.1 什么是定位 new?](#12.1 什么是定位 new?)
      • [12.2 定位 new 的完整释放流程](#12.2 定位 new 的完整释放流程)
      • [12.3 定位 new 用在哪里?](#12.3 定位 new 用在哪里?)
    • [十三、malloc/free 和 new/delete 的区别](#十三、malloc/free 和 new/delete 的区别)
      • [13.1 共同点](#13.1 共同点)
      • [13.2 不同点总览](#13.2 不同点总览)
      • [13.3 最核心区别](#13.3 最核心区别)
    • 十四、千万不要混用申请和释放方式
      • [14.1 malloc 必须配 free](#14.1 malloc 必须配 free)
      • [14.2 new 必须配 delete](#14.2 new 必须配 delete)
      • [14.3 new\[\] 必须配 delete\[\]](#14.3 new[] 必须配 delete[])
    • 十五、常见内存错误总结
      • [15.1 内存泄漏](#15.1 内存泄漏)
      • [15.2 野指针](#15.2 野指针)
      • [15.3 重复释放](#15.3 重复释放)
      • [15.4 越界访问](#15.4 越界访问)
      • [15.5 delete 和 delete\[\] 不匹配](#15.5 delete 和 delete[] 不匹配)
    • [十六、现代 C++ 中怎么更安全地管理内存?](#十六、现代 C++ 中怎么更安全地管理内存?)
      • [16.1 尽量少手动 new/delete](#16.1 尽量少手动 new/delete)
      • [16.2 手动管理内存时的基本习惯](#16.2 手动管理内存时的基本习惯)
    • 十七、本文总结

一、C/C++ 程序的内存区域划分

1.1 为什么要先理解内存分区?

在讲 mallocnew 之前,必须先知道程序运行时内存大致分成哪些区域。

因为不同变量的生命周期、访问方式和释放方式都不一样。

比如:

cpp 复制代码
int globalVar = 1;

void Test()
{
    int localVar = 1;
    int* p = (int*)malloc(sizeof(int));
}

这里的 globalVarlocalVarp*p 并不在同一个地方。

如果不理解内存区域,后面学习指针、动态内存、对象生命周期时就很容易乱。


1.2 常见内存区域

一个 C/C++ 程序运行时,常见内存区域可以粗略分为:

这里先抓住四个最常见的区域:

  • 数据段
  • 代码段 / 常量区

二、栈区:局部变量主要生活的地方

2.1 栈区存什么?

栈区主要存放:

  • 非静态局部变量
  • 函数参数
  • 函数调用过程中的临时数据
  • 返回值相关信息

例如:

cpp 复制代码
void Test()
{
    int localVar = 1;
    int num1[10] = { 1, 2, 3, 4 };
    char char2[] = "abcd";
}

这里:

cpp 复制代码
localVar
num1
char2

这些都是函数内部的局部变量,一般位于栈区。


2.2 栈区的特点

栈区有几个特点:

第一,空间由系统自动管理。

局部变量进入作用域时创建,离开作用域时自动销毁。

cpp 复制代码
void Func()
{
    int x = 10;
}

Func() 结束时,x 的生命周期也结束。

第二,栈空间通常比较小。

所以不建议在栈上创建特别大的数组。

例如:

cpp 复制代码
int arr[100000000];

这种写法可能导致栈溢出。

第三,栈的生命周期比较短。

局部变量离开作用域后就不能再使用。

这也是为什么不能返回局部变量地址:

cpp 复制代码
int* Bad()
{
    int x = 10;
    return &x; // 错误:x 离开函数后就销毁了
}

三、堆区:动态内存申请的地方

3.1 堆区存什么?

堆区用于程序运行期间的动态内存分配。

比如:

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

这里要分清楚:

cpp 复制代码
p

是局部指针变量,通常在栈上。

cpp 复制代码
*p

也就是 p 指向的那块动态申请的空间,在堆上。

这点非常重要。

指针变量本身和指针指向的空间,不一定在同一个内存区域。


3.2 堆区的特点

堆区有几个特点:

第一,空间由程序员主动申请。

C 语言用:

cpp 复制代码
malloc
calloc
realloc

C++ 常用:

cpp 复制代码
new
new[]

第二,空间需要主动释放。

C 语言用:

cpp 复制代码
free

C++ 常用:

cpp 复制代码
delete
delete[]

第三,堆空间生命周期更灵活。

只要你不释放,它就一直存在。

但这也带来了风险:

  • 忘记释放,造成内存泄漏;
  • 重复释放,造成程序崩溃;
  • 释放后继续访问,形成野指针;
  • 申请和释放方式不匹配,导致未定义行为。

四、数据段和代码段

4.1 数据段 / 静态区

数据段通常存放:

  • 全局变量
  • 静态全局变量
  • 静态局部变量

比如:

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;

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

这里:

cpp 复制代码
globalVar
staticGlobalVar
staticVar

都属于静态存储期对象,一般放在数据段 / 静态区。

它们的生命周期不是函数调用期间,而是整个程序运行期间。

比如 staticVar 虽然写在函数内部,但它不是普通局部变量。

cpp 复制代码
void Test()
{
    static int staticVar = 1;
}

staticVar 只初始化一次,并且生命周期持续到程序结束。


4.2 代码段 / 常量区

代码段主要存放:

  • 程序可执行代码
  • 只读常量
  • 字符串字面量等

例如:

cpp 复制代码
const char* pChar3 = "abcd";

这里要分清楚:

cpp 复制代码
pChar3

是一个局部指针变量,通常在栈上。

而:

cpp 复制代码
*pChar3

指向的是字符串字面量 "abcd" 中的字符,这个字符串字面量通常位于只读常量区。

这也是为什么下面这种写法是危险的:

cpp 复制代码
char* p = "abcd";
p[0] = 'x'; // 错误,不应该修改字符串常量

更推荐写成:

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

const 明确告诉自己和编译器:这块内容不应该被修改。


五、一道经典内存分布题

看下面代码:

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

逐个分析。


5.1 全局和静态变量

cpp 复制代码
globalVar
staticGlobalVar
staticVar

它们都位于数据段 / 静态区。

原因:

  • globalVar 是全局变量;
  • staticGlobalVar 是静态全局变量;
  • staticVar 是静态局部变量。

虽然 staticVar 写在函数里面,但它的生命周期不是函数调用期间,而是整个程序运行期间。


5.2 普通局部变量和数组

cpp 复制代码
localVar
num1
char2

它们是普通局部变量,通常位于栈区。

需要注意的是:

cpp 复制代码
char char2[] = "abcd";

这里 char2 是一个数组。

数组本身在栈上,里面存放了字符:

cpp 复制代码
'a' 'b' 'c' 'd' '\0'

所以:

cpp 复制代码
char2

在栈上。

cpp 复制代码
*char2

访问的是数组第一个元素,也在栈上。


5.3 指针变量和字符串常量

cpp 复制代码
const char* pChar3 = "abcd";

这里分两部分看:

cpp 复制代码
pChar3

是局部指针变量,通常在栈上。

cpp 复制代码
*pChar3

访问的是字符串字面量 "abcd" 的第一个字符,通常在代码段 / 常量区。

所以不要把指针变量和它指向的内容混为一谈。


5.4 指针变量和堆空间

cpp 复制代码
int* ptr1 = (int*)malloc(sizeof(int) * 4);

这里同样分两部分看:

cpp 复制代码
ptr1

是局部指针变量,通常在栈上。

cpp 复制代码
*ptr1

访问的是 malloc 申请出来的堆空间。

所以:

  • ptr1 在栈上;
  • *ptr1 在堆上。

理解这一点,是学动态内存的关键。


六、C 语言动态内存管理:malloc/calloc/realloc/free

6.1 malloc

malloc 用于从堆上申请一块指定字节数的空间。

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

它的特点是:

  • 参数是字节数;
  • 返回值是 void*
  • 使用时通常需要强制类型转换;
  • 申请失败返回 NULL
  • 不会初始化申请到的空间。

所以申请后通常要判空:

cpp 复制代码
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
{
    perror("malloc fail");
    return;
}

6.2 calloc

calloc 也用于动态申请空间。

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

它和 malloc 的主要区别是:

calloc 会把申请到的空间初始化为 0。

参数也不同:

cpp 复制代码
calloc(元素个数, 每个元素大小)

malloc 是:

cpp 复制代码
malloc(总字节数)

所以:

cpp 复制代码
calloc(10, sizeof(int))

表示申请 10 个 int 大小的空间,并把这些字节清零。


6.3 realloc

realloc 用于调整已经申请过的动态内存大小。

cpp 复制代码
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);

这里表示:

把原来 p2 指向的空间调整成可以存放 10 个 int 的空间。

realloc 有两种情况。

第一种,原地扩容成功。

原来的地址不变。

第二种,原地空间不够。

系统会重新找一块更大的空间,把旧数据拷贝过去,再释放旧空间。

所以 realloc 的返回值一定要接住。


6.4 realloc 的安全写法

很多人会直接写:

cpp 复制代码
p = (int*)realloc(p, newSize);

这有风险。

如果 realloc 失败,会返回 NULL

这时原来的空间并不会自动释放,但 p 已经被改成了 NULL,原地址丢失,造成内存泄漏。

更安全的写法是:

cpp 复制代码
int* tmp = (int*)realloc(p, newSize);
if (tmp == NULL)
{
    perror("realloc fail");
    // p 仍然有效,可以继续使用或之后释放
}
else
{
    p = tmp;
}

这段代码很值得记住。


6.5 free

free 用于释放 malloccallocrealloc 申请的空间。

cpp 复制代码
free(p);
p = NULL;

释放后最好把指针置空。

原因是:

cpp 复制代码
free(p);

只是释放了 p 指向的空间,并不会自动把 p 变成空指针。

如果后面继续使用 p,就会形成野指针。

所以推荐写:

cpp 复制代码
free(p);
p = NULL;

七、malloc、calloc、realloc 的区别

可以用一张表总结:

函数 作用 是否初始化 参数特点
malloc 申请指定字节数空间 不初始化 malloc(size)
calloc 申请多个元素空间 初始化为 0 calloc(count, size)
realloc 调整已有空间大小 新增空间不一定初始化 realloc(ptr, newSize)
free 释放动态空间 不涉及 free(ptr)

简单记:

  • malloc:只申请,不清空;
  • calloc:申请并清零;
  • realloc:调整已有空间;
  • free:释放空间。

八、C++ 为什么还要引入 new/delete?

8.1 malloc/free 在 C++ 中还能用吗?

能用。

C++ 兼容 C 的很多用法,所以 malloc/free 在 C++ 中仍然可以使用。

但是 C++ 有类和对象。

对于自定义类型来说,光申请一块空间还不够。

还要调用构造函数完成对象初始化。

同样,释放对象前也要调用析构函数清理资源。

malloc/free 不会自动调用构造函数和析构函数。

这就是 C++ 引入 new/delete 的重要原因。


8.2 new/delete 的基本用法

申请单个对象:

cpp 复制代码
int* p1 = new int;

申请单个对象并初始化:

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

申请数组:

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

释放时要匹配:

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

注意:

申请单个对象,用:

cpp 复制代码
new
delete

申请连续数组,用:

cpp 复制代码
new[]
delete[]

二者必须匹配使用。


九、new/delete 操作自定义类型

9.1 malloc/free 只管空间

先定义一个类:

cpp 复制代码
class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }

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

private:
    int _a;
};

如果写:

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

这只发生了两件事:

  1. 申请一块和 A 对象一样大的空间;
  2. 释放这块空间。

但是:

  • 构造函数不会调用;
  • 析构函数不会调用。

严格来说,p1 指向的只是"一块原始内存",还不能算一个真正完成构造的 A 对象。


9.2 new/delete 会调用构造和析构

如果写:

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

过程就不同了。

new A(1) 会做两件事:

  1. 申请空间;
  2. 在这块空间上调用构造函数,构造对象。

delete p2 也会做两件事:

  1. 调用析构函数,清理对象内部资源;
  2. 释放对象占用的空间。

这就是 new/deletemalloc/free 在自定义类型上的最大区别。


9.3 数组对象也一样

对于数组:

cpp 复制代码
A* p3 = (A*)malloc(sizeof(A) * 10);
free(p3);

不会调用 10 次构造函数和析构函数。

而:

cpp 复制代码
A* p4 = new A[10];
delete[] p4;

会:

  • 创建时调用 10 次构造函数;
  • 释放时调用 10 次析构函数。

所以管理自定义类型对象时,应该优先使用 new/delete,而不是 malloc/free

不过在现代 C++ 中,更进一步的建议是:

能不用手动 new/delete,就尽量使用标准库容器和智能指针管理资源。

例如:

cpp 复制代码
std::vector<int> v;
std::unique_ptr<A> p;

这类工具能减少内存泄漏和手动释放错误。


十、operator new 和 operator delete 是什么?

10.1 new 和 operator new 不是一回事

很多初学者会把 newoperator new 混在一起。

它们不是一个东西。

new 是操作符,也叫 new 表达式。

例如:

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

它会完成完整的对象创建过程:

  1. 调用 operator new 申请空间;
  2. 调用构造函数初始化对象。

operator new 是一个函数。

它只负责申请原始内存,不负责调用构造函数。

可以粗略理解为:

cpp 复制代码
new = operator new + 构造函数

10.2 delete 和 operator delete 也不是一回事

同理:

cpp 复制代码
delete p;

是一个完整的删除表达式。

它会做两件事:

  1. 调用析构函数;
  2. 调用 operator delete 释放空间。

operator delete 只是释放原始空间的函数,不负责调用析构函数。

可以粗略理解为:

cpp 复制代码
delete = 析构函数 + operator delete

10.3 operator new 底层通常还是 malloc

在很多实现中,operator new 底层也会调用类似 malloc 的机制申请空间。

但二者的失败处理方式不同。

malloc 失败通常返回 NULL

普通 new 失败通常抛出 std::bad_alloc 异常。

这也是为什么下面两种代码处理方式不同:

cpp 复制代码
int* p = (int*)malloc(sizeof(int));
if (p == NULL)
{
    // 申请失败
}

而:

cpp 复制代码
try
{
    int* p = new int;
}
catch (const std::bad_alloc& e)
{
    // 申请失败
}

当然,也可以使用 nothrow newnew 失败时返回空指针,但入门阶段先掌握普通 new 的异常机制即可。


十一、new/delete 的底层原理

11.1 new 单个对象的过程

对于:

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

底层可以理解成:

  1. 调用 operator new(sizeof(A)) 申请空间;
  2. 在申请到的空间上调用 A 的构造函数;
  3. 返回对象指针。

所以 new 不只是申请内存,它还负责构造对象。


11.2 delete 单个对象的过程

对于:

cpp 复制代码
delete p;

底层可以理解成:

  1. 调用 A 的析构函数;
  2. 调用 operator delete(p) 释放空间。

所以 delete 不只是释放内存,它还负责销毁对象。


11.3 new\[\] 的过程

对于:

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

底层可以理解成:

  1. 调用 operator new[] 申请足够容纳 10 个对象的空间;
  2. 在这块空间上依次调用 10 次构造函数。

也就是说,new[] 要构造多个对象。


11.4 delete\[\] 的过程

对于:

cpp 复制代码
delete[] p;

底层可以理解成:

  1. 对数组中的每个对象调用析构函数;
  2. 调用 operator delete[] 释放整块空间。

所以 new[] 必须配 delete[]

如果用错:

cpp 复制代码
A* p = new A[10];
delete p; // 错误

可能只析构部分对象,甚至导致更严重的问题。


十二、定位 new:在已有空间上构造对象

12.1 什么是定位 new?

定位 new,也叫 placement new。

它的作用是:

在一块已经分配好的原始内存上,显式调用构造函数创建对象。

语法:

cpp 复制代码
new(place_address) Type;
new(place_address) Type(initializer-list);

比如:

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

第一句:

cpp 复制代码
malloc(sizeof(A))

只是申请了一块原始空间。

第二句:

cpp 复制代码
new(p) A(10);

才是在这块空间上调用构造函数,把它变成一个真正的 A 对象。


12.2 定位 new 的完整释放流程

使用定位 new 后,释放时也要分两步。

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

new(p) A(10);  // 在已有空间上构造对象

p->~A();       // 手动调用析构函数
free(p);       // 释放原始空间

注意:

定位 new 不负责申请空间。

所以也不能直接用普通 delete 去释放。

因为空间是 malloc 来的,就应该用 free 释放。

但在 free 前,必须手动调用析构函数。


12.3 定位 new 用在哪里?

入门阶段只需要知道它的用途即可。

定位 new 常见于:

  • 内存池;
  • 对象池;
  • 自定义容器;
  • 高性能场景下的对象构造控制。

比如内存池提前申请一大块原始内存。

当需要创建对象时,不再频繁向系统申请小块内存,而是在已有内存上用定位 new 构造对象。

这可以减少频繁分配释放的开销。


十三、malloc/free 和 new/delete 的区别

13.1 共同点

它们的共同点是:

都可以从堆上申请空间,并且都需要程序员释放。

例如:

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

int* p2 = new int;
delete p2;

这两组都在动态管理堆空间。


13.2 不同点总览

对比项 malloc/free new/delete
本质 库函数 C++ 操作符
申请大小 需要手动计算字节数 根据类型自动计算
返回类型 void*,通常要强转 返回对应类型指针
初始化 不会自动初始化 可以初始化
失败处理 返回 NULL 默认抛异常
自定义类型 不调用构造/析构 调用构造/析构
数组申请 手动计算总大小 使用 new[]
释放方式 free delete/delete[]

13.3 最核心区别

对于内置类型:

cpp 复制代码
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;

二者差别相对小一些。

但对于自定义类型:

cpp 复制代码
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A;

差别就很大。

malloc 只申请空间。

new 申请空间并调用构造函数。

对应释放时:

free 只释放空间。

delete 先调用析构函数,再释放空间。

这就是核心区别。


十四、千万不要混用申请和释放方式

14.1 malloc 必须配 free

正确:

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

错误:

cpp 复制代码
int* p = (int*)malloc(sizeof(int));
delete p; // 错误

14.2 new 必须配 delete

正确:

cpp 复制代码
int* p = new int;
delete p;

错误:

cpp 复制代码
int* p = new int;
free(p); // 错误

14.3 new\[\] 必须配 delete\[\]

正确:

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

错误:

cpp 复制代码
int* p = new int[10];
delete p; // 错误

可以记一句话:

谁申请,谁释放;怎么申请,就怎么释放。


十五、常见内存错误总结

15.1 内存泄漏

申请了空间,但忘记释放。

cpp 复制代码
void Func()
{
    int* p = new int[10];
    // 忘记 delete[]
}

函数结束后,p 这个局部指针变量销毁了。

但它指向的堆空间还在。

这块空间再也找不到了,就形成内存泄漏。


15.2 野指针

释放空间后继续使用。

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

cout << *p << endl; // 错误

delete p 后,p 指向的空间已经无效。

p 自己的值还在。

这时继续使用 p,就是野指针访问。

建议释放后置空:

cpp 复制代码
delete p;
p = nullptr;

15.3 重复释放

同一块空间释放两次。

cpp 复制代码
int* p = new int;
delete p;
delete p; // 错误

第一次 delete 后,空间已经被释放。

第二次继续释放,行为未定义。

释放后置空可以降低风险:

cpp 复制代码
delete p;
p = nullptr;

delete p; // 对 nullptr 执行 delete 是安全的

15.4 越界访问

申请 10 个元素,却访问第 10 个下标。

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

p[10] = 1; // 错误,合法下标是 0~9

delete[] p;

越界访问可能不会立刻报错,但会破坏其他内存,属于非常危险的问题。


15.5 delete 和 delete\[\] 不匹配

cpp 复制代码
A* p = new A[10];
delete p; // 错误

对于自定义类型数组,这种错误尤其严重。

因为 delete[] 需要正确调用多个对象的析构函数。

如果写错,可能导致析构不完整、内存破坏或程序崩溃。


十六、现代 C++ 中怎么更安全地管理内存?

16.1 尽量少手动 new/delete

虽然本文重点讲 new/delete,但在真实工程中,现代 C++ 更推荐使用 RAII 思想管理资源。

简单说:

让对象在构造时获取资源,在析构时释放资源。

比如:

cpp 复制代码
std::vector<int> v;

vector 会自己管理动态数组空间。

你不需要手动 new[]delete[]

再比如:

cpp 复制代码
std::unique_ptr<A> p = std::make_unique<A>();

unique_ptr 会在生命周期结束时自动释放对象。

这能减少很多内存泄漏和异常路径下忘记释放的问题。


16.2 手动管理内存时的基本习惯

如果必须手动管理内存,建议遵守下面几条:

第一,申请后立刻判断是否成功。

对于 malloc

cpp 复制代码
int* p = (int*)malloc(sizeof(int) * n);
if (p == NULL)
{
    return;
}

第二,释放后立刻置空。

cpp 复制代码
free(p);
p = NULL;

或:

cpp 复制代码
delete p;
p = nullptr;

第三,申请和释放方式必须匹配。

cpp 复制代码
malloc  -> free
new     -> delete
new[]   -> delete[]

第四,自定义类型优先用 new/delete,不要用 malloc/free 直接处理对象。

第五,能用标准库容器,就不要自己管理动态数组。


十七、本文总结

本文主要梳理了 C++ 内存管理的核心内容。

第一,C/C++ 程序内存大致可以分为栈、堆、数据段、代码段和内存映射段。

第二,普通局部变量通常在栈上,全局变量和静态变量通常在数据段,动态申请的空间在堆上,字符串常量通常在只读常量区。

第三,指针变量本身和它指向的空间可能位于不同区域。比如局部指针变量在栈上,但它指向的动态空间在堆上。

第四,malloc/calloc/realloc/free 是 C 语言动态内存管理方式。

第五,malloc 只申请空间,不初始化;calloc 会清零;realloc 用于调整已有空间大小;free 用于释放空间。

第六,C++ 中 new/delete 不仅管理空间,还会处理对象构造和析构。

第七,newoperator new 不是一回事。operator new 只负责申请原始空间,new 表达式还会调用构造函数。

第八,deleteoperator delete 也不是一回事。delete 表达式会先调用析构函数,再释放空间。

第九,定位 new 用于在已经分配好的原始内存上显式构造对象,常见于内存池等场景。

第十,malloc/freenew/deletenew[]/delete[] 必须匹配使用,不能混用。


相关推荐
Mortalbreeze1 小时前
C++11 ---- 引用折叠、完美转发、可变模板参数、emplace系列接口
开发语言·c++
object not found1 小时前
Node.js fs 常用 API 整理:node:fs/promises、node:fs、fs 到底怎么用
开发语言·前端·javascript
C+++Python1 小时前
C++ 常量全面讲解
java·开发语言·c++
江屿风1 小时前
C++图论基础拓扑排序经典OJ题流食般投喂
开发语言·c++·笔记·算法·图论
芯岭技术郦1 小时前
MS32C001‑C:极致成本 32 位 MCU
c语言·开发语言·单片机
C+-C资深大佬1 小时前
C++ 数字与字符串互转
java·c++·算法
nexustech1 小时前
simplejson:Python JSON 处理的备用引擎
开发语言·python·其他·json
知识分享小能手1 小时前
Hadoop学习教程,从入门到精通, Hadoop 3.x 高可用集群 — 知识点详解(6)
大数据·hadoop·学习
雷工笔记1 小时前
MES系列48-MES 系统「质量管理」完整设计与实施方案
开发语言·javascript·ecmascript