重生之从0开始学习c++之内存管理

1. C/C++ 内存分布

1.1 内存分区图示

程序运行时的虚拟地址空间分层如下:

分区 存储内容 特点
栈 (Stack) 非静态局部变量、函数参数、返回地址等 向下增长,自动管理
内存映射段 动态库、共享内存 用于高效 I/O 和进程通信
堆 (Heap) 动态分配的内存 (malloc/new) 向上增长,手动管理
数据段 (Static) 已初始化的全局变量、静态变量 程序启动时分配,结束时释放
代码段 (Text) 可执行代码、只读常量(如字符串字面量) 只读,共享

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

1.2示例代码与变量位置

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);
}
cpp 复制代码
 选择题:
选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
globalVar在哪里?____   
staticGlobalVar在哪里?____
staticVar在哪里?____   
localVar在哪里?____
num1 在哪里?____
char2在哪里?____       
*char2在哪里?___
pChar3在哪里?____      
*pChar3在哪里?____
ptr1在哪里?____        
*ptr1在哪里?____

下面我们来看看这些都选什么呢?

前两个都是全局变量,所以都选C没问题吧,那么第三个是局部静态变量,所以也选C对不对,那么对于localVar,普通局部变量,函数调用时在栈上分配,所以选A

num1,局部数组,整个数组空间都在栈上,所以选A,那么前五个是不是完事了呀,让我们看看后面的。

那么char2应该在哪里呢?局部字符数组,是不是内容从常量区拷贝到栈上啊,所以选A,那么我们对于char2进行解引用,是不是就拿到数组首元素啦,数组在栈上,元素自然在栈上,所以选A,

pchar3是不是为指向数组首元素的地址呀,局部指针变量,存储地址值,所以选A,
对于pchar3解引用,很多人会在这里掉入陷阱,认为这里的*pchar3在栈上,就跟上面对char2进行解引用一样,实则不然,这里其实应该选D,为什么呢?因为这里指针是指向字符串字面量 "abcd",存储在只读常量区,我们的abcd并没有被拷贝到栈上
,所以存在代码段,那么下面两个就不用多说了吧,一个A,一个B。

综上所述:

变量 位置 原因
globalVar C. 数据段 全局变量,程序启动时分配,程序结束时释放
staticGlobalVar C. 数据段 全局静态变量,链接属性为内部,内存位置同全局变量
staticVar C. 数据段 局部静态变量,生命周期提升为全局,但作用域限于函数内
localVar A. 栈 普通局部变量,函数调用时在栈上分配
num1 A. 栈 局部数组,整个数组空间都在栈上
char2 A. 栈 局部字符数组,内容从常量区拷贝到栈上
*char2 A. 栈 数组首元素,数组在栈上,元素自然在栈上
pChar3 A. 栈 局部指针变量,存储地址值
*pChar3 D. 代码段 指针指向字符串字面量 "abcd",存储在只读常量区
ptr1 A. 栈 局部指针变量
*ptr1 B. 堆 malloc 分配的内存在堆上

2. C 语言动态内存管理 ------ malloc/calloc/realloc/free

c 复制代码
 malloc/calloc/realloc的区别?
 
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 );
}
c 复制代码
int* ptr1 = (int*)malloc(sizeof(int) * 4);
  • 功能:在堆上分配指定字节数的未初始化内存。
  • 参数:需要分配的字节数。
  • 返回值:void*,需要强制类型转换。
  • 失败:返回 NULL。
c 复制代码
malloc(16) 后:
堆:┌─────────────────┐
    │ ? │ ? │ ? │ ? │   (垃圾值)
    └─────────────────┘
    ↑
   ptr1
c 复制代码
int* ptr2 = (int*)calloc(4, sizeof(int));
  • 功能:分配 num * size 字节的内存,并初始化为 0。
  • 参数:元素个数,每个元素大小。
  • 优点:自动清零,适合数组。
c 复制代码
calloc(4, sizeof(int)) 后:
堆:┌─────────────────┐
    │ 0 │ 0 │ 0 │ 0 │
    └─────────────────┘
    ↑
   ptr2
c 复制代码
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
  • 功能:调整已分配内存块的大小。

  • 行为:

    如果原内存后有足够空间,直接扩展并返回原地址。

    否则,另找一块足够大的内存,将原数据拷贝过去,释放原内存,返回新地址。

  • 注意:ptr3 可能等于 ptr2,也可能不同。如果 realloc 失败,返回 NULL,但原内存不会被释放。

c 复制代码
free(ptr1);
free(ptr3);  // 注意:原代码中 ptr2 被 realloc 后不应再 free(ptr2),因为 realloc 可能已释放
  • 功能:释放 malloc/calloc/realloc 分配的内存。
  • 释放后:指针变成悬垂指针,建议置为 NULL。

3. C++内存管理方式

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

3.1 为什么需要 new/delete?

malloc/free 只能分配内存,不能构造对象。对于类类型,这远远不够:

cpp 复制代码
class A {
public:
    A() { cout << "构造" << endl; }  // malloc 不会调用这个!
    ~A() { cout << "析构" << endl; } // free 不会调用这个!
};

3.2 内置类型的 new/delete

