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

C++中的内存管理

学完了类与对象,这节我们来了解一下内存里的那些事

文章目录

一、C/C++中的内存分布

1. 常量区(代码段) (Text Segment)

2. 静态区(数据段) (Data Segment)

3. 堆区 (Heap)

4. 栈区 (Stack)

5. 内存映射区域 (Memory-mapped Region)

6. 堆栈溢出

 内存布局总结

重要细节:

二、C/C++的内存管理方式

        C语言中的内存管理

        C++中的内存管理

new/delete操作内置类型

        new和delete操作自定义类型

        operator new与operator delete函数

new/delete的实现原理

对于内置类型:

对于自定义类型:

定位new表达式(placement-new)

三、malloc/free和new/delete的区别

1. 使用的语言

2. 内存分配和释放方式

3. 类型安全

4. 构造和析构函数

5. 内存分配失败

6. 效率

7. 使用习惯


前言

在C语言解答我们就已经学习了内存里面的那些东西,我们也会使用动态内存的一些函数,但是那些函数都是在C语言中所使用的,现在我们学习C++了,也要学习一下C++中动态内存的一些东西。


一、C/C++中的内存分布

在我们学习如果管理内存之前,我们首先要知道内存中有什么东西,内存中的东西是如何分布的。在 C/C++ 中,程序的内存分布是指程序在运行时,操作系统如何将不同类型的数据存储在内存的不同区域。通常,程序的内存可以分为以下几个主要部分:

1. 常量区(代码段) (Text Segment)

  • 内容:包含程序的可执行代码。
  • 特点:此区域通常是只读的,防止程序意外地修改其指令。操作系统会将其加载到内存中,并且可能会共享同一程序实例的多个进程。

2. 静态区(数据段) (Data Segment)

  • 内容:用于存储程序中的全局变量、静态变量(即全程存在的变量),以及它们的初始值。
  • 分为两部分
    • 已初始化数据段:存储已初始化的全局和静态变量。
    • 未初始化数据段 (BSS段):存储未初始化的全局和静态变量。BSS段的变量会在程序启动时被操作系统自动初始化为 0。

3. 堆区 (Heap)

  • 内容:用于动态分配内存。由程序员通过 malloccallocrealloc 等函数(在 C 语言中)或 new 操作符(在 C++ 中)分配内存。
  • 特点:堆是一个可以动态扩展的区域,程序员需要手动管理内存的分配与释放(通过 freedelete 等)。如果堆内存不及时释放,会导致内存泄漏。

4. 栈区 (Stack)

  • 内容:用于存储局部变量、函数参数、返回地址等。每当调用一个函数时,栈会为该函数分配空间,当函数执行完毕时,空间被释放。
  • 特点
    • 先进后出:栈的特点是后进先出(LIFO)。每次函数调用会压栈,函数返回会出栈。
    • 局部变量存储:局部变量通常存储在栈上,当函数退出时,它们的内存会自动释放。

5. 内存映射区域 (Memory-mapped Region)

  • 内容:主要用于加载共享库、内存映射文件等。这个区域由操作系统管理,可以包括动态链接库、共享内存、I/O 映射等。
  • 特点:这些区域的内容通常由操作系统管理和控制,程序可以直接访问,但不直接由程序员管理。

6. 堆栈溢出

  • 内容:当栈的空间不足时,可能发生栈溢出。一般来说,栈空间的大小是有限的。
  • 表现:栈溢出可能导致程序崩溃,常见原因包括无限递归或者分配过多的局部变量。

 内存布局总结

区域内容生命周期管理方式
栈区局部变量、函数参数、返回地址函数调用时分配,调用结束时销毁自动管理
堆区动态分配的内存由程序员手动管理程序员手动管理
数据区全局变量、静态变量、常量程序运行期间持续存在自动管理
文本区程序的代码(机器码)程序运行时一直存在操作系统加载

内存布局大致如下所示

+------------------------+  高地址
|   内存映射区域        |
+------------------------+
|   堆                   |
+------------------------+
|   BSS段(未初始化数据) |
+------------------------+
|   数据段(已初始化数据)|
+------------------------+
|   代码段(Text段)     |
+------------------------+  低地址
|   栈                   |
+------------------------+

重要细节:

  • 栈和堆的增长方向:通常情况下,栈的增长方向是向下(即地址从高到低),而堆的增长方向是向上(即地址从低到高)。因此,当栈和堆相遇时,可能会导致“栈溢出”或“堆溢出”。
  • 内存管理:在堆上分配的内存需要程序员显式地管理,在栈上分配的内存由编译器自动管理。

光说概念理念啥的还是太悬了,我们还是用具体实例来看看吧

二、C/C++的内存管理方式

C语言中的内存管理

