C++ -内存管理

博客主页:【夜泉_ly】
本文专栏:【C++】
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述

C++ -内存管理

  • C/C++ -内存管理的深入探讨
    • 1. 数据存储分类
      • 1.1 局部数据
      • 1.2 静态数据
      • 1.3 常量数据
      • 1.4 动态申请的数据
    • 2. 内存区域划分
      • 2.1 栈区
      • 2.2 堆区
      • 2.3 静态区/数据段
      • 2.4 常量区/代码段
      • 2.5 相关问题
        • 问题:
        • 答案及详解:
        • 小结
    • 3. 动态内存管理
      • 3.1 使用 malloc 与 new 的对比
      • 3.2 数组的动态分配
      • 3.3 自定义类型的动态分配
        • 说明
      • 小结
    • 4. `operator new` 与 `operator delete`
      • 4.1 基本用法
      • 4.2 `operator new`与`new`、`malloc`的关系
        • 错误处理
        • 关系
        • 对于自定义类型,各函数调用的顺序
    • 5. 定位 `new` 表达式
      • 5.1 使用 `malloc` 与 `new` 的结合
      • 5.2 带参数的构造函数
      • 5.3 手动调用析构函数
      • 5.4 释放内存
      • 5.5 适用场景
        • 内存管理与性能优化
      • 小结
    • 总结

C/C++ -内存管理的深入探讨

内存管理是编程语言中一个重要的主题,尤其在 C 和 C++ 中,开发者需要手动管理内存,以确保程序的高效运行。本文将对 C/C++ 的内存管理进行详细分析,涵盖数据存储分类、内存区域划分、动态内存管理等内容。

1. 数据存储分类

在程序中,需要存储多种数据,根据其用途和存储方式,可以将其分为以下几类:

1.1 局部数据

局部数据通常在函数内部定义,这些变量的生命周期与函数调用的时长直接相关。函数结束时,局部数据将自动被销毁,释放相应的内存空间。
局部数据的特点在于它们的存储位置在栈区,内存的分配和释放由操作系统自动管理,避免了内存泄漏的风险。

1.2 静态数据

静态数据是指在程序的整个运行期间都保持存在的数据。这类数据在程序加载时分配内存,并在程序结束时释放。
静态数据的存储位置在静态区,适合那些需要在多个函数之间共享的信息。

1.3 常量数据

常量数据是指在定义后不允许修改的变量。这类数据通常存储在常量区,用于保护程序中不应被更改的值,如数学常数或配置参数。
使用常量有助于提高代码的可读性和安全性。

1.4 动态申请的数据

动态申请的数据是指在运行时根据需求分配的内存。这些数据存储在堆区,使用后需要手动释放。
动态内存管理提供了灵活性,使程序能够根据实际需求调整内存使用。

2. 内存区域划分

在运行时,操作系统对每个进程的地址空间进行了划分,以满足不同数据的存储需求。具体的内存区域包括:
在这里插入图片描述

2.1 栈区

栈区是用于存储局部变量和函数调用信息的区域,内存的分配和释放是自动进行的。
栈的生长方向是向下的,每次函数调用时会创建一个栈帧,存储函数参数、局部变量以及返回地址。
当函数返回时,栈帧被销毁,所占用的内存被自动释放。
这种机制使得局部变量的管理高效而简便,但也意味着局部数据的生存期受到函数调用的限制。

2.2 堆区

堆区用于动态内存分配,其内存的分配和释放需要开发者手动管理。
与栈区不同,堆的生长方向是向上的,允许程序在运行时根据需要动态地申请内存。
动态内存管理的灵活性是其最大的优点,但同时也需要注意内存的释放,以防止内存泄漏和碎片化。

2.3 静态区/数据段

静态区用于存储静态变量和全局变量,这些变量在程序整个生命周期内保持存在。
由于其在程序启动时分配内存,因此不需要手动管理。

2.4 常量区/代码段

常量区用于存储程序的代码和只读数据。
此区域的数据在程序执行期间不会被修改,确保了程序的稳定性。

2.5 相关问题

void test()
{char charArray[] = "aaaa"; const char* ptr1 = "aaaa";int* ptr2 = (int*)malloc(sizeof(int) * 4);
}
问题:
  1. charArray 在哪儿?
  2. *charArray 在哪儿?
  3. ptr1 在哪儿?
  4. *ptr1 在哪儿?
  5. ptr2 在哪儿?
  6. *ptr2 在哪儿?