cpp 复制代码
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}           // 注意:数组释放必须用 delete[]

3.3 自定义类型的 new/delete

cpp 复制代码
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
}
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;
}
cpp 复制代码
A* p1 = (A*)malloc(sizeof(A));   // 只分配内存,不调用构造
A* p2 = new A(1);                // 分配内存 + 调用构造函数

free(p1);                        // 只释放内存,不调用析构
delete p2;                       // 调用析构函数 + 释放内存

关键区别:new 将内存分配和对象构造一体化,delete 将对象析构和内存释放一体化。

cpp 复制代码
new A(1) 的执行流程:
    │
    ├─ 1. 调用 operator new(size) 申请内存
    │       └─ 内部调用 malloc
    │
    ├─ 2. 在申请的内存上调用构造函数 A::A(1)
    │       └─ 对象初始化完成
    │
    └─ 3. 返回指向该对象的指针

delete p2 的执行流程:
    │
    ├─ 1. 调用析构函数 p2->~A()
    │       └─ 对象清理资源
    │
    ├─ 2. 调用 operator delete(p2) 释放内存
    │       └─ 内部调用 free
    │
    └─ 完成

4. operator new 与 operator delete 函数(底层机制)

4.1 它们是函数,不是运算符重载

虽然名字里有 operator,但它们是全局函数,可以被用户替换(重载)。

cpp 复制代码
void* operator new(size_t size) {
    void* p = malloc(size);
    if (p == nullptr) throw std::bad_alloc();  // 失败抛异常,不返回 NULL
    return p;
}

void operator delete(void* p) noexcept {
    free(p);
}

new 表达式与 operator new 的关系:

  • new 是关键字,编译时会被展开为:先调用 operator new,再调用构造函数。
  • operator new 只负责分配内存。

为什么 new 失败抛异常而 malloc 返回 NULL?

这是 C++ 设计哲学:用异常处理错误,而不是检查返回值。这样代码更简洁,但需要配合 try-catch。

cpp 复制代码
try {
    int* p = new int[1000000000000];
} catch (const std::bad_alloc& e) {
    std::cerr << "内存不足!" << std::endl;
}

5. new 和 delete 的实现原理

对于 int、char 等,new 和 malloc 行为几乎一致(只是语法不同),因为不需要构造/析构。

new T 的原理:

  1. 调用 operator new(sizeof(T)) 分配内存。
  2. 在分配的内存上调用 T 的构造函数。

delete p 的原理:

  1. 调用 p->~T() 析构对象。
  2. 调用 operator delete§ 释放内存。

new T[N] 的原理:

  1. 调用 operator new[](N * sizeof(T) + 额外空间)。额外空间用于存储对象个数(以便 delete[]知道要调用多少次析构)。
  2. 循环调用构造函数 N 次。

delete[] p 的原理:

  1. 从额外存储中读取对象个数 N。
  2. 逆序调用析构函数 N 次。
  3. 调用 operator delete[] 释放内存。
cpp 复制代码
new A[3] 分配的内存布局:
┌─────────────────────────────────────────────┐
│ 对象个数 (size_t) │ A[0] │ A[1] │ A[2] │    │
└─────────────────────────────────────────────┘
       ↑
       └── delete[] 通过这里知道要析构 3 次
       返回给用户的指针指向 A[0] 的位置

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

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

  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在释放空间前会调用析构函数完成空间中资源的清理释放
cpp 复制代码
内存分区:栈(局部)、堆(动态)、数据段(全局/静态)、代码段(常量)
    │
    ├─ C 语言动态内存:malloc/calloc/realloc/free
    │       ├─ malloc:分配不初始化
    │       ├─ calloc:分配并清零
    │       └─ realloc:调整大小
    │
    └─ C++ 动态内存:new/delete
            ├─ new:operator new(分配内存) + 构造函数
            ├─ delete:析构函数 + operator delete(释放内存)
            ├─ new[]/delete[]:处理数组,额外存储个数
            └─ placement new:在已有内存上构造对象
相关推荐
wuyoula2 小时前
全新轻量级高性能跨平台 AI聊天+AI网关桌面
服务器·开发语言·c++·人工智能
m0_716765232 小时前
数据结构--单链表的插入、删除、查找详解
c语言·开发语言·数据结构·c++·笔记·学习·visual studio
牢姐与蒯2 小时前
c++进阶之继承
c++
_李小白2 小时前
【OSG学习笔记】Day 53: Text3D( 三维文字)
笔记·学习·3d
yu85939582 小时前
C++ 虚拟磁盘与虚拟光驱实现
开发语言·c++
Tingjct2 小时前
C++ 多态
java·开发语言·c++
CompaqCV2 小时前
OpencvSharp 算子学习教案之 - Cv2.Subtract 重载1
学习·c#·opencvsharp算子·opencv教程
lclin_20203 小时前
大恒Galaxy SDK MFC开发(1):枚举、打开、开始_停止采集基础流程(无画面版)
c++·机器视觉·工业相机·大恒相机·galaxysdk
zhangrelay3 小时前
智能时代机器人工程师・云原生 + 大模型 + 智能体 全栈成长计划(2026 版)
笔记·学习