首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《C++初阶之内存管理》【内存分布 + operator new/delete + 定位new】

《C++初阶之内存管理》【内存分布 + operator new/delete + 定位new】

作者头像
序属秋秋秋
发布2025-12-18 15:52:50
发布2025-12-18 15:52:50
1570
举报

往期《C++初阶》回顾: /------------ 入门基础 ------------/ 【C++的前世今生】 【命名空间 + 输入&输出 + 缺省参数 + 函数重载】 【普通引用 + 常量引用 + 内联函数 + nullptr】 /------------ 类和对象 ------------/ 【类 + 类域 + 访问限定符 + 对象的大小 + this指针】 【类的六大默认成员函数】 【初始化列表 + 自定义类型转换 + static成员】 【友元 + 内部类 + 匿名对象】 【经典案例:日期类】

前言:

🌈hi~ 小伙伴们你们好呀!(。・ω・。)ノ♡ 博主回家休整了几天,充完电满血复活啦~ 💪 今天为大家带来假期里的第一篇重磅博客【内存分布 + operator new/delete + 定位new】! 这可是C++内存管理的核心内容,非常重要哦!✨

这篇博客聚焦于 C++ 的内存管理相关内容,要知道, 💾内存管理可是 C++ 学习中相当核心且关键的部分。🔑 它直接关系到程序的运行效率、稳定性以及资源利用的合理性,无论是日常的程序开发,还是应对更复杂的项目,扎实掌握这部分知识都必不可少。 所以,大家一定要耐心看完呦☕ ( ̄ω ̄;),相信你看完后会对 C++ 的内存机制有更清晰的认识~(•̀ᴗ•́)و

---------------C/C++的内存分布---------------

C/C++的内存是怎么划分的?

内存是计算机中极为关键的资源,如同城市的土地,合理规划才能高效利用。 在 C++ 编程世界里,为实现对这一资源的高效利用,计算机将内存进行了细致的划分,主要分为以下几个区域:


栈区(Stack Segment): 栈区如同程序员的「临时工作台」,由编译器自动搭建与清理。

  • 函数调用时,局部变量(如:int num;)、函数参数返回地址以及临时计算结果都会被存储于此。
  • 它遵循后进先出(LIFO)原则,就像:堆叠盘子般,最后放入的盘子最先取出。
  • 内存地址从高向低连续增长,因底层有专用寄存器(例如:ESP、EBP)快速定位,压栈与出栈操作仅需简单指令,执行效率极高。
  • 但它空间有限,Windows 默认约 1MB,Linux 通常为 8 - 10MB,递归过深或大量局部变量易引发栈溢出,导致程序崩溃。

堆区(Heap Segment): 堆区是程序的「动态仓库」,灵活应对不确定生命周期的数据存储。

  • 用于存放new(C++)或malloc(C语言)动态分配的内存,例如:int* ptr = new int[10];创建的数组。
  • 堆区相比栈区内存更大,但分配过程复杂,需调用内存分配函数搜索可用空间,若不足还需请求系统扩展。
  • 频繁分配与释放易产生内存碎片,降低使用效率。且堆内存需手动通过deletefree释放,否则会造成内存泄漏,唯有程序结束时,操作系统才强制回收。

数据段(静态区,Data Segment): 数据段是存储「长效数据的家园」,分为 初始化数据段BSS 段(也可叫做:未初始化数据段

  • 全局变量(int global = 1;)、静态全局变量(static int s_global = 2;)以及函数内的静态局部变量(void func() { static int s_local = 3; })均存放于此。
  • 初始化数据段保存已赋值的变量BSS 段存储未初始化变量,程序启动时自动清零。
  • 这些变量自程序启动分配内存,直至结束才释放,生命周期贯穿全程。

代码段(常量区,Code Segment/Text Segment): 代码段是程序的「核心指令库」与「常量博物馆」。

  • 字符串字面量(如:“Hello, World!”)、用const修饰的全局常量(const int PI = 3.14;),部分编译器处理也存放其中。
  • 编译后的机器指令在此有序排列,确保程序按预定逻辑执行。
  • 此区域具备 只读属性,防止程序运行时意外篡改指令常量,保障执行稳定性与安全性,其内存自程序启动时固定分配,伴随程序始终 。

C/C++的内存分布是怎么样的?

注意:其实C++的内存分布和C语言的内存分布是一样的。 所以:下面我们先来看一段C语言的代码,通过C语言的代码来了解一下其内存是怎么进行分布的,进而来学习一下C++的内存是怎么分布的?

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

// “全局变量” - 存储在静态区/全局区(数据段)
// 生命周期:整个程序运行期间
// 作用域:整个程序可见
int globalvar = 1;

// “静态全局变量” - 存储在静态区/全局区(数据段)
// 生命周期:整个程序运行期间
// 作用域:仅在当前文件可见(限制了外部链接性)
static int staticGlobalvar = 1;

void Test()
{
    // “静态局部变量” - 存储在静态区/全局区(数据段)
    // 生命周期:整个程序运行期间
    // 作用域:仅在Test函数内可见
    static int staticVar = 1;

    // “局部变量”(自动变量) - 存储在(栈区)
    // 生命周期:函数调用期间
    // 作用域:当前代码块内
    int localVar = 1;

    // “局部数组” - 存储在(栈区)
    // 生命周期:函数调用期间
    // 作用域:当前代码块内
    // 初始化:前4个元素显式初始化,剩余6个元素默认初始化为0
    int num1[10] = { 1, 2, 3, 4 };

    // “局部字符数组” - 存储在(栈区)
    // 生命周期:函数调用期间
    // 作用域:当前代码块内
    // 初始化:从字符串常量"abcd"拷贝内容(包括结尾的'\0')
    char char2[] = "abcd";

    // “指向字符串常量的指针” - 字符串"abcd"存储在常量区(代码段)
    // pChar3本身是“局部指针变量”,存储在(栈区)
    // 生命周期:函数调用期间
    // 作用域:当前代码块内
    const char* pChar3 = "abcd";

    // “动态内存分配” - 存储在(堆区)
    // ptr1是“局部指针变量”,存储在(栈区)
    // 分配:16字节空间(4个int)
    // 注意:分配的内存未初始化,内容不确定
    int* ptr1 = (int*)malloc(sizeof(int) * 4);

    // 动态内存分配 - 存储在(堆区)
    // ptr2是“局部指针变量”,存储在(栈区)
    // 分配:16字节空间(4个int)
    // 注意:calloc会将分配的内存初始化为0
    int* ptr2 = (int*)calloc(4, sizeof(int));

    // 重新分配内存 - 可能仍在堆区原位置,也可能移动到新位置
    // ptr3是“局部指针变量”,存储在(栈区)
    // 重新分配:16字节空间(4个int)
    // 注意:原ptr2指向的内存会被释放,ptr3可能指向新地址
    //       如果realloc失败返回NULL,但原ptr2指向的内存仍有效
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);

    // 释放堆内存
    // 释放ptr1指向的16字节空间
    free(ptr1);
    ptr1 = NULL;

    // 释放堆内存
    // 释放ptr3指向的16字节空间
    // 注意:因为ptr3可能是realloc后的新指针,所以不需要再释放ptr2
    free(ptr3);
    ptr3 = NULL;

}