答案及详解:
  1. charArray 在哪儿?

    • 答案: 栈
    • 详解: charArray 是一个局部数组,定义在 test 函数内部。局部变量(包括数组)存储在栈区,随着函数调用的开始而创建,在函数结束时自动释放。
  2. *charArray 在哪儿?

    • 答案: 栈
    • 详解: *charArray 表示数组的首元素的地址,它也是一个局部变量。数组的元素存储在栈上,因此 *charArray 的值(即首元素的地址)同样位于栈中。
  3. ptr1 在哪儿?

    • 答案: 栈
    • 详解: ptr1 是一个指向常量字符串的指针,它在 test 函数中定义,因此其存储在栈区。虽然指针 ptr1 指向常量字符串,但指针本身仍然是局部变量,存放在栈上。
  4. *ptr1 在哪儿?

    • 答案: 常量区
    • 详解: *ptr1 是指向常量字符串 “aaaa” 的指针,常量字符串存储在常量区(或代码段)。因此 *ptr1 的值(即指向的字符串)位于常量区。
  5. ptr2 在哪儿?

    • 答案: 栈
    • 详解: ptr2 是一个指向动态分配内存的指针,定义在 test 函数内部,因此其存储在栈区。指针本身是局部变量,存储在栈中。
  6. *ptr2 在哪儿?

    • 答案: 堆
    • 详解: *ptr2 指向通过 malloc 动态分配的内存区域。malloc 函数分配的内存位于堆区,因此 *ptr2 的值(即指向的内存)存储在堆中。
小结

通过上述问题及解析,可以清楚地看到 C/C++ 中不同类型的变量和指针所存储的内存区域。
理解这些概念有助于开发者在进行内存管理时,选择合适的策略,确保程序的高效性和安全性。

补充一点:const修饰的变量只是具有常性(常变量),不代表存储在常量区。
可简单验证:

int main()
{int a;const int b = 0;cout << &a << endl;cout << &b << endl;return 0;
}

运行结果如下图:
在这里插入图片描述
可以发现,变量a、变量b的地址是相邻的,a肯定在栈区,因此可知b也在栈区。

3. 动态内存管理

在 C 语言中,动态内存管理主要依赖于 malloccallocreallocfree 等函数。
而 C++ 在此基础上进行了扩展,提供了 newdelete 操作符,简化了内存管理的复杂性。

3.1 使用 malloc 与 new 的对比

C 语言的动态内存分配需要手动计算所需内存字节数,并强制类型转换:

int* p1 = (int*)malloc(sizeof(int));
free(p1);

而在 C++ 中,使用 new 进行内存分配时,类型转换是自动的,并且会调用构造函数

int* p2 = new int; // 自动处理类型
delete p2; // 释放内存

这种设计简化了内存管理,降低了出错的可能性。

3.2 数组的动态分配

对于需要分配多个元素的情况,C 和 C++ 也有不同的处理方式:

// C
int* p1 = (int*)malloc(sizeof(int) * 10);
free(p1);
// C++
int* p2 = new int[10]; // 直接使用 new
delete[] p2; // 释放数组内存

使用 new 时,C++ 可以调用构造函数并初始化元素,提高了灵活性和安全性。

3.3 自定义类型的动态分配

在 C++ 中,可以使用 new 运算符为自定义类型(如类或结构体)动态分配内存。以下是一个简单的类 Person 示例:

class Person 
{
public:std::string name;int age;Person(std::string n, int a) : name(n), age(a) {std::cout << "Person created: " << name << ", Age: " << age << std::endl;}~Person() {std::cout << "Person destroyed: " << name << std::endl;}
};

可以使用 new 来动态分配一个 Person 对象:

void createPerson() 
{Person* p1 = new Person("Alice", 30); // 动态分配 Person 对象delete p1; // 释放内存
}

对于需要动态分配多个对象的情况,可以使用 new[] 进行数组的动态分配:

void createPersonArray() 
{int size = 3; // 指定 Person 数组的大小// 使用聚合初始化动态分配 Person 对象数组Person* people = new Person[size] {{"Alice", 30},   // 第一个对象{"Bob", 25},     // 第二个对象{"Charlie", 35}  // 第三个对象};// 也可以使用匿名对象初始化// Person* people = new Person[size] {//     Person("Alice", 30), //     Person("Bob", 25), //     Person("Charlie", 35)// };// 输出每个 Person 对象的信息for (int i = 0; i < size; i++) {std::cout << "Person " << i + 1 << ": " << people[i].name << ", Age: " << people[i].age << std::endl;}delete[] people; // 释放动态分配的内存
}
说明
  • 聚合初始化:这种方式可以直接使用初始化列表来为数组中的每个对象赋值。在这个例子中,数组的每个元素都是通过大括号 {} 中的初始化值构造的。
  • 匿名对象初始化:在第二个注释的例子中,使用了匿名对象的方式来初始化,这在某些情况下可能更为清晰,尤其是当需要调用非默认构造函数时。
  • 默认构造函数的注意事项:如果 Person 类没有定义默认构造函数,则在使用数组初始化时,必须确保所有元素都能被完全初始化。否则,编译器会产生错误。