首先我们先来回顾一下我们之前所学习的C语言中的动态内存管理方式:malloc,calloc,realloc,free.

	//1.malloc//void *malloc(size_t size);//size表示的是我们要申请空间中的元素的大小,由于malloc函数在库中的数据类型是void*,所以我们每次使用都要进行强转int* p1 = (int*)malloc(sizeof(int));//2.calloc//void* calloc(size_t num, size_t size); //num表示要分配的元素个数,size表示的是每个元素的大小,由于calloc函数在库中的数据类型是void*,所以我们每次使用都要进行强转int* p2 = (int*)calloc(4, sizeof(int));//3.realloc//void* realloc(void* ptr, size_t new_size);//ptr指向的是已经分配内存的空间地址,new_size表示要重新分配的内存大小(单位是字节),同样地,它每次使用时也要使用强转int* p3 = (int*)realloc(p2, sizeof(int)*2);//4.free//void free(void *ptr);//ptr表示的是想要释放的内存空间地址,直接传递一个指针名字即可free(p1);free(p2);free(p3);

我们从上面的代码和注释,我们可以了解到上面三种函数都是申请空间的函数,最后一种函数是用来对动态申请的内存进行释放,最后一个函数在动态内存管理中是一个不可或缺的,如果我们只是一味地去申请空间,而不去释放空间,就很容易造成内存泄漏的问题。

现在我们对上面那三种动态申请内存的函数:malloc/calloc/reaclloc进行一下辨析。我们只从单词拼写来看,上面三个单词是十分相似的,仅个别单词的差别,但是它们的作用大同小异。malloc函数是从堆区主动为对象分配一块内存连续的空间,并返回该内存空间的起始地址。使用malloc函数动态申请的空间是不会进行初始化的,也就是说它所申请的空间里的内容是一个随机值,使用malloc函数时我们要始终记得NULL检查,防止返回一个NULL指针造成申请空间失败我们却发现不了。calloc函数也是一个动态内存申请的函数,它的功能和malloc函数是十分相似的,但是它有两点不同:1.它会指定动态申请的元素个数;2.它会将动态申请的内容全部初始化为0。有时候在某些情况calloc函数进行动态内存申请还是很好用的。最后是realloc函数,这个函数与前面那两个函数的功能有所差异,前面两个函数是直接申请空间,而这个函数是为一个对象重新申请并分配空间。reaclloc可以扩展或缩小已分配内存的大小。如果扩展内存,原有数据保持不变,新增内存会被清零;如果缩小内存,数据保持不变。当 reaclloc成功时,它可能会在内存的不同位置分配新的空间,因此必须将返回的指针保存到一个新的变量中,否则原指针可能会丢失。

C++中的内存管理

C语言的内存管理方式在C++中其实也是可以用的,但是在某些方面使用起来有点麻烦,于是C++就使用自己内存管理的方式:使用new/delete操作符进行内存管理。

new/delete操作内置类型

	// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[10];//动态申请3个int类型的空间并进行初始化int* ptr7 = new int[3] {1, 2, 3};delete ptr4;delete ptr5;delete[] ptr6;delete[]ptr7;

我们从上面的代码可以看到我们使用C++中的new操作符,只要new 数据类型(参数)就可以动态申请一个指定类型的空间并且可以帮我们初始化为任意值(若要初始化一个空间的内容,我们在()内传递我们要初始化的值,若要初始化一个数组空间的内容,我们在{ }内传递我们想要初始化的值)。对比于C语言中的malloc/calloc,它们书写的方式麻烦(需要进行强转,给其传递要开辟的空间字节大小,还不能够随意初始化为自己想要的值),C++中的new操作符就清楚明白多了。至于delete操作符,它对于一个空间的释放以及一个数组空间的释放是有一些差别的,希望我们能够注意。

new和delete操作自定义类型


class A
{
public://写了A类的构造函数(带缺省参数)A(int a=10 ): _a(a){cout << "A():" << this << endl;}//A类的析构函数~A(){cout << "~A():" << this << endl;}
private:int _a;
};
int main()
{// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间//使用自定义类型来动态申请内存,malloc,new都可以申请。但是malloc只能够开空间,new不仅开空间而且还会调用A类的构造函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;cout << endl;// 内置类型是几乎是一样的(因为内置类型没有构造函数,只要开辟空间即可)int* p3 = (int*)malloc(sizeof(int)); // int* p4 = new int;free(p3);delete p4;cout << endl;//同样地使用自定义类型动态申请数组内存空间,new不仅开空间还会调用构造函数中参数的缺省值进行初始化,如果构造函数中没有默认的缺省值,则无法进行初始化A* p5 = (A*)malloc(sizeof(A) * 10);A* p6 = new A[10];free(p5);delete[] p6;return 0;
}

