一、前言

内存管理是 C++ 语言的核心特性之一,也是区分高级开发者与初学者的重要标志。动态内存分配允许程序在运行时向操作系统申请和释放内存,突破了静态内存大小固定、生命周期受限的瓶颈,广泛应用于动态数据结构、高性能容器、大型项目开发中。

C++ 提供了new/delete关键字实现动态内存管理,同时兼容 C 语言的malloc/free,但二者在用法、底层机制、安全性上存在本质区别。C语言的动态内存分配在本人博客的C语言专栏中已经发布,本文将主要讲解 C++中的动态内存分配。

二、为什么需要动态内存分配?(核心痛点)

  1. 静态内存的局限性
    • 编译期确定大小,无法灵活扩容
    • 栈内存空间有限,无法存储大量数据
    • 内存生命周期由作用域控制,无法自由管理
  2. 动态内存的优势
    • 运行时分配内存,大小灵活可控
    • 内存生命周期由程序员手动管理
    • 支持动态数据结构(链表、栈、队列、树等)
    • 高效利用堆内存,适合大数据存储

三、栈内存 vs 堆内存

  1. 栈内存(自动内存,由计算机自行管理变量的生存周期)
    • 分配方式:编译器自动分配 / 释放
    • 存储内容:局部变量、函数参数
    • 特点:速度快、空间小、生命周期固定
  2. 堆内存(动态内存,程序员手动管理内存的申请与释放)
    • 分配方式:手动申请 / 手动释放
    • 存储内容:动态创建的对象、数组
    • 特点:速度较慢、空间大、生命周期自由
  3. 核心区别总结
  栈区内存 堆区内存
管理方式 编译器自动管理:由系统自动分配和释放,无需程序员干预。 程序员手动管理:需使用 new/deletemalloc/free(C语言,C++兼容) 手动分配与释放(现代C++推荐使用智能指针自动管理,可以更好的避免内存泄漏带来的风险)。
分配速度 极快:系统底层有专门的寄存器/指针记录栈顶,分配和释放只需移动指针。 较慢:涉及复杂的内存寻址、空闲链表查找以及可能的系统调用。
内存大小 较小且受限:通常为几MB(Windows默认1MB,Linux默认8MB),超出易导致栈溢出(Stack Overflow)。 极大且灵活:受限于系统可用虚拟内存,适合分配大型数据结构。
生命周期 与作用域绑定:变量在离开其定义的作用域(如函数返回)时自动销毁。 与代码逻辑绑定:从 new 分配开始,直到显式 delete 之后结束。
碎片问题 无碎片:栈是连续的内存块,采用“先进后出”的线性结构,不存在内存碎片。 易产生碎片:频繁的 new/delete 会导致内存空间不连续,降低内存利用率及分配效率。
生长方向 向低地址生长:从高地址向低地址扩展。 向高地址生长:从低地址向高地址扩展(两者在内存中相向而行)。
适用场景 局部变量、函数参数、生命周期短且大小在编译期已知的数据。 对象生命周期超出当前作用域、大小在运行时才确定、或需要跨函数/线程共享的大型数据。

 

四、C++ 动态内存分配核心:new /delete

1. 基础用法

  • 单个变量的动态分配与释放:new / delete
int main() {
	int* p = new int(10);//申请堆区内存,并给这片内存初始化

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

	delete p;//释放堆区内存
	p = nullptr;//释放后立刻置空,防止产生野指针或者二次释放

	return 0;
}

  • 动态数组的分配与释放:new[] / delete[]
int main() {
	int* p = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	//从堆区申请10个int型变量构成的数组,并依次给每个int元素初始化
	
	for (int i = 0; i < 10; i++) {
		cout << "p[" << i << "]=" << p[i] << " ";
	}

	delete[]p;
	p = nullptr;//释放后立刻置空
}

2. 底层原理(简单讲解,不晦涩)

  • new 做了两件事:

       对基本数据类型来讲:new分配内存+初始化

       对自己设计的类来讲:new分配内存 + 调用构造函数(构造对象)

  • delete 做了两件事:调用析构函数 + 释放内存

五、C 风格动态内存:malloc /free/realloc

  1. 函数用法简单解释
    • malloc:分配内存,只进行内存分配并不会构造对象或者初始化,如果想初始化内存需要使用calloc函数
    • free:释放内存,只是释放内存
    • realloc:扩容函数,对已有空间进行扩容
  2. 适用场景与局限性

适用场景:

1.malloc运行时才能确定需要的空间大小的时候。

2.栈区内存不够,需要申请大量的内存的时候。

3.需要跨函数跨作用域使用数据的时候。

4.实现动态数据结构的时候,比如链表的结点,树的结点等。

局限性:

1.只能申请和释放内存,并不能对类的对象进行构造和析构。

2.malloc分配失败直接返回空,使用从堆区malloc申请的内存之前需要先判断是否分配成功。

3.malloc必须手动计算需要申请的内存大小。

六、new/delete 与 malloc/free 核心区别(面试高频!)

1.本质区别:运算符 vs 库函数

        malloc/free是库函数,而new/delete是运算符

2.是否调用构造 / 析构函数

        malloc不会调用构造函数,free不会调用析构函数

        new会调用构造函数,delete会调用析构函数

3.内存分配失败处理

        malloc内存分配失败直接返回nullptr,new内存分配失败会抛出异常

4..语法简洁性

       malloc需要程序员自行/手动计算需要的内存字节大小,而new只需要给出需要的元素个数即可。

5.类型安全性

        malloc返回结果是void*类型的指针,需要根据需要进行强转,而new是直接指定需要的数据类型,可以直接使用,类型更加安全