小结

动态内存管理是 C/C++ 编程中的重要主题。通过使用 newdelete,开发者能够灵活地创建和管理对象。合理使用动态分配不仅可以提高程序的性能,还可以避免内存泄漏等问题。

4. operator newoperator delete

在 C++ 中,operator newoperator delete 是内存管理的底层机制,作为全局函数,它们用于动态内存的分配和释放。

4.1 基本用法

使用 operator newoperator delete 的基本语法如下:

int* p1 = (int*)operator new(sizeof(int)); // 使用 operator new
operator delete(p1); // 释放内存
//C:
//int* p1 = (int*)malloc(sizeof(int));
//free(p1);

可以看见,其用法上与mallocfree是一样的。
但是,这两者不仅仅是 mallocfree 的简单替代,而是提供了更复杂的功能,满足面向对象编程的需求。

4.2 operator newnewmalloc的关系

错误处理

面向对象语言错误处理,不喜欢用返回值,更喜欢用抛异常,例如当内存分配失败时, malloc 返回 nullptr,而 new 则会抛出 std::bad_alloc 异常:

int* p1 = (int*)malloc(1024*1024*1024);;
while(p1 != nullptr)p1 = (int*)malloc(1024*1024*1024);
cout << p1 << endl;//malloc失败返回空

运行结果:

00000000
while(1)p1 = new int[1024 * 1024];// 内存分配失败,抛出异常

在这里插入图片描述
捕获异常:

try 
{p1 = new int[10000000000]; // 内存分配失败,抛出异常
} 
catch (const std::exception& e) 
{std::cout << e.what() << std::endl; // 捕获异常并处理
}

运行结果:
在这里插入图片描述

关系

在前面我说明了newmalloc的主要区别是new最后会调用构造函数。
但是,new的第一步还是开空间,而开空间时则用到了malloc
又因为,malloc的错误处理不能满足需求,因此,operator new对其进行了封装:

int* p1 = (int*)operator new(sizeof(int)); // 使用 operator new
operator delete(p1); // 释放内存

在这里插入图片描述
可以看见,operator newmalloc的基础上,添加了抛出异常的功能。

而在new中,直接调用的是operator new

int* ptr = new int;

在这里插入图片描述

对于自定义类型,各函数调用的顺序

自定义一个List类:

class List
{
public:List() :size(0),capacity(0){ptrArray = (int*)malloc(sizeof(int));}~List(){free(ptrArray);ptrArray = NULL;}
private:int* ptrArray;int size;int capacity;
};

然后new/delete

List* ptrlist = new List;
delete ptrlist;

调用顺序如下图:
在这里插入图片描述
在这里插入图片描述

5. 定位 new 表达式

在C++中,new 表达式不仅用于动态分配内存,还允许开发者在特定内存区域内显式调用构造函数。这种用法在需要更细粒度的内存控制时非常有用。以下是相关示例及其解释:

5.1 使用 mallocnew 的结合

在某些情况下,开发者可能会首先使用 malloc 函数分配一块内存,然后使用 new 表达式在这块内存上显式构造对象。下面是如何实现这一点的示例:

A* p1 = (A*)malloc(sizeof(A)); // 使用 malloc 分配内存
new(p1) A; // 在已分配的内存上调用 A 类的默认构造函数

这里,malloc 函数只分配了足够的内存来存放一个 A 类型的对象,但并未调用其构造函数。通过 new(p1) A;,我们显式地在 p1 指向的内存地址上构造了一个 A 类的对象。这种方式可以让开发者控制对象的构造过程,尤其是在处理特定的内存对齐或预分配内存块时非常有用。

5.2 带参数的构造函数

如果 A 类的构造函数需要参数,也可以在 new 表达式中传递这些参数。例如:

new(p1) A(1); // 调用 A 类的构造函数,并传递参数 1

这种写法允许开发者灵活地创建对象,并根据需要初始化其状态。

5.3 手动调用析构函数