由上面的代码以及运行结果我们可以看出来,对于那些自定义类型的动态内存管理,malloc/new,free/delete 都是可以的,但是对于那些自定义类型,我们在之前就已经学习过了类中有属于自己的默认构造函数/析构函数,malloc/free虽然可以申请释放空间,但是它们不能够调用到自定义类型的构造/析构函数。而在C++中,那些研发出new/delete的大佬们明显发现了这件事情,对于一个自定义类型怎么能够不调用它们的构造函数与析构函数呢,于是它们就将这些功能打包给了new/delete了。

operator new与operator delete函数

我们在上面已经说过了,new/delete就是malloc/free的一个升级版,那么它们为什么如此相像呢?学到这里不知道你们有没有这样的疑问。难道它们都是为了内存管理,C++研发者为了图简单,就照着malloc/free随便设计两个操作符就当作C++专属的呢。其实new/delete的底层就是malloc/free。new/delete它们两个是操作符,它们是对一个变量进行一个操作,真正发挥作用的是在编译器里面的两个全局函数:operator new和operator delete。每次我们使用操作符new/delete,它们就悄悄调用上面两个函数。而这两个函数的本质就是malloc/free,在C++中将其进行封装变成了两个新的函数,但是还是有一点点的小差别的:malloc如果申请空间失败就会返回NULL,而operator new 申请空间失败就会抛出异常,而这也是面向对象语言的一个经典特点。上面这两个函数只是实现开辟空间与释放空间的功能,并没有调用构造函数/析构函数的功能。

