云计算百科
云计算领域专业知识百科平台

内存分区与new

1. C++内存分区模型

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

分区存放内容生命周期管理方式
代码区 编译后的二进制代码(函数体、只读常量) 程序运行期 操作系统管理
全局/静态存储区 全局变量、静态变量、常量(const全局) 程序运行期 编译时分配,自动释放
栈区 局部变量、函数参数、返回地址等 函数调用期间 编译器自动管理
堆区 动态分配的内存(new/malloc) 由程序员控制 程序员手动分配和释放
常量区(在全局区中) 字符串字面量、const修饰的全局常量 程序运行期 只读,不可修改

1.1 代码区

存放CPU执行的机器指令,通常为只读,防止程序意外修改。函数体的二进制代码就存放在这里。

代码区是共享和共读的。

代码区是共享的,共享的目的是当多进程运行同一个可执行文件时(例如多用户同时运行同一个程序,或同一个程序被多次启动),操作系统只需要在物理内存中加载一份代码,然后让多个进程的虚拟地址空间映射到同一份物理代码页上。

好处:

1.节省内存,避免加载相同代码浪费物理空间。

2.提高效率:减少磁盘I/O和内存占用。

注意:这种共享是只读的共享,代码是只读的,任何进程不能修改它,否则会破坏其他进程的执行

1.2 全局/静态存储区(包含常量区)

  • 全局变量(在所有函数外定义)

  • static修饰的变量(无论是全局还是局部)

  • 已初始化的全局变量和未初始化的全局变量分属两个相邻区域(.data和.bss段)

  • 生命周期从程序开始到结束。

  • 该区域的数据在程序结束后由操作系统释放。

  • 该区域还包含常量区,字符串常量和其他常量。

示例:

int globalVar = 10; // .data 段
static int staticVar = 20; // .data 段
int uninitGlobal; // .bss 段,自动初始化为0

1.3 栈区

由编译器自动分配释放,存放函数的参数值、局部变量等。栈内存分配速度快,但空间有限(通常几MB)。函数调用时,参数和局部变量压栈,函数返回时出栈。

注意:不返回局部变量的地址,栈区开辟的数据由编译器自动释放

局部变量存放在栈区,栈区数据在函数执行完后自动释放

#include <iostream>
using namespace std;
void func()
{
int a = 10; //在栈区,执行后自动释放
return &a;//返回地址
}
int main()
{
cout << "1" << endl;
int* p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}

结果:乱码,报错

示例:

void func() {
int a = 5; // 栈上分配
char buf[100]; // 栈上数组
}

1.4 堆区

由程序员显式分配和释放(new/delete或malloc/free)。若未释放,程序结束时可能由操作系统回收,但长期运行的程序会内存泄漏。堆空间大,但分配和释放较慢,可能产生内存碎片。

栈区即使有数据地址,数据本身也被释放了,不能使用:而堆区的数据不会被自动释放,因此指针指向的数据可以被解引用

堆区的数据不能通过变量直接访问,只能通过指针间接访问

2.5 常量区

存放字符串字面量和const修饰的全局常量。该区域只读,修改会导致未定义行为(通常段错误)。

示例:

const char* p = "hello"; // "hello"存储在常量区,p指向它
char* q = "world"; // 危险:修改*q会崩溃,应使用const char*

2. new/delete操作符深入解析

new和delete是C++中用于动态内存管理的操作符,它们不仅分配内存,还会调用对象的构造函数和析构函数。

2.1 new的三种用法

(1) 分配单个对象

int* p = new int; // 未初始化
int* q = new int(10); // 初始化为10
MyClass* obj = new MyClass; // 调用默认构造函数

(2) 分配数组

int* arr = new int[100]; // 未初始化
MyClass* objs = new MyClass[10]; // 调用10次默认构造函数

(3) 定位new(placement new)

在已分配的内存上构造对象,不分配新内存。

#include <new>
char buffer[sizeof(MyClass)];
MyClass* p = new (buffer) MyClass; // 在buffer上构造对象