6.总结:C++ 优先使用 new/delete

七、动态内存常见错误与避坑指南(重点!)

1.内存泄漏(最常见)

        (1)只申请不释放

int main() {
	int* p = new int(10);//只申请

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


	//delete p;//不释放,就会造成内存泄漏
	//p = nullptr;

	return 0;
}

        (2)重复赋值

int main() {
	int* p = new int(10);//只申请

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

	int a = 100;
	p = new int(200);
	//给p重复赋值,原本10的那片空间的地址被覆盖了,之后也无法找回,这片空间就会一直被占用不会释放

	delete p;//释放的是200的空间,10的空间的地址丢失,无法释放
	p = nullptr;

	return 0;
}

        (3)丢失指针:将申请的空间的地址给了块作用域变量或是局部变量,出了块作用域,变量被销毁了,变量中存储的地址就找不到了

void func(int* p) {//变量p是块作用域变量
	p = new int(6);
}//一旦这个函数调用结束,变量p就会被销毁,申请的指向堆区空间的指针丢失
int main() {
	int* m = nullptr;
	func(m);

	delete m;
	m = nullptr;
	return 0;
}

        (4)还有一种情况就是指针p++操作后释放p,释放不完全,也会造成内存泄漏

int main() {
	int* p = new int(10);//只申请

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

	p++;
	//p向后移动,指向的不是申请的空间的第一个字节的地址了

	delete p;//此时释放的话,也会造成内存泄漏
	p = nullptr;

	return 0;
}

这种情况,可以将变量p设置为指针常量,让p指向的地址不会发生改变(int* const p;)

        (5)父类指针指向子类对象(如果子类存在动态内存分配的话)

如果子类在堆上分配了内存,而父类指针指向它,当 delete 父类指针时,如果父类析构函数不是虚函数,只会调用父类析构函数,不会调用子类析构函数 → 子类动态内存永远不释放 → 内存泄漏!

// 父类
class Father {
public:
    // 普通析构函数(不是虚函数)
    ~Father() {
        // 父类没有动态内存
    }
};

// 子类
class Son : public Father {
private:
    // 子类有动态内存!
    int* p;

public:
    Son() {
        // 子类在堆上分配内存
        p = new int[100];
    }

    // 子类析构函数:释放内存
    ~Son() {
        delete[] p;
    }
};

int main() {
    // 父类指针 指向 子类对象
    Father* f = new Son();

    // 关键问题在这里!
    //delete f;
    f->~Father();
    return 0;
}

这个问题的解决方法就是:将析构函数设置为虚函数!

 

    2.检测与避免方法

检测内存泄漏的方法:

(1)自己封装检测函数

eg:maollc/free检测

void my_malloc(size_t size) {
    void* p = malloc(size);
    printf("malloc申请了内存:%p\n", p);
}
void my_free(void* p) {
    printf("free释放了内存:%p\n", p);
    free(p);
    p = nullptr;
}

(2)Windows + Visual Studio 自带检测(无需安装任何工具)

VS 内置了 CRT 内存泄漏检测器,代码加几行就能用,直接定位到泄漏的代码行

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
int main() {
    //开启内存泄漏检测
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    int* p = new int(10);

    return 0;
}

(3)最通用:Linux/macOS → Valgrind(行业标准)

Valgrind 是 C++ 内存问题的黄金工具,能检测:内存泄漏、野指针、越界访问、重复释放

使用命令:

g++ -g test.cpp -o test
valgrind --leak-check=full ./test

输出结果:

LEAK SUMMARY:
   definitely lost: 4 bytes in 1 block
   indirectly lost: 0 bytes

避免内存泄漏的方法:

(1)申请与释放必须成对出现

(2)释放后立刻把指针置空

(3)不要让指针偏移(用 int* const p 保护)

int* const p = malloc(10 * sizeof(int));
p++; // 编译报错!从根源防止地址丢失

(4)继承结构中,父类析构函数必须是虚析构函数

(5)不用的内存立刻释放,不要等到程序结束

(6)使用C++ 智能指针(最推荐)自动释放,永远不会泄漏。

八、new的两个重载

1.定位new(placement new)

在已有的内存上创建对象,不会重新分配内存

语法格式:new(已有空间的地址)类型(参数)

class Person {

};
int main() {
    char buff[100];//提前开辟空间
    Person* p = new(buff) Person;

    p->~Person();
    return 0;
}

因为定位new不会分配内存,释放只用手动析构(不用free/delete)

定位new的应用:内存池、预分配内存、高性能框架

2.nothrow new(作用类似malloc)

我们常用的new如果分配内存失败会抛出异常,

但是nothrow new,如果分配内存失败,不会抛出异常,直接返回nullptr,和malloc类似。

int main() {
    int* p = new(nothrow) int[1000000000];

    if (p == nullptr) {
        cout << "内存分配失败!\n";
    }

    delete[] p;
    p = nullptr;
    return 0;
}

其实在这里会发现:

1. 定位new+malloc可以实现普通new的功能。

malloc申请空间,定位new在已有的内存空间上创建对象,这就是内存申请+构造函数的调用。

2. free+调用析构函数能够实现delete的功能。

 

需要注意的是:使用裸指针管理堆区内存存在一定的风险,所以一般在C++中使用智能指针对堆区内存进行管理,可以更好的避免内存泄漏的问题。

九、结束语

1. 合理使用动态内存、养成规范释放的习惯,才能写出更稳健的代码。

2. 吃透new/delete的用法与坑点,避开内存问题,稳步提升编码能力。

 

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