/*
*new/delete 是两个操作符,它们是通过调用全局域中的两个函数operator new / operator delete 来进行动态内存管理的
*而上面那两个函数本身又是我们之前C语言中的malloc与free的一个封装展现,,那两个函数单独使用和malloc与free的功能基本差不多
* 不过operator delete在内存释放失败时会抛出异常,而free则会返回一个NULL指针,这是一个差异
* new:   1.调用operator new来动态申请空间   2.调用构造函数进行初始化
* delete:1.调用析构函数,完成对象内存中资源的清理   2.调用operator delete来动态释放空间
* 至于new T[N]  ,  delete[]就是上面那个原理重复N次
*/class A
{
public:A(int a = 10):_a(a){cout << "A(int a ):" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;
};int main()
{A* p1 = new A(10);p1->~A();cout << endl;// 这里我们调用operator new 即调用malloc, 因此我们的使用方式也和malloc一样,要先进行强转,然后在传要创建空间的大小A* p3 = (A*)operator new (sizeof(A));new(p3)A;//这一步是定位new表达式,我们是为了手动调出p3的构造函数//上面的两步合在一块就是一个new所实现的功能了p3->~A();//这是手动调用析构函数operator delete(p3);cout << endl;A* p2 = new A;p2->~A();return 0;
}

从上面的代码我们可以看出来,单单使用operator new/operator delete仅仅是起到malloc/free的功能,如果想要调用构造函数/析构函数就需要我们自己手动去调用了。所以我们就索性直接使用new/delete这两个操作符,想要的功能它们都会实现。

new/delete的实现原理

对于内置类型:

它们的实现原理和malloc/free一样,虽然new/delete调用的是上面那两个函数,但是那两个函数的本质还是malloc/free。不同的是new/delete类型是申请一个类型的内存空间,new[]/delete[]是申请连续的内存空间,malloc申请失败时会返回NULL,而new申请失败时则会抛出异常。

对于自定义类型:

new的原理:1.调用operator new函数来动态申请内存空间;2.调用构造函数进行初始化。

delete的原理:1.调用析构函数对对象资源的清理;2.调用operator delete函数来释放内存空间。

newT[N]的原理:1.调用operator new[]函数的调用,在operator new[]中实际调用operator new N次完成对N个对象的空间申请;2.调用N次构造函数进行对N个对象进行初始化

delete[ ]的原理:1.在释放空间上进行N次析构函数的调用,完成对N个对象的资源清理;2.调用operator delete[ ]函数来释放内存,实际上是调用operator delete来释放内存。

定位new表达式(placement-new)

placement new 是 C++ 中的一种特殊形式的 new 表达式,它允许用户在指定的内存位置上构造对象,而不是在堆上动态分配内存。与常规的 new 表达式不同,placement new 不会分配内存,而是将对象构造在一个已经存在的内存区域中。

语法:

new (pointer) Type(args);
  • pointer 是指向预分配内存的指针,必须传递指针。
  • Type 是要构造的对象类型。
  • args 是传递给构造函数的参数,即类型的初始化列表

使用场景: 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

int main()
{char* buffer[sizeof(int)];  // 预分配一块内存,给这个字符数组分配整型字节的大小空间// 在 buffer 上使用placement new构造一个int对象int* ptr = new (buffer) int(42);  // 在buffer的位置上构造int对象并初始化为42,buffer是数组名,也是数组首地址,即指针cout << *ptr << endl;  // 输出42return 0;
}

同样地对于自定义类型的变量使用定位new,它同样能够发挥new操作符的作用(调用构造函数初始化,申请空间)但是它是指定内存空间进行分配内存空间,并不是从堆中分配内存空间,这就是定位new的功能。

三、malloc/free和new/delete的区别

1. 使用的语言

  • malloc 和 free 是 C 语言中的内存管理函数。
  • new 和 delete 是 C++ 中的内存管理操作符。

2. 内存分配和释放方式

  • malloc:用来从堆中分配一块指定大小的内存。返回的是一个 void* 指针,需要进行类型转换才能使用。它只关心分配的字节数,不会调用构造函数。
  • free:用来释放 malloc 分配的内存块,它接受一个指针,释放对应的内存区域。
  • new:用来为对象分配内存并调用构造函数。它不仅分配内存,还返回一个指向已构造对象的指针。
  • delete:用来释放通过 new 分配的内存,并自动调用对象的析构函数。

3. 类型安全

  • malloc/freemalloc 返回的是 void*,需要强制转换为目标类型的指针,存在类型不安全的风险。free 的参数是 void*,可以接收任何类型的指针。
  • new/deletenew 和 delete 是类型安全的,new 返回所需类型的指针,不需要类型转换。delete 的参数也是相应类型的指针。

4. 构造和析构函数

  • malloc/freemalloc 只是简单地分配内存,不会调用对象的构造函数。free 也不会调用对象的析构函数。
  • new/deletenew 会调用对象的构造函数,delete 会调用析构函数,确保对象的资源能够被正确清理。

5. 内存分配失败

  • malloc:如果 malloc 无法分配所需的内存,它会返回 NULL
  • newnew 如果无法分配内存,会抛出 std::bad_alloc 异常(除非使用 new(std::nothrow),此时会返回 nullptr)。

6. 效率

  • malloc/free:在 C 语言中,malloc 和 free 主要用于手动管理内存,不会涉及对象的构造和析构,因此相对较低级和直接。
  • new/deletenew 和 delete 由 C++ 编译器实现,通常会做额外的检查(比如调用构造/析构函数、内存池管理等),可能比 malloc 和 free 稍微慢一些,但它们与对象的生命周期管理是紧密结合的。

7. 使用习惯

  • malloc/free:这些函数更常见于 C 语言中,或者在与 C 兼容的 C++ 代码中使用。
  • new/delete:这两个操作符是 C++ 推荐的内存管理方式,它们与 C++ 的面向对象特性(如构造、析构)兼容。

http://www.lryc.cn/news/530267.html

相关文章:

  • MySQL为什么默认引擎是InnoDB ?
  • ComfyUI安装调用DeepSeek——DeepSeek多模态之图形模型安装问题解决(ComfyUI-Janus-Pro)
  • 电脑要使用cuda需要进行什么配置
  • 利用Muduo库实现简单且健壮的Echo服务器
  • Scratch 《像素战场》系列综合游戏:像素战场游戏Ⅰ~Ⅲ 介绍
  • Android学习制作app(ESP8266-01S连接-简单制作)
  • 三甲医院大型生信服务器多配置方案剖析与应用(2024版)
  • 【Unity3D】实现横版2D游戏——单向平台(简易版)
  • 大白话讲清楚embedding原理
  • 电脑优化大师-解决电脑卡顿问题
  • el-table组件样式如何二次修改?
  • java练习(1)
  • UbuntuWindows双系统安装
  • DeepSeek大模型技术深度解析:揭开Transformer架构的神秘面纱
  • MusicFree-开源的第三方音乐在线播放和下载工具, 支持歌单导入[对标落雪音乐]
  • Versal - 基础4(VD100+Versal IBERT)
  • vue2和vue3路由封装及区别
  • Windows 系统下使用 Ollama 离线部署 DeepSeek - R1 模型指南
  • 性能测试网络风险诊断有哪些?
  • 八股文 (一)
  • TVM调度原语完全指南:从入门到微架构级优化
  • c语言进阶(简单的函数 数组 指针 预处理 文件 结构体)
  • 终极版已激活!绿话纯净,打开即用!!!
  • Vue.js 什么是 Composition API?
  • MySQL高可用
  • 30.Word:设计并制作新年贺卡以及标签【30】
  • Flink2支持提交StreamGraph到Flink集群
  • 大模型本地化部署(Ollama + Open-WebUI)
  • C++哈希(链地址法)(二)详解
  • IME关于输入法横屏全屏显示问题-Android14