2.2 new的内部实现流程

对于new T(T为类型),编译器大致转换为以下步骤:

  • 调用operator new(sizeof(T))分配原始内存(内部通常调用malloc)。

  • 若分配成功,在获得的内存上调用T的构造函数。

  • 返回指向该对象的指针。

  • 对于new T[n]:

  • 调用operator new[](sizeof(T) * n + 额外空间),额外空间通常用于存储数组元素个数(以便delete[]知道调用多少次析构函数)。

  • 依次调用每个元素的构造函数。

  • 返回指向第一个元素的指针。

  • operator new可以被重载,提供自定义的内存分配策略。

    2.3 delete的内部实现流程

    对于delete p:

  • 调用p所指向对象的析构函数。

  • 调用operator delete(p)释放内存(内部通常调用free)。

  • 对于delete[] p:

  • 根据存储的元素个数,逆序调用每个元素的析构函数。

  • 调用operator delete[]释放内存。

  • 2.4 new[]与delete[]必须配对使用

    • new[]分配的内存布局中可能在对象数组前有一个“数量”字段,若用delete释放,会导致:

      • 只调用第一个元素的析构函数,其他元素未析构(资源泄漏)。

      • 释放的指针不是原始分配地址,导致内存释放错误。

    • 反之,new分配的对象用delete[]释放,也会导致未定义行为。

    示例:

    int* p = new int[10];
    delete p; // 错误!应该用 delete[] p

    MyClass* q = new MyClass;
    delete[] q; // 错误!应该用 delete q

    3. new/delete与malloc/free的区别

    特性new/deletemalloc/free
    属性 C++操作符 C/C++标准库函数
    是否需要头文件 不需要 需要<cstdlib>或<malloc.h>
    返回类型 类型严格匹配的指针 void*,需强制转换
    分配失败 抛出std::bad_alloc异常 返回NULL
    构造函数/析构函数 自动调用 不调用
    内存大小计算 由编译器根据类型计算 需手动指定字节数
    重载 可以重载operator new 不可重载
    数组支持 new[]自动记录元素个数 需自行记录

    示例:

    // malloc/free
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == nullptr) { /* 处理失败 */ }
    free(p);

    // new/delete
    int* q = new int[10];
    // 若失败抛出异常,可用try-catch或nothrow版本
    delete[] q;

    4. 常见内存问题与最佳实践

    4.1 内存泄漏

    分配后忘记释放,尤其是异常路径。
    解决:使用智能指针(std::unique_ptr、std::shared_ptr)、RAII、容器(std::vector)代替裸指针。

    4.2 野指针

    指针指向已释放的内存或非法地址。
    解决:释放后置nullptr,避免重复释放;初始化指针为nullptr。

    4.3 重复释放(double free)

    同一地址释放两次,导致堆损坏。
    解决:遵循谁分配谁释放原则,使用智能指针自动管理。

    4.4 内存越界

    访问数组边界之外。
    解决:使用std::array、std::vector,或者手动检查边界。

    4.5 混用new[]与delete

    解决:严格配对使用,用std::vector管理动态数组。

    4.6 最佳实践建议

    • 优先使用STL容器和智能指针,减少手动内存管理。

    • 若必须手动管理,遵循“在构造函数中分配,在析构函数中释放”的RAII原则。

    • 为自定义类重载operator new/delete时,同时重载new[]/delete[]。

    • 使用nothrow版本的new以避免异常,或者用try-catch处理bad_alloc。

    #include <new>
    int* p = new (std::nothrow) int[100];
    if (p == nullptr) {
    // 处理分配失败
    }

    5. 总结

    本文详细介绍了C++的内存分区模型,包括代码区、全局/静态区、栈区、堆区和常量区,并深入分析了new/delete的工作机制。理解这些底层原理有助于:

    • 定位和避免内存错误

    • 优化程序性能(如减少堆分配,使用栈)

    • 正确实现自定义内存管理

    • 写出更安全、更高效的C++代码

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 内存分区与new
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!