int main()
{
    Test();

    return 0;
}
在这里插入图片描述
在这里插入图片描述

请你仔细阅读上面的代码完成下面的填空题:(答案在这些问题的下面,请勿提前借鉴) 选项A.栈 B.堆 C.数据段 (静态区) D.代码段 (常量区)

  1. globalvar 在哪里?
  2. staticGlobalvar 在哪里?
  3. staticVar 在哪里?
  4. localvar 在哪里?
  5. num1 在哪里?

  1. char2 在哪里?
  2. *char2 在哪里?
  3. pChar3 在哪里?
  4. *pChar3 在哪里?
  5. ptr1 在哪里?
  6. *ptr1 在哪里?

上阙答案:【C C C A A

下阙答案:【A A A D A B

下面的内容是解析,如果上面的问题你都答对了话,我也建议你看一下: 1. globalvar 在哪里? 答案:C. 数据段 (静态区) 解释

  • globalvar全局变量,定义在所有函数外部。
  • 全局变量存储在数据段(静态区),生命周期为整个程序运行期间,作用域为整个程序。

2. staticGlobalvar 在哪里? 答案:C. 数据段 (静态区) 解释

  • staticGlobalvar静态全局变量,使用 static 修饰。
  • 静态全局变量同样存储在数据段(静态区),但作用域被限制在定义它的文件内(外部不可见)

3. staticVar 在哪里? 答案:C. 数据段 (静态区) 解释

  • staticVar静态局部变量,在函数内部定义但使用 static 修饰。
  • 静态局部变量存储在数据段(静态区),生命周期为整个程序运行期间,但作用域仅限于定义它的函数内部。

4. localvar 在哪里? 答案:A. 栈 解释

  • localvar普通局部变量,在函数内部定义且未使用 static 修饰。
  • 局部变量存储在栈区,生命周期为函数调用期间,函数返回后自动销毁。

5. num1 在哪里? 答案:A. 栈 解释

  • num1局部数组,在函数内部定义。
  • 数组本身是局部变量,存储在栈区,其内容(10 个 int)也在栈上连续分配。