需要注意的是,当使用这种方式构造对象时,C++不会自动调用析构函数。因此,在对象不再使用时,必须手动调用析构函数以释放资源:

p1->~A(); // 手动调用 A 类的析构函数

这一步骤是必要的,因为如果不调用析构函数,可能会导致资源泄漏,尤其是当对象管理动态分配的内存或其他系统资源时。

5.4 释放内存

最后,使用 free 函数来释放之前用 malloc 分配的内存:

free(p1); // 释放分配的内存

这种方法确保了在不再需要对象后,能够适当地回收内存资源。

5.5 适用场景

这种使用模式通常用于池化技术:

内存管理与性能优化

在 C/C++ 的内存管理中,动态内存分配的频繁调用可能导致内存碎片化,从而影响程序的运行效率。为了避免这一问题,开发者可以采用对象池(Object Pooling)技术,将相同类型的对象集中管理,从而减少内存的分配和释放次数。

对象池可以预先分配一定数量的对象,并在需要时从池中取出,使用完后再返回到池中。这样不仅减少了内存分配带来的开销,还提高了内存利用率,避免了频繁的堆内存申请。

小结

通过理解和使用 new 表达式的这些特性,开发者可以在C++中更灵活地管理对象的生命周期,从而提高程序的性能和效率。

总结

C/C++ 的内存管理是一个复杂而重要的主题,理解其内存区域的划分、动态内存管理的机制以及异常处理对于编写高效且安全的代码至关重要。通过合理运用内存管理技术和优化策略,开发者可以提升程序的性能,减少内存泄漏和碎片化问题。随着编程语言的发展,内存管理的方式也在不断演进,未来可能会出现更加智能和自动化的内存管理技术。
在这里插入图片描述


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/444683.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML5--裸体回顾

免责声明&#xff1a;本文仅做分享~ 详情请参考以下&#xff1a; HTML 系列教程 (w3school.com.cn) 菜鸟教程 - 学的不仅是技术&#xff0c;更是梦想&#xff01; --本文是光秃秃的空壳. 标题标签 段落标签 换行和水平线 文本格式化标签 &#xff08;一般用左边的&#xff…

抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它能创建一系列相关的对象&#xff0c;而无需指定其具体类&#xff0c;另一种说法是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。它提供了一种创建对象的最…

如何让信息学奥赛学习“边玩边学”?——趣味编程让枯燥学习变得有趣

信息学奥赛&#xff08;NOI&#xff09;作为一项高水平的编程竞赛&#xff0c;内容涉及到大量的算法、数据结构和复杂的逻辑思维&#xff0c;对学生的要求非常高。然而&#xff0c;面对枯燥的知识点和高难度的题目&#xff0c;很多学生在备赛过程中容易感到乏味甚至放弃。那么&…

YOLO11改进|SPPF篇|引入SPPFCSPC金字塔结构

目录 一、【SPPFCSPC】金字塔结构1.1【SPPFCSPC】金字塔结构介绍1.2【SPPFCSPC】核心代码 二、添加【SPPFCSPC】金字塔结构2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、【SPPFCSPC】金字塔结构 1.1【SPPFCSPC】金字塔结构介绍 下图是…

重学SpringBoot3-集成Redis(一)之基础功能

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;一&#xff09;之基础功能 1. 项目初始化2. 配置 Redis3. 配置 Redis 序列化4. 操作 Redis 工具类5. 编写 REST 控制器6. 测试 AP…

思维训练(数论 + 哈希表 + 贪心)

传送门:Dashboard - Codeforces Round 950 (Div. 3) - Codeforces B. Choosing Cubes(排序) Dmitry has n cubes, numbered from left to right from 1 to n. The cube with index f is his favorite. Dmitry threw all the cubes on the table, and the i-th cube showed t…

【HarmonyOS】HMRouter使用详解(四)路由拦截

路由拦截器 可以对指定或全局路由跳转时添加拦截器&#xff0c;作用是可以实现在页面切换前做判断是否有进入当前页面的权限。这篇文章将实现登录的全局路由拦截样式。 新建拦截器类 通过继承IHMInterceptor接口实现生命周期接口的方法重写。 通过添加HMInterceptor装饰器&…

xss之dom类型

目录 xss关于dom类型 1、闭合方式 2、闭合&#xff0c;直接输入1&#xff0c;成功闭合 上我们的pikachu xss关于dom类型 1、闭合方式 输入123&#xff0c;然后打开f12&#xff0c;审查元素&#xff0c;之前一直没有搞懂为什么要在前面加上个单引号 输入两个双引号&#x…

