一、前言

学好 C++ 内存管理是写出高质量代码的关键。相比 C 语言的 malloc/free,C++ 提供了更强大、更安全的 new/delete 机制。这期来全面了解 C/C++ 的内存分布、new/delete 的用法以及常见的内存问题。

二、C/C++ 内存分布

一个 C/C++ 程序运行时,内存被划分为以下几个区域:

区域 存放内容 分配方式 生命周期
栈 (Stack) 局部变量、函数参数、返回地址 自动分配释放 函数结束即释放
堆 (Heap) 动态申请的内存(malloc/new) 手动申请释放 程序员控制
全局/静态区 (Data Segment) 全局变量、static 变量 程序启动分配 程序结束释放
常量区 (.rodata) 字符串常量、const 全局量 编译时确定 只读,程序结束释放
代码区 (.text) 函数代码(二进制指令) 编译时确定 只读,程序结束释放
int global_var = 0;          // 全局/静态区
static int static_var = 1;   // 全局/静态区
const int const_global = 2;  // 常量区

void Func() {
    int local_var = 3;       // 栈区
    static int func_static = 4;  // 全局/静态区
    int* p = new int(5);     // p 在栈,指向的内容在堆
    const char* str = "hello";  // str 在栈,指向常量区
    delete p;
}

三、new / delete 的基本用法

C++ 的 new/delete 比 malloc/free 更强大,它会自动调用构造和析构函数。

// 单个对象
int* p1 = new int;            // 不初始化(值是随机值)
int* p2 = new int(10);        // 初始化为 10
int* p3 = new int();          // 值初始化为 0
int* p4 = new int{};          // C++11 列表初始化
delete p1;                    // 释放单个对象
delete p2;

// 数组
int* arr1 = new int[10];          // 不初始化
int* arr2 = new int[10]();        // 所有元素初始化为 0
int* arr3 = new int[10]{1,2,3};   // 前三个为 1,2,3,其余为 0
delete[] arr1;                    // 释放数组必须用 delete[]
delete[] arr2;
delete[] arr3;

// 自定义类型——new 会调用构造函数
class A {
public:
    A(int x = 0) : _x(x) { cout << "构造 " << _x << endl; }
    ~A() { cout << "析构 " << _x << endl; }
private:
    int _x;
};

A* pa = new A(42);    // 输出:构造 42
delete pa;            // 输出:析构 42

A* arr = new A[3];    // 输出三次:构造 0
delete[] arr;         // 输出三次:析构 0

注意:new[] 必须搭配 delete[]new 必须搭配 delete,混用会导致未定义行为!

四、new/delete 与 malloc/free 的详细对比

对比维度 malloc/free new/delete
本质 函数(C 标准库) 运算符(C++ 关键字)
返回值 void*,需要强转 自动类型匹配
大小 需要手动计算字节数 编译器自动计算
自定义类型 只开辟空间,不调构造函数 开空间 + 调构造函数
失败处理 返回 NULL 抛出 std::bad_alloc 异常
数组处理 手动计算总大小 new[] 自动处理
可重载 不可 可以(全局或类级别)
扩展功能 支持 placement new
// malloc 的写法:
int* p1 = (int*)malloc(sizeof(int));
if (p1 == nullptr) { /* 处理错误 */ }
free(p1);

// new 的写法(更简洁):
int* p2 = new int(10);    // 不需要强转,不需要 sizeof
delete p2;                 // 不返回错误码,失败抛异常

