当前位置: 首页 > news >正文

详解C/C++内存管理

1. 内存布局的分布

在讲解内存管理前,我们先了解一下C/C++中内存布局的分布,一个典型的 C++ 程序内存通常分为以下几个区域:

(1)栈 :用于存储函数的局部变量、函数参数、返回地址等

         管理方式:由编译器自动分配和释放。当函数被调用时,其变量在栈上创建;当函数返回时,这些内存被自动回收。

         特点:速度快,但大小有限(通常几 MB)。生命周期由作用域决定。

#include<iostream>
using namespace std;
int Add(int x, int y)//这里的函数调用就是栈的创建
{return x + y;
}
int main()
{std::cout << Add(10, 20) << endl;//调用结束后,空间会被收回return 0;
}

(2)堆 :用于动态分配的内存。这是手动管理内存的主要战场

         管理方式:由程序员显式地分配(newmalloc)和释放(deletefree。如果忘记释放,会导致内存泄漏。

          特点:容量大(只受系统可用内存限制),但分配和释放速度较慢,需要手动管理生命周期。

int main()
{int* ptr= (int*)malloc(10*sizeof(int));//malloc在堆上开辟空间int* ptr2 = new int(1);//用new来开辟空间,还可以进行初始化,给ptr2初始化为1;return 0;
}

(3)全局/静态存储区 :用于存储全局变量、静态变量(static 关键字)。

        管理方式:在程序开始时分配,程序结束时释放。

        特点:生命周期贯穿整个程序。

int x=10;//全局变量
int Add(int x,int y)
{static int=k//静态变量,相当于全局变量k=x+y;return k;
}
int main()
{int y=0;//局部变量。return 0;
}

(4)常量存储区 :用于存储字符串常量和其他常量。

         特点:里面的数据是只读的,修改它会导致未定义行为(通常程序崩溃)

       

const char* str = "Hello, World"; //"Hello, World" 本身在这个区域

(5)代码区:用于存储程序的二进制代码(计算机指令)

知道了以上的区域划分后,我们可以来看一个练习

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

答案为: CCCAAAAADAB

2.手动内存管理类

在C语言当中,我们通常用malloc/realloc/free等内存函数来管理内存。那么在C++当中我们管理内存通常用new/delete。

2.1 new/delete操作内置类型

基础语法如下:

int main()
{int* ptr1 = new int;//动态申请一个int类型空间int* ptr2 = new int(10);//动态申请一个int类型空间并初始化为10;int* ptr3 = new int[3];//动态申请三个int类型空间delete ptr1;delete ptr2;delete []ptr3;//特别主义,释放第三种的空间的时候,在delete后面加[];return 0;
}

2.2 newdelete操作自定义类型

new和delete当然也可以操作自定义类型。

我们看以下代码即可理解:

#include<iostream>
using namespace std;
class A
{
private:int _a;
public:A(int a = 0):_a(a){cout << "A()" << this << endl;}~A(){cout << "~A()" << this << endl;}
};
int main()
{//new/delete和malloc/free的最大区别是,malloc/free只负责开空间,//new/delete在开空间的基础上还会调用自定义类型里面的构造函数和析构函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;return 0;
}

在内置类型里面new/delete和malloc/free的区别就不大,可以理解为几乎是一样的。

int main()
{int* ptr1 = (int*)malloc(sizeof(int));int* ptr2 = new int(0);free(ptr1);delete ptr2;return 0;
}

3.operator newoperator delete函数

尽管我们已经了解了new/delete的用法,但是实际上我们真正的想掌握重载new delete的方法,首先就要对new/delete表达式的工作机理有更多了解。

例如我们刚才的例子:

int main()
{int* ptr1 = new int;//动态申请一个int类型空间int* ptr2 = new int(10);//动态申请一个int类型空间并初始化为10;int* ptr3 = new int[3];//动态申请三个int类型空间delete ptr1;delete ptr2;delete []ptr3;//特别主义,释放第三种的空间的时候,在delete后面加[];return 0;
}

以上的代码实际上执行了三步操作。第一步,new表达式调用一个名为operator new(operator new[ ])的标准库函数。也就是说new/delete只是一个定义,它要调用内层的标准库函数。这个operator new/operator delete的库函数会分配一块足够大,原始的,未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造函数这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。

当我们使用一条delete表达式删除一个动态分配的对象时

int main()
{int* ptr2 = new int(0);int* ptr3 = new int[3];delete ptr2;//销毁*ptr2,让后释放sp指向的内存空间delete []ptr3;//销毁数组中的元素,让后释放对应的内存空间。return 0;
}

这里的delete实际上执行了两步操作。第一步,对ptr2所指的对象或者ptr3所指向的数组中的元素执行对应的析构函数。第二步。编译器调用名为operator delete(operator delete[ ])的标准库函数来释放内存空间

如果应用程序希望控制内存分配的过程,则他们需要定义自己的operator new函数和operator delete函数。即使在标准库已经存在这两个函数的定义,我们仍旧可以定义自己的版本。编译器不会对这种重复的定义提出异议,相反,编译器将使用我们自定义的版本替换标准库定义的版本

应用程序可以在全局作用域中定义operator new和operator delete函数,也可以将它们定义为成员函数。当编译发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数。

如果被分配(释放)的对象是类类型时,则编译器首先在类及其基类的作用域中查找。此时如果该类含有operator new/delete成员。则相应的表达式将调用这些成员。否则,编译器在全局作用域查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表示会;如果没找到,就用标准库函数。

3.1 operator new/delete 接口

标准库中定义了operator new/delete的八个重载版本。其中前4个版本可能抛出bad_alloc异常,后四个则不会抛出异常。

void* operator new(size_t);//分配一个对象
void* operator new[](size_t);//分配一个数组
void* operator delete(void*) noexcept;//释放一个对象
void* operator delete[](void*) noexcept;//释放一个数组
//这些版本承诺不会抛出异常
void* operator new(size_t,nothrow_t&) noexcept;
void* operator new[](size_t,nothrow_t&) noexcept;
void* operator delete(void*,nothrow_t&) noexcept;
void* operator delete[](void*,nothrow_t&) noexcept;

我们来逐个语句解释:

(1)void* operator new(std::size_t); //分配一个对象

作用:请求分配至少size字节的连续内存空间。

参数: size 需要分配的字节数。对于单个对象,这个值通常等于sizeof(myclass) ,但编译器可能传入更大的值。

返回值:成功时,返回指向分配的内存块起始位置的指针。

失败行为:如果分配失败(如内存不足),默认情况下会抛出 std::bad_alloc 异常

(2)void* operator new[](std::size_t); //分配一个数组

作用:与单个对象版本完全相同,但它用于分配数组。

参数:size e - 需要分配的字节数。对于 new myclass[N],这个值通常至少是 N * sizeof(myclass),但编译器可能会分配更多空间(例如用于存储数组长度等元信息)。

(3) void operator delete(void*) noexcept; //释放一个对象

作用:释放之前由operator new分配的单个对象的内存。

参数:ptr - 一个指向待释放内存块的指针,必须是之前从 operator new 返回的值或者是空指针。

行为:如果 ptr 是 nullptr,则该函数不执行任何操作。该函数不调用析构函数(析构函数由 delete 表达式在调用 operator delete 之前调用)。

异常规范noexcept 表示该函数承诺不会抛出任何异常。这对于析构路径中的安全性至关重要

(4) void operator delete[](void*) noexcept; //释放一个数组

作用:释放之前由 operator new[] 分配的数组的内存

调用时机:当你使用 delete[] arr 时,底层就会调用这个函数。

(5)//这些版本承诺不会抛出异常

void* operator new(std::size_t, std::nothrow_t&) noexcept;

void* operator new[](std::size_t, std::nothrow_t&) noexcept;

作用:与基础版本功能相同,都是分配内存。

关键区别:如果分配失败,它们不会抛出 std::bad_alloc 异常,而是直接返回一个空指针 (nullptr)

(6)void operator delete(void*, std::nothrow_t&) noexcept;

    void operator delete[](void*, std::nothrow_t&) noexcept;

作用:用于释放由对应的 Nothrow operator new 分配的内存。

提示:在实践中,标准的 operator delete(void*) 版本完全可以释放任何 operator new 版本(包括nothrow)分配的内存。释放机制是通用的。这些版本的存在主要是为了语法上的对称性和完整性,但很少需要显式使用。

总结:

分离性:内存分配 (operator new) / 释放 (operator delete) 与对象构造(构造函数) / 析构(析构函数)是分离的。

异常安全:基础版本通过异常报告失败,而 nothrow 版本通过返回 nullptr 报告失败。

通常你不会直接调用它们:你使用 new 和 delete 表达式,编译器会自动为你生成调用这些底层函数的代码。

重载:你可以为你自定义的类重载这些函数,以实现自定义的内存管理策略(例如内存池)。

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

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

方是:

1. mallocfree是函数,newdelete是操作符

2. malloc申请的空间不会初始化,new可以初始化

3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,

如果是多个对象,[]中指定对象个数即可

4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new

要捕获异常

6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new

在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成

空间中资源的清理释放

http://www.dtcms.com/a/393075.html

相关文章:

  • SSM(springboot部分)
  • C++ std:string和Qt的QString有哪些差异?
  • FunASR开源项目实战:解锁语音识别新姿势
  • (华为杯)数学建模比赛编程助手
  • 通义千问对postgresql wire协议的连接和执行SQL过程的解释
  • 钣金折弯机被远程锁机了怎么办
  • 基于陌讯AIGC检测算法的高性能部署实践:FastAPI与多进程并发设计详解
  • 群晖 NAS 远程访问痛点解决:神卓 N600 公网 IP 盒实战体验
  • JavaWeb之HttpServletRequest与HttpServletResponse详解及快递管理系统实践
  • Git详细介绍
  • 大话计算机网络(上)
  • JVM方法调用机制深度解析:从aload_1到invokevirtual的完整旅程
  • STM32CubeIDE学习——安装
  • 追觅宣布进军手机市场,已经白热化的手机赛道追觅优势何在?
  • AI智能体开发工作流的成功案例分享及思路
  • 【算法基础】String、Hash 与 Stack
  • 使用springboot开发一个宿舍管理系统练习项目
  • 像素版推箱子游戏
  • 2025年CSP-J认证 普及组初赛真题解析 CCF信息学奥赛C++ 中小学初级组 第一轮真题-选择题解析
  • 【精品资料鉴赏】121页可编辑PPT详解医药集团合规管控规划方案
  • Linux用户权限与进程管理深度解析
  • [数据结构] 反射,枚举与lambda表达式
  • 奇异值:数据科学的数学基石与应用核心
  • Python 2025:安全编程与漏洞防范实战指南
  • ​​[硬件电路-286]:高速轨到轨比较器TLV3603DCKR 功能概述与管脚定义
  • CAR 细胞疗法:破解自身免疫性疾病的 “免疫纠错” 新路径
  • FreeRTOS实战指南 — 5 多任务系统实现流程
  • `css`使单词保持连贯的两种方法
  • 【Vue3 ✨】Vue3 入门之旅 · 第三篇:模板语法与数据绑定
  • 分类预测 | Matlab实现PCA-BP主成分分析结合BP神经网络多特征分类预测