6. char2 在哪里? 答案:A. 栈 解释

  • char2局部字符数组,在函数内部定义。
  • 数组存储在栈区,内容是字符串 "abcd" 的副本(包含末尾的 '\0'

7. *char2 在哪里? 答案:A. 栈 解释

  • *char2 表示数组的第一个元素(即:字符 'a'
  • 由于数组 char2 存储在栈区,其元素也在栈区。

8. pChar3 在哪里? 答案:A. 栈 解释

  • pChar3局部指针变量,在函数内部定义。
  • 指针本身存储在栈区,但其指向的字符串常量 "abcd" 存储在代码段(常量区)

9. *pChar3 在哪里? 答案:D. 代码段 (常量区) 解释

  • *pChar3 表示指针指向的第一个字符(即:'a'
  • 由于 pChar3 指向字符串常量 "abcd",而字符串常量存储在代码段(常量区),因此 *pChar3 也在常量区。

10. ptr1 在哪里? 答案:A. 栈 解释

  • ptr1局部指针变量,在函数内部定义。
  • 指针本身存储在栈区,但其指向的内存(通过 malloc 分配)在堆区。

11. *ptr1 在哪里? 答案:B. 堆 解释

  • *ptr1 表示指针指向的第一个 int 值
  • 由于 ptr1 指向通过 malloc 分配的堆内存,因此 *ptr1 在堆区。
代码语言:javascript
复制
高地址
┌─────────────┐
│   栈区       │← localvar, num1, char2, pChar3, ptr1,ptr2,ptr3
├─────────────┤
│   堆区       │← *ptr1(malloc分配的4个int), *ptr2(calloc分配的4个int) , *ptr3(realloc后的内存块)
├─────────────┤
│   数据段     │← globalvar, staticGlobalvar, staticVar
├─────────────┤
│   代码段     │← *pChar3指向的字符串常量
└─────────────┘
低地址
在这里插入图片描述
在这里插入图片描述

C语言的动态内存管理方式是什么?

在 C 语言中,动态内存管理是通过一组标准库函数实现的,主要用于在程序运行时 动态分配使用释放内存空间 与编译时自动分配的栈内存不同,动态内存分配更灵活,适用于无法在编译期确定内存大小的场景(例如:动态数组、链表等数据结构) C 语言提供了四个关键函数用于动态内存管理:malloccallocreallocfree

1. malloc申请未初始化的内存块

功能:分配指定字节数的内存空间,返回指向该内存起始地址的 void* 指针(需强制类型转换为目标类型)

语法

代码语言:javascript
复制
void* malloc(size_t size); // size 为所需内存的字节数

特点

  • 分配的内存未初始化,内容为随机值(垃圾值)
  • 若分配失败(如:内存不足),返回 NULL,需检查返回值避免野指针

示例

代码语言:javascript
复制
int* arr = (int*)malloc(5 * sizeof(int)); // 分配 5 个 int 类型的空间(20 字节)
if (arr == NULL) 
{
    perror("malloc failed"); // 处理分配失败
    exit(EXIT_FAILURE);
}

2. calloc申请初始化的内存块

功能:分配指定数量和单个元素大小的内存块,并将内存初始化为 0

语法

代码语言:javascript
复制
void* calloc(size_t num_elements, size_t element_size); // num_elements 为元素个数,element_size 为单个元素大小

特点

  • 自动计算总字节数num_elements × element_size),避免 malloc 中手动计算的误差
  • 内存块被清零,适合需要初始化的场景(如:数组、结构体)

示例

代码语言:javascript
复制
int* arr = (int*)calloc(5, sizeof(int)); // 分配 5 个 int 空间,初始化为 0

3. realloc调整已分配内存的大小

功能:修改一个原先由 malloccalloc 分配的内存块的大小,可扩大或缩小。

语法

代码语言:javascript
复制
void* realloc(void* ptr, size_t new_size); // ptr 为旧内存指针,new_size 为新内存大小

特点

  • ptrNULL —> 等价于 malloc(new_size)
  • ptr 不为 NULLnew_size0 —> 等价于 free(ptr)
  • 调整内存时,原有数据会被保留,但新扩展的内存未初始化(若扩大)
  • 可能因原内存块附近空间不足,重新分配一块新内存并复制数据,原指针可能失效,需用新返回值更新指针。

示例

代码语言:javascript
复制
int* arr = (int*)malloc(5 * sizeof(int));   // 初始分配 5 个元素
  
arr = (int*)realloc(arr, 10 * sizeof(int)); // 扩容至 10 个元素
if (arr == NULL) { /* 处理失败 */ }

4. free释放动态分配的内存

功能:释放由 malloccallocrealloc 分配的内存空间,归还系统。

语法

代码语言:javascript
复制
void free(void* ptr); // ptr 为待释放的内存指针

注意事项

  • 只能释放动态分配的内存,不可释放栈内存(如:局部变量地址)或未分配的指针
  • 释放后,指针应手动置为 NULL,避免成为野指针(指向无效内存的指针)
  • 多次释放同一指针会导致未定义行为(程序崩溃或数据损坏)

示例

代码语言:javascript
复制
int* arr = (int*)malloc(5 * sizeof(int));

// 使用内存...

free(arr); // 释放内存
arr = NULL; // 置空指针,防止野指针
关于C语言内存管理的面试题,小伙子你敢试试吗?
代码语言:javascript
复制
void Test()
{
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	
	// 这里需要free(p2)吗? 如果不需要释放请问这是为什么?
	free(p3);
}

在上面的这段代码中,不需要手动调用 free(p2),原因如下:

realloc 的行为

  • realloc 成功扩展内存时,它会返回一个新的指针(可能与原指针相同,也可能不同)
  • 如果内存需要搬迁(原空间附近无法扩展),realloc会:
    1. 分配一块新的足够大的内存区域(本例中为 10 * sizeof(int)
    2. 自动将原内存(p2 指向的内容)复制到新内存
    3. 释放原内存(p2 指向的空间)
    4. 返回新内存的地址(赋给 p3
  • 因此一旦 realloc 成功返回,原指针 p2 就已经失效,其指向的内存已被自动释放。

双重释放风险

  • 如果手动调用 free(p2),会导致双重释放(先被 realloc 释放,后被手动释放),这是未定义行为,可能导致程序崩溃。

正确的写法

代码语言:javascript
复制
void Test()
{
    int* p2 = (int*)calloc(4, sizeof(int));
    int* p3 = (int*)realloc(p2, sizeof(int) * 10);
    
    if (p3 == NULL) 
    {
        // 处理分配失败的情况(此时 p2 仍有效,需释放)
        free(p2);
        return;
    }
    // p2 已被 realloc 自动释放,无需再 free(p2)
    free(p3); // 只需释放新指针
}

C++的动态内存管理方式是什么?

C++ 的动态内存管理是通过 newdelete 操作符实现的,用于在程序运行时动态 申请释放 内存。 动态内存分配的特点

  • 在堆区分配内存,通过 new 申请,delete 释放。
  • 适用于以下场景:
    • 无法在编译时确定内存大小(如:根据用户输入创建数组)
    • 需要长期保存数据(如:跨函数传递的对象)
    • 实现动态数据结构(如:链表、树)
怎么使用new和delete管理内置类型的变量?

new:申请动态内存

基本语法

代码语言:javascript
复制
// 分配单个对象
数据类型* 指针变量 = new 数据类型;       // 分配但未初始化
数据类型* 指针变量 = new 数据类型(值);   // 分配并初始化(任意C++版本均支持对基础类型的初始化)  

// 分配数组
数据类型* 指针变量 = new 数据类型[元素个数];  // 分配数组(未初始化)
数据类型* 指针变量 = new 数据类型[元素个数]{值1, 值2, ...};  //分配数组并初始化(C++11起支持列表初始化,C++98需手动赋值)  

示例

代码语言:javascript
复制
int* p1 = new int;        // 分配一个 int 类型的内存,未初始化,元素为随机值  
int* p2 = new int(10);    // 分配一个 int 类型的内存,并初始化为 10


int* p3 = new int[5];     // 分配包含 5 个 int 的数组,未初始化,元素为随机值 

// C++11:列表初始化(C++98不支持,但可通过循环手动赋值)  
int* arr1 = new int[3]{1, 2, 3};  // 初始化为{1,2,3}  
int* arr2 = new int[5]{1, 2};     // 前2个为1,2,其余为0(值初始化)  
int* arr3 = new int[4]{};         // 全为0  

delete:释放动态内存

基本语法

代码语言:javascript
复制
// 释放单个对象
delete 指针变量;       // 释放单个对象

// 释放数组
delete[] 指针变量;     // 释放数组(必须与 new[] 配对使用)

示例

代码语言:javascript
复制
delete p1;   // 释放单个 int
delete p2;   // 释放单个 int


delete[] arr1; // 释放 int 数组
delete[] arr2; // 释放 int 数组
delete[] arr3; // 释放 int 数组

总结:使用new和delete进行管理存储在堆区上面的内置类型的变量

代码语言:javascript
复制
void Test()
{
	// 动态申请一个int类型的空间,未进行初始化存储随机值
	int* ptr4 = new int;
	// 动态申请一个int类型的空间,并初始化为10
	int* ptr5 = new int(10);
    
	// 动态申请10个int类型的空间,未进行初始化存储随机值
	int* ptr6 = new int[3];
    // 动态申请10个int类型的空间,并进行部分的初始化:前两个元素初始化为1和2,最后一个元素为随机值
	int* ptr7 = new int[3]{1,2};
    
	delete ptr4;
	delete ptr5;
	delete[] ptr6;
    delete[] ptr7;
}
在这里插入图片描述
在这里插入图片描述
怎么使用new和delete管理自定义类型的对象?

在 C++ 中,使用newdelete管理自定义类型(类或结构体)的对象时,需要特别注意:

  1. 构造函数和析构函数的调用
  2. 内存分配的正确性
  3. 资源管理的安全性

动态创建和销毁单个对象

语法

代码语言:javascript
复制
---------------------------动态创建和销毁“单个对象”---------------------------

// 创建对象(自动调用构造函数)
类名* 对象指针 = new 类名(构造参数);  // 带参数的构造函数
类名* 对象指针 = new 类名;           // 默认构造函数(若存在)

// 销毁对象(自动调用析构函数)
delete 对象指针;

示例

代码语言:javascript
复制
class Resource
{
public:
    // 构造函数:初始化资源
    Resource(const string& name) : _name(name)
    {
        cout << "创建资源: " << _name << endl;
    }

    // 析构函数:释放资源
    ~Resource()
    {
        cout << "释放资源: " << _name << endl;
    }

private:
    string _name;
};

// 使用示例
void test()
{
    // 创建对象(调用构造函数)
    Resource* res = new Resource("文件");

    // 使用对象...

    // 销毁对象(调用析构函数)
    delete res;
}

动态创建和销毁对象数组

语法

代码语言:javascript
复制
---------------------------动态创建和销毁“对象数组”---------------------------
    
// 创建对象数组(为每个元素调用构造函数)
类名* 数组指针 = new 类名[元素数量];            // 使用默认构造函数
类名* 数组指针 = new 类名[元素数量]{初始化列表};  // C++11+:列表初始化

// 销毁数组(为每个元素调用析构函数)
delete[] 数组指针;  // 必须使用 [],否则内存泄漏

示例

代码语言:javascript
复制
class Point
{
public:
    int _x, _y;

    Point() : _x(0), _y(0) { cout << "默认构造" << endl; }
    Point(int x, int y) : _x(x), _y(y) { cout << "带参构造" << endl; }
};

// 创建数组示例
void testArray()
{
    // 创建数组(调用3次默认构造函数)
    Point* arr1 = new Point[3];

    // C++11+:列表初始化(调用带参构造函数)
    Point* arr2 = new Point[2]{ Point(1,1), Point(2,2) };

    // 销毁数组(必须用 delete[])
    delete[] arr1;
    delete[] arr2;
}

使用new和delete进行内存管理时需要注意什么?

C++在使用new和delete进行内存的管理的时候需要注意的事项有以下几点:

  1. 配对使用对象与数组区分
  2. 防止内存泄露及时释放
  3. 避免野指针指针置空
  4. 异常安全异常处理
  5. 深拷贝与浅拷贝自定义拷贝控制

下面我们来详细的学习一下上面的5点的注意事项:

配对使用

  • 对象与数组区分new用于分配内存,delete用于释放内存。要确保newdelete正确配对,使用new分配的内存用delete释放;使用new[]分配数组内存,就得用delete[]释放 ,不匹配使用会导致未定义行为 。
    • int* p = new int;,后续需用delete p;释放
    • int* arr = new int[5]; ,则需delete[] arr;释放

防止内存泄漏

  • 及时释放:使用new分配的内存,必须在不再使用时用delete释放。
    • 若忘记释放,随着程序运行,被占用的内存无法回收,会导致内存泄漏,使程序占用内存越来越多,最终可能耗尽系统内存,影响程序性能甚至导致程序崩溃 。
    • 例如:在函数中int* ptr = new int; ,函数结束前需delete ptr;

避免悬挂指针(野指针)

  • 指针置空:释放内存后,原来指向该内存的指针就成为悬挂指针(野指针),继续使用会导致程序崩溃或出现不可预测的行为。所以在delete之后,应将指针设置为nullptr
    • 例如int* p = new int; delete p; p = nullptr;

异常安全

异常处理:当new操作因内存不足等原因失败时,会抛出std::bad_alloc异常。为避免程序因异常终止,在使用new时,建议使用try-catch块等异常处理机制来捕获和处理异常 。

代码语言:javascript
复制
try 
{
    int* ptr = new int[100000000]; // 可能因内存不足抛出异常
} 
catch (const std::bad_alloc& e) 
{
    std::cerr << "内存分配失败: " << e.what() << std::endl;
}

深拷贝与浅拷贝

自定义拷贝控制

如果自定义类中包含指针成员等动态资源,需要正确实现拷贝构造函数赋值运算符,避免浅拷贝问题。

否则在对象拷贝或赋值时,可能出现多个对象的指针成员指向同一块内存,当对象析构或释放内存时,会导致重复释放内存泄漏等问题 。

代码语言:javascript
复制
class MyClass 
{
public:
    MyClass(int* ptr) : data(ptr) {}
    // 未定义拷贝构造函数和赋值运算符,存在浅拷贝问题
    
private:
    int* data;
};

应修改为

代码语言:javascript
复制
class MyClass 
{
public:
    MyClass(int* ptr) 
    :data(new int(*ptr)) 
    {}
      
      
    // 拷贝构造函数
    MyClass(const MyClass& other) 
    :data(new int(*other.data)) 
    {}
    
    // 赋值运算符
    MyClass& operator=(const MyClass& other) 
    {
        if (this != &other) 
        {
            delete data;
            data = new int(*other.data);
        }
        
        return *this;
    }
      
    ~MyClass() 
    {
        delete data;
    }
private:
    int* data;
};

C语言和C++的动态内存管理方式有什么区别?

C++的动态内存管理方式和C语言的动态内存管理方式的区别主要有以下5点:

  1. 操作符/函数
  2. 类型安全
  3. 内存初始化
  4. 内存释放和资源管理
  5. 错误处理

下面的内容我们来详细的讲解一下上面的5点区别:

C++ 和 C 语言的动态内存管理方式存在多方面区别: 1. 操作符 / 函数

  • C 语言:主要通过库函数malloccallocrealloc来分配内存 ,用free函数释放内存。
    • 这些都是 标准库函数,需要包含<stdlib.h>头文件才能使用 。
    • 使用malloc分配内存:int* ptr = (int*)malloc(sizeof(int));
    • 使用free释放内存:free(ptr);
  • C++:使用new操作符分配内存 ,delete操作符释放内存;对于数组,用new[]分配,delete[]释放。
    • 它们是 C++ 语言的 关键字
    • 单一对象分配/释放:分配时int* ptr = new int; ,释放时delete ptr;
    • 对象数组分配/释放:分配时int* arr = new int[5]; ,释放delete[] arr;

2. 类型安全

  • C 语言malloccallocrealloc返回的是void*类型的指针,使用时需要进行强制类型转换 。
    • 例如int* ptr = (int*)malloc(sizeof(int)); ,如果类型转换错误,可能导致编译错误或运行时错误。
  • C++new操作符会自动根据要创建对象的类型返回相应类型的指针,无需强制类型转换 。
    • 例如int* ptr = new int; ,返回的就是int*类型指针,更符合类型安全原则。

3. 内存初始化

  • C 语言malloc分配的内存不会进行初始化,其内容是不确定的垃圾值 ;calloc会将分配的内存初始化为 0
    • int* ptr = (int*)malloc(sizeof(int)); ,此时*ptr的值不确定
    • int* arr = (int*)calloc(5, sizeof(int)); ,此时数组arr的每个元素都是 0
  • C++new操作符在分配内存后:
    • 对于内置类型,如果提供初始值则会进行初始化。
      • 例如int* ptr = new int(5); ,指针ptr指向的int值为 5
    • 对于自定义类型,会调用相应的构造函数进行初始化 。
      • 例如class A { public: A() { std::cout << "构造函数被调用" << std::endl; } }; A* a = new A; ,此时会调用A的构造函数

4. 内存释放与资源管理

  • C 语言free函数只是简单地释放malloccallocrealloc分配的内存空间,不会调用对象的析构函数(如果是自定义类型对象)
    • 例如:自定义类class B { public: ~B() { std::cout << "析构函数被调用" << std::endl; } }; B* b = (B*)malloc(sizeof(B)); free(b); ,这里free(b)不会调用B的析构函数,可能导致资源泄漏等问题。
  • C++delete操作符在释放自定义类型对象内存时,会自动调用对象的析构函数 。
    • 对于数组,delete[]会为每个元素调用析构函数。
      • 例如B* arrB = new B[3]; delete[] arrB; ,会依次调用 3 个B对象的析构函数,确保对象内部资源得到正确清理。

5. 错误处理

  • C 语言malloccallocrealloc函数在内存分配失败时返回NULL ,需要手动检查返回值来判断分配是否成功。
    • 例如int* ptr = (int*)malloc(sizeof(int)); if (ptr == NULL) { // 处理内存分配失败情况 }
  • C++new操作符在内存分配失败时会抛出std::bad_alloc异常 ,需要使用异常处理机制(如:try-catch块)来捕获和处理异常。
    • 例如try { int* ptr = new int; } catch (const std::bad_alloc& e) { std::cerr << "内存分配失败: " << e.what() << std::endl; }

总体而言:C++ 的动态内存管理方式(new/delete)相比 C 语言(malloc/free等)在类型安全、对象初始化与资源管理等方面具有一定优势,更符合面向对象编程的理念 。 但 C 语言的方式也有其灵活性,在一些特定场景(如:与 C 代码兼容等)下仍会使用 。

对比维度

C++ (new/delete)

C (malloc/free)

操作符/函数

使用操作符:new、new[]、delete、delete[]

使用函数:malloc()、calloc()、realloc()、free()

类型安全

自动类型推导,无需强制转换 (如:int* p = new int;`)

需要显式类型转换 (如:int* p = (int*)malloc(sizeof(int));)

内存初始化

new:调用构造函数 new Type():值初始化 new Type[N]{}:列表初始化

malloc:不初始化 calloc:清零初始化 不调用构造函数

内存释放和资源管理

delete:调用析构函数后释放内存 delete[]:对数组每个元素调用析构函数

free:仅释放内存,不调用任何清理函数 需手动释放资源(如:文件句柄)

错误处理

默认抛出std::bad_alloc异常可通过new (nothrow)返回nullptr(C++17起更明确)

返回NULL表示失败 需手动检查返回值(如:if (!p) { /* 处理错误 */ })

代码语言:javascript
复制
#include <iostream>
using namespace std;
class A
{
public:
    /*-------------------------构造函数(带默认参数)-------------------------*/ 
    A(int a = 0)
        : _a(a)  
    {
        
        std::cout << "A():" << this << std::endl;  // 构造函数调用时打印对象地址
    }

    /*-------------------------析构函数-------------------------*/
    ~A()
    {
        
        std::cout << "~A():" << this << std::endl; // 析构函数调用时打印对象地址
    }

private:
    int _a;  
};


int main()
{
    // ============== 自定义类型的内存分配对比 ==============
    cout << "自定义类型的内存分配对比" << endl;
    /*------------使用malloc和new在堆区开辟“单一对象空间”的对比------------*/
    // 1. 使用malloc分配内存 - 不会调用构造函数
    // 只分配内存空间,不进行对象构造
    A* p1 = (A*)malloc(sizeof(A));  // 只分配空间,无构造

    // 2. 使用new分配内存 - 会调用构造函数
    // 分配内存空间并调用构造函数初始化对象
    A* p2 = new A(1);  // 分配空间并构造,参数1传递给构造函数

    /*------------使用free和delete在堆区释放“单一对象空间”的对比------------*/
    free(p1);   // 只释放内存,不调用析构函数
    delete p2;  // 先调用析构函数,再释放内存

    // ============== 内置类型的内存分配对比 ==============
    cout << "内置类型的内存分配对比" << endl;
    // 4. 内置类型使用malloc和new几乎相同
    int* p3 = (int*)malloc(sizeof(int));  // C风格分配
    int* p4 = new int;                    // C++风格分配

    // 释放方式
    free(p3);    // C风格释放
    delete p4;   // C++风格释放

    // ============== 数组内存分配对比 ==============
    cout << "数组内存分配对比" << endl;
    /*------------使用malloc和new在堆区开辟“对象数组空间”的对比------------*/
    // 5. 使用malloc分配对象数组 - 不调用构造函数
    A* p5 = (A*)malloc(sizeof(A) * 10);  // 只分配空间,不构造对象

    // 6. 使用new分配对象数组 - 调用每个元素的构造函数
    A* p6 = new A[10];  // 分配空间并调用10次默认构造函数

    /*------------使用malloc和new在堆区释放“对象数组空间”的对比------------*/
    free(p5);      // 只释放内存,不调用析构函数
    delete[] p6;   // 调用每个元素的析构函数,再释放内存
    // 注意:必须使用delete[]来释放数组,使用delete会导致未定义行为

    return 0;
}

/*
程序输出分析:
1. 只有new A(1)会输出构造信息:A():0x...
2. 只有delete p2和delete[] p6会输出析构信息:~A():0x...
3. malloc/free不会触发构造/析构函数调用
4. 对于内置类型,new/delete和malloc/free行为几乎相同
5. 对象数组必须使用delete[]释放,否则会导致内存泄漏
*/
在这里插入图片描述
在这里插入图片描述

---------------operator new与operator delete函数---------------

什么是operator new与operator delete函数?

operator newoperator delete: 是用于内存分配和释放的底层函数,它们构成了动态内存管理的基础。

  • 它们是 C++ 标准库中预定义的 全局函数,负责实际的内存分配与释放(类似 C 语言的 malloc 和 free)
  • 它们是 静态函数,不依赖于任何对象实例,可以直接调用

new/delete 操作符的关系

  • new 表达式(如:int* p = new int;)的执行流程:
    1. 调用 operator new 分配原始内存。
    2. 在分配的内存上调用对象的构造函数。
  • delete 表达式(如:delete p;)的执行流程:
    1. 调用对象的析构函数。
    2. 调用 operator delete 释放内存。

---------------new/new[] 和delete/delete[] 的实现原理---------------

new和delete的实现原理是什么?

newdelete:是用于动态内存管理的操作符,它们的实现原理涉及到内存分配对象构造与析构等方面,具体如下:


new的实现原理: 1. 调用operator new分配内存:new操作符在底层首先会调用全局的operator new函数(可以自定义重载)来分配所需大小的内存空间。 operator new函数的默认实现通常基于malloc函数,其过程大概如下:

  • 尝试通过malloc分配指定大小(size_t类型参数表示)的内存。
    • 如果malloc分配成功:直接返回分配到的内存指针
    • 如果malloc分配失败operator new会尝试执行用户设置的空间不足应对措施(通过set_new_handler设置)
      • 如果存在该措施:则继续尝试分配内存。
      • 如果没有设置或尝试后仍失败:就会抛出std::bad_alloc类型的异常来表明内存分配失败 。

2. 调用构造函数初始化对象(针对自定义类型):在成功分配内存后:

  • 如果new创建的是 自定义类型(类或结构体)的对象:
  • new操作符会在分配得到的内存上调用该类型的构造函数,对对象进行初始化,完成从一块原始内存到一个可用对象的转变。
    • 定义class A { public: A() { std::cout << "构造函数被调用" << std::endl; } }; ,执行A* a = new A;时,在operator new分配好内存后,就会调用A的构造函数 。
  • 如果new创建的是 内置类型(如:intdouble等)的变量:
    • 如果提供了初始值(如:int* ptr = new int(5); ),则将对应内存初始化为给定值
    • 如果未提供初始值(如:int* ptr = new int; ),则值是未定义的(类似分配的原始内存状态)

delete的实现原理: 1. 调用析构函数清理对象资源(针对自定义类型):当使用delete操作符时 :

  • 如果删除的是 自定义类型 的对象,首先会调用该对象的析构函数。析构函数会清理对象在生命周期内所管理的资源,
    • 例如:释放对象内部动态分配的内存、关闭打开的文件句柄等。
    • 如上述:class A ,执行delete a;时,会先调用A的析构函数。
  • 如果删除的是 内置类型 的对象,由于没有资源清理的需求,此步骤直接跳过。

2. 调用operator delete释放内存

  • 在调用完析构函数(如果有)之后,delete操作符会调用全局的operator delete函数(也可自定义重载)来释放对象所占用的内存。
  • operator delete函数的默认实现基于free函数,它会将之前operator new分配的内存归还给系统堆内存,供后续重新分配使用 。

new[]和delete[]的实现原理是什么?

对于数组形式的 new[]delete[]

  • new[]在分配数组内存时:(针对自定义类型)
    • 除了为数组元素分配足够的内存空间外,还会额外多分配 4 字节来记录数组元素的个数。这是为了保证 delete[] 能够正确知道需要调用几次析构函数。
  • delete[]在释放数组内存时:(针对自定义类型)
    • 会根据之前记录的元素个数,依次调用每个数组元素的析构函数,然后再调用 operator delete[] 函数释放整个数组占用的内存空间 。

总之newdelete 通过与operator newoperator delete函数协作,以及对构造函数析构函数的调用,实现了 C++ 中动态内存的分配与释放,同时兼顾了对象的初始化与资源清理等操作 。

operator new/delete与new/delete的区别是什么?

特性

new/delete 表达式

operator new/delete 函数

本质

语言内置操作符

全局静态函数

功能

分配内存 + 调用构造函数

仅分配原始内存 + 不调用构造

能否重载

常见用途

动态创建对象

自定义内存管理策略

---------------定位new表达式 ---------------

什么是定位new?

定位 new(Placement New):是一种特殊的new表达式,允许在已分配的内存块上构造对象,而无需额外分配内存。

  • 它的核心作用是将对象构造与内存分配解耦,常用于内存池、嵌入式系统、高性能编程等场景。
  • 在实际开发中,定位 new 表达式通常会与内存池配合使用。
    • 由于内存池分配的内存并未经过初始化,
    • 因此若要在这块内存上创建自定义类型的对象,就需要借助定位 new 表达式来显式调用构造函数,
    • 从而完成对象的初始化工作。

定位new的语法new (内存地址) 类型(构造参数);

  • 内存地址:必须是一个void*指针,指向已分配的内存块
  • 类型:要构造的对象类型
  • 构造参数:传递给对象构造函数的参数(可选)

代码示例:在预分配内存上构造对象

代码语言:javascript
复制
#include <iostream>
#include <new> // 必须包含此头文件

using namespace std;

class Point
{
public:
    int _x, _y;

    Point(int x = 0, int y = 0) : _x(x), _y(y)
    {
        cout << "构造 Point(" << _x << ", " << _y << ")" << endl;
    }

    ~Point()
    {
        cout << "析构 Point(" << _x << ", " << _y << ")" << endl;
    }
};

int main()
{
    // 1. 预分配内存(可通过 malloc、new 或栈内存)
    char* buffer = new char[sizeof(Point)]; // 堆上分配
    // 或:char buffer[sizeof(Point)];      // 栈上分配

    // 2. 使用定位new在buffer上构造对象
    Point* p = new (buffer) Point(10, 20);

    // 3. 使用对象
    cout << "p->x: " << p->_x << ", p->y: " << p->_y << endl;

    // 4. 手动调用析构函数(定位new不会自动调用析构)
    p->~Point();

    // 5. 释放预分配的内存(如果是堆上分配的)
    delete[] buffer;

    return 0;
}

定位 new 的核心特点:

  1. 不分配内存,只构造对象
    • 定位 new 不会调用operator new分配内存,而是直接在指定地址上构造对象
    • 内存必须提前分配,且大小和对齐方式需满足对象要求
  2. 需手动调用析构函数
    • 定位 new 构造的对象不会自动析构,需显式调用析构函数(如:p->~Point();
    • 若忘记调用析构函数,会导致资源泄漏(如:动态分配的内存未释放)
  3. 内存来源灵活
    • 可以使用堆内存(如:new char[])、栈内存(如:局部数组)或共享内存
    • 适用于需要精确控制内存位置的场景(如:嵌入式系统中的特定地址)

怎么使用定位new对malloc和operator new分配的空间进行初始化?

代码语言:javascript
复制
#include <iostream>
using namespace std;

class A
{
public:
    /*------------构造函数,带默认参数(a默认为0)------------*/
    A(int a = 0)
        : _a(a)  
    {
        cout << "A():" << this << endl;  // 输出构造函数调用信息及对象地址
    }

    /*------------析构函数------------*/
    // 
    ~A()
    {
        cout << "~A():" << this << endl;  // 输出析构函数调用信息及对象地址
    }

private:
    int _a;  
};

// 定位new(placement new)的使用示例
int main()
{
    /*------------示例1:使用malloc分配内存 + 定位new构造对象------------*/
    cout << "使用malloc分配内存 + 定位new构造对象" << endl;
    // 1. 使用malloc分配一块与A对象大小相同的内存
    //    此时p1指向的是未初始化的内存,还不是一个合法的A对象(构造函数未调用)
    A* p1 = (A*)malloc(sizeof(A));

    // 2. 使用定位new在已分配的内存上构造A对象
    //    new(p1)A; 会在p1指向的内存上调用A的构造函数
    //    如果A的构造函数需要参数,应该写成 new(p1)A(参数);
    new(p1)A;  // 这里调用的是A的默认构造函数(因为a有默认值0)

    // 3. 显式调用析构函数(因为对象是用定位new构造的,需要手动管理)
    p1->~A();

    // 4. 释放malloc分配的内存
    free(p1);

    /*------------示例2:使用operator new分配内存 + 定位new构造对象------------*/
    cout << "使用operator new分配内存 + 定位new构造对象" << endl;
    // 1. 使用operator new分配内存(与malloc类似,但属于C++的机制)
    A* p2 = (A*)operator new(sizeof(A));

    // 2. 使用定位new构造对象,并传入参数10(调用A(int)构造函数)
    new(p2)A(10);

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

    // 4. 使用operator delete释放内存
    operator delete(p2);

    return 0;
}
在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • ---------------C/C++的内存分布---------------
    • C/C++的内存是怎么划分的?
    • C/C++的内存分布是怎么样的?
    • C语言的动态内存管理方式是什么?
      • 关于C语言内存管理的面试题,小伙子你敢试试吗?
    • C++的动态内存管理方式是什么?
      • 怎么使用new和delete管理内置类型的变量?
      • 怎么使用new和delete管理自定义类型的对象?
    • 使用new和delete进行内存管理时需要注意什么?
    • C语言和C++的动态内存管理方式有什么区别?
  • ---------------operator new与operator delete函数---------------
    • 什么是operator new与operator delete函数?
  • ---------------new/new[] 和delete/delete[] 的实现原理---------------
    • new和delete的实现原理是什么?
    • new[]和delete[]的实现原理是什么?
    • operator new/delete与new/delete的区别是什么?
  • ---------------定位new表达式 ---------------
    • 什么是定位new?
    • 怎么使用定位new对malloc和operator new分配的空间进行初始化?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档