工作流程对比:

  • new 内部流程:调用 operator new(内部调用 malloc)→ 调用构造函数
  • delete 内部流程:调用析构函数 → 调用 operator delete(内部调用 free

五、定位 new (placement new)

在已分配好的内存上构造对象,不额外申请空间。

#include <new>  // 需要包含头文件

// 基本用法
void* mem = operator new(sizeof(A));  // 只分配内存
A* p = new(mem) A(10);                // 在已有内存上构造
// 使用对象...
p->~A();                              // 手动调用析构
operator delete(mem);                 // 手动释放内存

// 实用场景:内存池
alignas(A) char buffer[sizeof(A)];    // 栈上空间

A* p2 = new(buffer) A(42);            // 在栈上 buffer 构造,不涉及堆分配
p2->~A();                             // 手动析构

placement new 的应用场景:

  • 内存池(预先分配大块内存,避免频繁的堆分配)
  • 嵌入式/实时系统(不允许动态内存分配失败)
  • 共享内存中构造对象

六、内存泄漏与检测方法

什么是内存泄漏? 动态分配的内存没有被释放,程序失去对这块内存的掌控。长期运行的程序(如服务器)内存泄漏会导致内存耗尽。

void LeakExample() {
    int* p = new int[1000];
    // 忘记 delete[] p — 内存泄漏!
    // 函数结束,p 指针销毁,但堆上 1000 个 int 永远不会被释放
}

常见内存泄漏场景:

  1. new 后忘记 delete
  2. new[] 用了 delete(或反过来)
  3. 异常导致 delete 被跳过
  4. 基类析构函数不是虚函数

检测内存泄漏的方法:

// 方法1:_CrtDumpMemoryLeaks (Windows/MSVC)
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

int main() {
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    int* p = new int(42);
    // 不 delete,运行后会在输出窗口显示内存泄漏报告
    return 0;
}

// 方法2:使用 Valgrind (Linux)
// $ valgrind --leak-check=full ./program

// 方法3:使用智能指针避免手动管理
#include <memory>
void SafeExample() {
    std::unique_ptr<int[]> p(new int[1000]);
    // 离开作用域自动 delete,不会泄漏
}

七、RAII 简介

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 管理资源的核心理念:在构造函数中获取资源,在析构函数中释放资源。

// 传统的麻烦方式
void OldWay() {
    int* p = new int[100];
    // ... 可能抛出异常,跳过 delete
    delete[] p;  // 异常时执行不到这里
}

// RAII 方式——用对象管理资源
class IntArray {
public:
    IntArray(size_t n) : _data(new int[n]), _size(n) {}
    ~IntArray() { delete[] _data; }
    // 拷贝构造、赋值运算符(实现深拷贝,此处省略)
private:
    int* _data;
    size_t _size;
};

void RAIIWay() {
    IntArray arr(100);  // 构造函数获取资源
    // ... 即使抛出异常,arr 析构时也会释放资源
}  // 离开作用域自动调用析构函数

C++ 标准库的智能指针(unique_ptrshared_ptrweak_ptr)就是 RAII 思想的典型应用。

八、operator new/delete 的重载

可以全局重载或类级别重载,用于定制内存分配策略。

// 全局重载——影响所有 new/delete
void* operator new(size_t size) {
    std::cout << "全局 new,大小:" << size << std::endl;
    void* p = malloc(size);
    if (!p) throw std::bad_alloc();
    return p;
}

void operator delete(void* p) noexcept {
    std::cout << "全局 delete" << std::endl;
    free(p);
}

// 类级别重载——只影响该类
class MyClass {
public:
    void* operator new(size_t size) {
        std::cout << "MyClass 专属 new" << std::endl;
        return malloc(size);
    }
    void operator delete(void* p) noexcept {
        std::cout << "MyClass 专属 delete" << std::endl;
        free(p);
    }
};

int main() {
    int* p = new int(5);       // 调用全局重载
    delete p;
    MyClass* mp = new MyClass; // 调用类级别重载
    delete mp;
    return 0;
}

重载的应用场景:

  • 内存池(减少碎片、提高分配速度)
  • 统计内存使用情况(调试或性能分析)
  • 特定对齐要求

九、面试常见问题

Q1:new 和 malloc 的区别是什么?
A:new 是运算符,malloc 是函数;new 自动计算大小、返回正确类型、调用构造函数、失败抛异常;malloc 需手动计算大小、返回 void*、不调用构造函数、失败返回 NULL。

Q2:new[] 和 delete[] 如何知道数组大小?
A:大多数编译器在 new[] 分配的内存块头部额外存储了数组元素个数。delete[] 读取这个值,从而知道要调用多少次析构函数。

Q3:什么是内存泄漏?如何避免?
A:动态分配的内存没有被释放导致资源浪费。避免方法:用 RAII(智能指针)、new/delete 成对出现、使用内存检测工具。

Q4:placement new 有什么用途?
A:在已分配的内存上构造对象,主要用于内存池、嵌入式系统、共享内存等场景,避免频繁堆分配的开销。

十、总结

  1. C/C++ 内存五区分布及各自特点
  2. new/delete 的基本用法(单个对象和数组)
  3. new/delete 与 malloc/free 的全面对比
  4. placement new 的用法和场景
  5. 内存泄漏的原因和检测方法
  6. RAII 的核心思想
  7. operator new/delete 的重载

内存管理是 C++ 学习的重点也是难点,掌握好这些知识对写出高效、稳定的程序至关重要。下期进入模板初阶!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