[C语言] 函数详解:库函数与自定义函数

文章目录 函数的概念库函数和自定义函数库函数使用库函数示例常用库函数及头文件 自定义函数自定义函数的基本结构示例&#xff1a;实现两个数的求和函数自定义函数的好处 函数的返回值有返回值的函数无返回值的函数 函数的声明与调用声明函数在另一个文件中调用函数示例&#…

【永磁同步电机(PMSM)】 9. 滑模观测器(SMO)的算法与仿真

【永磁同步电机&#xff08;PMSM&#xff09;】 滑模观测器&#xff08;SMO&#xff09;的算法与仿真 1. 滑模观测器的基本原理2. 滑模观测器的数学模型2.1 PMSM 的数学模型2.2 滑模观测器的设计 3. 基于反正切&#xff08;ATAN&#xff09;的滑模观测器3.1 反正切函数3.2 基于…

使用aloam跑hesai Pandar-XT32激光雷达数据

参考自利用aloam跑数据集_aloam数据集-CSDN博客 第一步&#xff1a;查看bag的信息 输入rosbag info来查看bag包的信息&#xff1a; joeyjoey-Legion-Y7000P-IRX9:~$ rosbag info /home/joey/Downloads/data2022/indoor/LiDAR_IMU.bag path: /home/joey/Downloads/da…

在 Qt 中实现可拖动的无边框 MainWindow 并设置圆角效果

在应用程序的界面设计中,很多时候我们希望窗口能够拥有更好的视觉效果,比如设置圆角以及去除默认的标题栏,使窗口看起来更加美观。此外,还需要支持用户通过鼠标拖动窗口。在本文中,我们将详细介绍如何在 Qt 中实现这些效果。 如图: 一、设置无边框窗口 Qt 提供了 Qt::F…

游离的 HEAD 如何解决

简介 问题描述&#xff1a;使用 IDEA 在提交代码时&#xff0c;禁止提交 如何解决&#xff1a;迁出分支再提交&#xff0c;最后合并到 main 或 master 上 如何解决

kali在git外网的代理

如果发现用git无法直接连接到某些外网项目。可以配置一下代理。 vi /etc/proxychains4.conf 主机可以开一下机场代理&#xff0c;查一下主机的地址和代理所开的端口&#xff0c;我这里是7890 写上代码&#xff1a; socks5 <your ip> <your port> 写上之后wq保…

鸢尾花书实践和知识记录[6-23数据聚类]

文章目录 思维导图数据聚类和引例基于图论的聚类算法算法流程1构造数据构造距离矩阵相似度相似度矩阵创建图 拉普拉斯矩阵标准拉普拉斯矩阵(Combinatorial Laplacian)归一化拉普拉斯矩阵 (Normalized Laplacian)无标度拉普拉斯矩阵 (Signless Laplacian)归一化对称拉普拉斯矩阵…

C++入门基础知识107—【关于C++continue 语句】

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///C爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于C continue 语句的相关内容&#xff01;…

【Node.js】图片水印

上传时加水印 用户上传原始图片->服务器&#xff1a;保留原始图片以及水印图片动态水印 用户上传原始图片->服务器&#xff1a;只保留原始图片 请求图片时&#xff0c;服务器动态加水印 根据业务需求自行更改操作&#xff0c;下面只讲最简单的给图片加水印。 主要使用到…

【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验

前言 在Vue3应用中&#xff0c;用户可能会在一个页面上进行数据编辑&#xff0c;如填写表单或修改表格中的数据。当用户在未保存更改的情况下尝试离开当前页面时&#xff0c;我们希望能够弹出提示框&#xff0c;告知用户有未保存的更改&#xff0c;并询问是否确定离开。 一、使…

MySQL事务日志—redo日志介绍

MySQL事务日志—redo日志 事务有4种特性&#xff1a; 原子性、一致性、隔离性和持久性。 那么事务的四种特性到底是基于什么机制实现? 事务的原子性、一致性由事务的 undo 日志事务的隔离性由锁机制和MVCC实现。事务的持久性由redo 日志来保证。 两类日志概述&#xff1a;…

【大数据】Hive快速入门

文章目录 概述一、Hive的基本概念二、Hive的架构与组件三、Hive的特点与优势四、Hive的应用场景五、Hive的官方网站与资源 驱动说明&#xff08;Driver&#xff09;一、主要功能二、与其他组件的交互三、工作流程四、重要性 元数据模型(Metastore)一、Hive元数据模型的基本概念…