c++ 内存管理一:初识内存分配工具

2023-11-06

前言

侯捷 c++内存管理学习总结笔记。

在C++中,有几种常用的内存分配工具可以帮助进行动态内存管理。

在这里插入图片描述
从c++应用程序自上而下,通常会有这样的几种分配内存的方式,当然最终都是直接或间接的调用系统的API。

1 new 和 delete

newdelete:new操作符用于在堆上分配内存,delete操作符用于释放先前分配的内存。它们是最基本的内存分配工具,在C++中非常常见。

例如,使用new操作符分配单个对象的内存:

string* str= new string("123");

使用delete操作符释放内存:

delete str;

从源码来看string* str= new string("123");,这句代码的动作分为三步:

operator new是一个全局函数,后面会介绍。

1 void* mem = operator new(sizeof(string)); //alloc

2 string* str= static_cast<string*>(mem); //cast

3 pStr->string::string("123");//ctor

出于好奇,我们能否直接使用第三步呢?

class A
{
public:
  int id;
  
  A() : id(0)      { cout << "default ctor. this="  << this << " id=" << id << endl;  }
  A(int i) : id(i) { cout << "ctor. this="  << this << " id=" << id << endl;  }
  ~A()             { cout << "dtor. this="  << this << " id=" << id << endl;  }
};
	
void test_call_ctor_directly()
{

    string* pstr = new string;
    cout << "str= " << *pstr << endl; 
//! pstr->string::string("jjhou");  
                        //[Error] 'class std::basic_string<char>' has no member named 'string'
//! pstr->~string();	//crash -- crash正确, crash 只因上一行被注释
    cout << "str= " << *pstr << endl;


//------------

  	A* pA = new A(1);         	//ctor. this=000307A8 id=1
  	cout << pA->id << endl;   	//1
//!	pA->A::A(3);                //in VC6 : ctor. this=000307A8 id=3
  								//in GCC : [Error] cannot call constructor 'A::A' directly
  								
//!	A::A(5);	  				//in VC6 : ctor. this=0013FF60 id=5
                      			//         dtor. this=0013FF60  	
  								//in GCC : [Error] cannot call constructor 'A::A' directly
  								//         [Note] for a function-style cast, remove the redundant '::A'
		
  	cout << pA->id << endl;   	//in VC6 : 3
  								//in GCC : 1  	
  	
  	delete pA;                	//dtor. this=000307A8 
}

从测试用例来看,直接使用第三步这种方式调用构造,在某些编译器标准中是可行的。

2 new[]和delete[]

new[]delete[]:与new和delete类似,new[]操作符用于在堆上分配数组的内存,delete[]操作符用于释放先前分配的数组内存。

例如,使用new[]操作符分配一个整型数组的内存:

int* arr = new int[10];

使用delete[]操作符释放数组内存:

delete[] arr;

当然这里需要注意的是析构的顺序和构造是相反的,构造是从从下标0 1 2开始, 而析构则是从下标2 1 0开始。

3 operator new

operator new 是 C++ 中的一个操作符和函数,用于在堆上分配内存。它是一个全局的分配函数,可以根据需要进行重载和自定义。

operator new 的基本语法如下:

void* operator new (std::size_t size);

这会分配 size 字节大小的内存,并返回指向分配内存的指针。如果分配失败,operator new 会抛出一个 std::bad_alloc 异常。

以下是一些常见用法:

int* num = new int; // 使用 new 运算符分配一个整数对象的内存

上述代码等效于以下使用 operator new 的操作:

int* num = static_cast<int*>(operator new(sizeof(int))); // 手动调用 operator new 分配内存

需要注意的是,使用 operator new 进行内存分配后,必须手动调用相应的析构函数来销毁对象,并使用 operator delete 进行内存释放。

delete num; // 释放之前由 new 运算符分配的整数对象的内存

对于数组的分配和释放,可以使用 operator new[]operator delete[]

int* arr = new int[10]; // 使用 new 运算符分配一个包含 10 个整数的数组的内存

delete[] arr; // 释放之前由 new 运算符分配的整数数组的内存

同样,上述代码等效于以下使用 operator new[]operator delete[] 的操作:

int* arr = static_cast<int*>(operator new[](10 * sizeof(int))); // 手动调用 operator new[] 分配内存

operator delete[](arr); // 手动调用 operator delete[] 释放内存

需要注意的是,operator newoperator new[] 通常在内部被 newnew[] 运算符隐式调用,我们可以通过重载它们来实现自定义的内存分配行为或内存池的使用。

这里补充一个问题:

operator new源码

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
        {       // try to allocate size bytes
        void *p;
        while ((p = malloc(size)) == 0)
                if (_callnewh(size) == 0)
                {       // report no memory
                static const std::bad_alloc nomem;
                _RAISE(nomem);
                }
 
        return (p);
        }

operator new函数来分配内存。当operator new函数无法分配足够的内存时,会抛出一个std::bad_alloc异常。如果你希望接管这个异常并进行自定义的处理,可以使用std::set_new_handler函数来注册一个自定义的new-handler

下面是一个示例,展示了如何使用std::set_new_handler来接管new操作符的异常处理:

#include <iostream>
#include <new>

// 自定义的new-handler函数
void customNewHandler()
{
    std::cout << "Allocation failed! Custom new-handler called." << std::endl;
    // 这里可以进行一些处理,如释放一些内存资源或者记录日志等
    throw std::bad_alloc(); // 抛出std::bad_alloc异常
}

int main()
{
    std::set_new_handler(customNewHandler); // 注册自定义的new-handler

    try
    {
        int* ptr = operator new int[1000000000000]; // 尝试分配一个非常大的内存块
        // 在正常情况下,当内存分配成功时,这里将会执行
        operator delete[] ptr;
    }
    catch (const std::bad_alloc& e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
        // 这里可以进行进一步的处理,如恢复内存状态或者终止程序等
    }

    return 0;
}

在上述示例中,我们定义了一个名为customNewHandler的自定义new-handler函数。我们通过调用std::set_new_handler将其注册为全局的new-handler。当new操作无法分配内存时,会自动调用该函数。

请注意,这种接管new-handler的方式只会在使用new操作符分配内存时生效,对于直接使用malloc之类的函数是不起作用的。此外,应当谨慎使用自定义的new-handler,确保它能够正确处理内存分配失败的情况,并适当进行异常处理。

4 placement new

“placement new” 是 C++ 中的一个特殊用法,它允许你在提供的内存地址上构造一个对象,通常情况下,使用 “new” 运算符会在堆上分配内存,并在分配的内存上构造对象。而 “placement new” 则允许你在给定的内存地址上进行对象的构造,而无需分配额外的内存。

下面是 “placement new” 的使用方式示例:

#include <iostream>

class MyClass {
public:
  MyClass(int val) : value(val) {
    std::cout << "Constructor called for value: " << value << std::endl;
  }

  ~MyClass() {
    std::cout << "Destructor called for value: " << value << std::endl;
  }

private:
  int value;
};

int main() {
  // 分配一块内存
  void* memory = operator new(sizeof(MyClass));

  // 使用 placement new 在给定的内存地址上构造对象
  MyClass* obj = new (memory) MyClass(42);

  // 使用对象
  std::cout << "Value of the object: " << obj->getValue() << std::endl;

  // 销毁对象,但不会释放内存
  obj->~MyClass();

  // 释放内存
  operator delete(memory);

  return 0;
}

在上述代码中,我们首先通过 operator new 分配了一块内存,然后使用 “placement new” 在该内存上构造了一个 MyClass 对象,并进行使用。最后,通过显式调用析构函数 obj->~MyClass() 销毁对象,然后用 operator delete 释放内存。

从源码中去看这句代码

  // 分配一块内存
  void* buf = operator new(sizeof(MyClass));
  // 使用 placement new 在给定的内存地址上构造对象
  MyClass* obj = new (buf) MyClass(42);

MyClass* obj = new (buf) MyClass(42);可以转换为

 void* mem = operator new(sizeof(MyClass),buf);
 obj  = static_cast<MyClass*>(mem);
 obj->MyClass::MyClass(42);

这里发现operator new(与前面相比多了buf这个参数,这个函数的定义operator new(size_t,void * loc){return loc;}也能找得到。这里`operator new直接return loc是合理的,没有分配内存,因为loc是已经分配好的。

所以不存在placement delete,因为placement new 没有分配内存。当然,有时候我们又想把placement new 对应的operator delete称为placement delete,这都是自己对术语的理解。

这里补充下这两个operator new区别
在 C++ 标准中,operator new 是负责动态分配内存的函数,其函数签名为 void* operator new(std::size_t), 在分配内存时会调用这个函数。而 operator new(size_t, void* loc) 的函数签名是用于 placement new 的,它用于在指定的内存位置构造对象,而不是进行内存分配。所以,这两个函数具有不同的功能和用途,并且无法直接对 operator new(size_t, void* loc) 进行重载。

需要注意的是,使用 “placement new” 时需要手动管理对象的生命周期,包括显式调用析构函数和释放内存。因此,这种用法更加底层和高级,需要确保正确的使用方式以避免内存泄漏和未定义行为。

补充
array new + placement new 示例

class A
{
public:
  int id;
  
  A() : id(0)      { cout << "default ctor. this="  << this << " id=" << id << endl;  }
  A(int i) : id(i) { cout << "ctor. this="  << this << " id=" << id << endl;  }
  ~A()             { cout << "dtor. this="  << this << " id=" << id << endl;  }
};
void main()
{
	A* buf = (A*)(new char[sizeof(A)*size]);
   	A* tmp = buf;   
	   
	cout << "buf=" << buf << "  tmp=" << tmp << endl;	
   	
   	for(int i = 0; i < size; ++i)
	    new (tmp++) A(i);  			//3次 ctor 

	cout << "buf=" << buf << "  tmp=" << tmp << endl;
		    
//!	delete [] buf;    	//crash. why?
						//因为这其实是 char array,看到 delete [] buf;编译器会企图唤起多次 A::~A. 
						// 但 array memory 布局中找不到与 array 元素个數相关的信息, 
						// -- 整个格局都错乱 (从我对 VC 的认识而言),於是崩潰。 
	delete buf;     	//dtor just one time, ~[0]	

	cout << "\n\n";
	
}

从我的理解来看,buf这里是一块内存,假如size是3,我们基于这块内存去创建了A(0)对象,基于buf+1这块内存创建了A(1)对象,基于buf+2这块内存创建了A(2)对象,所以这里并不矛盾,delete[]和new[]要配对,但前提是他们的操作对象是同一个,说白了我们并没有使用new A[3],自然不能delete []. 即使我们间接通过placement new使得这三个对象是连续的。
所以这里我们实际上只开辟了buf指向这个内存,delete buf回收即可。但delete只是释放了A(0),A(1)对象和A(2)对象还在吗?当然是没有了,一切都是基于内存创建的,内存没了,自然就没了。

5 malloc和free

malloc和free:malloc函数用于在堆上分配指定大小的内存,free函数用于释放先前分配的内存。这是C语言中常用的内存分配工具,在C++中也可以使用。

例如,使用malloc函数分配内存:

int* num = (int*)malloc(sizeof(int));

使用free函数释放内存:

free(num);

需要注意的是,使用malloc分配的内存需要强制类型转换为目标指针类型。

malloc是C语言中用于动态内存分配的函数,它的底层实现原理涉及操作系统和C运行时库的内存管理。

  1. 内存分配方式:malloc通过调用操作系统的系统调用(如brkmmap)来获取一段连续的虚拟内存空间。这段内存空间通常是以页为单位进行分配的,一般大小为4KB或更大。操作系统会将这段连续的虚拟内存映射到进程的地址空间。

  2. 内存块管理:为了管理已分配和未分配的内存块,C运行时库会维护一个数据结构(如堆或链表)。这个数据结构用于跟踪可用的内存块和已分配的内存块。当执行malloc时,C运行时库会在内存块中找到一个适合大小的空闲块。

  3. 内存对齐:为了满足特定类型的内存对齐要求,malloc分配的内存块通常会进行对齐处理。对齐是指将内存地址调整为特定的倍数,以保证数据在内存中的存储和访问效率。通常情况下,对齐方式和大小由编译器和操作系统决定。

  4. 内存分配策略:malloc采用了一些内存分配策略来提高性能和空间利用率。例如,它可能会使用不同的内存分区或堆分配算法,如“首次适应”、“最佳适应”或“worst-fit”。这些算法的目标是在满足分配请求的情况下,尽可能地减少内存碎片和提高分配效率。

总体来说,malloc的底层实现是由操作系统和C运行时库共同完成的。操作系统负责分配虚拟内存,而C运行时库负责管理已分配和未分配的内存块。malloc函数的目标是提供简单而高效的内存分配功能,以便程序员能够灵活地进行动态内存管理。后续章节会详细介绍。

6 allocator

在msvc标准中, std::allocator:std::allocator是C++标准库中提供的内存分配器。它是一个泛型类模板,可以用于分配任何类型的内存。

使用std::allocator分配内存:

std::allocator<int> alloc;
int* num = alloc.allocate(1);

使用std::allocator释放内存:

alloc.deallocate(num, 1);

std::allocator提供了更高级的内存管理功能,例如构造和销毁对象,可以在需要时自动调用相应的构造函数和析构函数。当然不同的平台可能接口有所不同,例如 gnu和msvc就有所差异。

在gnu中:

void* p= alloc::allocate(512);
alloc::deallocate(p,512);

除了上述工具,还可以使用自定义的内存分配器来管理内存,通过重载new和delete运算符,或实现自己的内存池等高级技术来实现。这些自定义的工具可以根据特定应用的需求和场景来提供更灵活和高效的内存管理方式。后续章节会详细介绍。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c++ 内存管理一:初识内存分配工具 的相关文章

随机推荐

  • UVa1614

    这道题是一道好题 我想了很久都没有想出合适的方案 这道题考了我们贪心 不确定 数学推导 确定 的能力 看来我的数学逻辑以及推理能力还需要加强啊 题意不说 直接上思路 由于1 lt ai lt i的条件 我们需要从这里入手求解 首先 我们需要
  • Vue判断字符串(或数组)中是否包含某个元素

    Vue判断字符串中是否包含某个字符串 方法有好多种 这里暂时先说我知道的两种 以后知道了别的 会继续更新 方法一 includes方法 数组 字符串都可以 var str Hello World if str includes World
  • 关于多层感知机(MLP)你必须知道的20个知识点

    问题1 MLP的基本组成单元是什么 答 MLP的基本组成单元是神经元 neuron 它通过激活函数对输入进行加权求和和非线性变换 问题2 MLP通常有几层 答 MLP通常有输入层 隐藏层和输出层 隐藏层可以有一层或多层 问题3 MLP的训练
  • 配置ntp客户端与服务器端时间的同步

    1 实验机器介绍 Ip地址 服务器1 192 168 245 128 服务器2 192 168 245 130 客户端1 192 168 245 129 实验前准备 在ntpS1 和ntpS2 中 配置外部服务器为同步服务器 并开放给192
  • 前端内存泄漏和溢出的情况以及解决办法

    写在前面 在平时写代码时 内存泄漏的情况会时有发生 虽然js有内存回收机制 但在平时编程中还是需要注意避免内存泄漏的情况 前几天做移动端时遇到一个内存泄漏造成移动端页面卡顿的问题 所以想总结下前端内存泄漏的情况 回顾下基础知识 一 什么是内
  • RPM 的 spec 文件如何编写

    在关于 RPM 软件包构建的上一篇文章中 你了解到了源 RPM 包括软件的源代码以及 spec 文件 这篇文章深入研究了 spec 文件 该文件中包含了有关如何构建 RPM 的指令 同样 本文以 fpaste 为例 了解源代码 在开始编写
  • 全球及中国汽车用导航行业应用前景与销售渠道分析报告2022-2028年

    全球及中国汽车用导航行业应用前景与销售渠道分析报告2022 2028年 修订日期 2022年1月 专员对接 周文文 查询鸿晟信合研究院了解详细内容 第一章 汽车用导航产业概述 1 1 汽车用导航定义及产品技术参数 1 2 汽车用导航分类 1
  • Idea:修改新项目默认设置

    修改Idea新项目默认设置 使用idea开发时 即使在settings设置项目配置 如maven 在新建项目也会发现项目配置变为默认 这时需要设置新建项目配置 配置步骤 File New Projects Setup Settings fo
  • Log4j 配置文件(log4j.properties)的所在路径问题

    一般我们直接将log4j properties放置在src目录下 这样系统自动会找到的 其实就是放在WEB INF classes文件下 这个路径在classpath下 所以直接就能找到 我们写Logger的时候如下 public clas
  • Servlet配置、会话管理——Servlet【总结】

    Servlet配置 初始化参数 Servlet除了可以从请求信息中获取信息外 还可以从配置文件中获取配置参数信息 配置文件中的参数信息与具体的请求无关 而是Servlet初始化时调用的 如此可以避免硬编码信息 提高Servlet的可移植性
  • 创建的源文件后缀不是.c,在一些编译器上不能编译

    问题描述 源文件的名字和后缀写的比较随意 后缀不是 c 代码没有语法高亮 可能在一些编译器上不能编译通过 现象 解决办法 C语言代码中我们有约定 源文件的后缀是 c 头文件的后缀是 h 这样在大部分的IDE中代码打开都是有语法高亮的
  • 想学软件开发做程序员,学习Python必备的Python从入门到精通约650GB全面学习资料

    先简单介绍一下Python的情况 后面文章最后附有零基础自学Python从入门到精通学习Python的视频文档源码低阶到高阶等全面学习资料约650GB 1 什么是PythonPython 是一个有条理的和强大的面向对象的程序设计语言 类似于
  • 使用 Selenium 和 Python 爬取股票网站历史资金数据的简易教程

    一 需求及技术介绍 在金融投资领域 了解股票市场的历史资金数据对于制定投资策略和做出明智的决策至关重要 这些数据包含着股票的交易量 资金流向 持股比例等关键指标 能够为投资者提供有价值的参考和分析依据 通过利用 Selenium 模拟浏览器
  • Debug-CDK编译

    问题描述 make No rule to make target xxx c needed by Obj xxx o Stop 解决方法 删掉obj文件夹 重新编译
  • 【网络基础】通俗易懂的了解HTTPS的整体过程

    文章目录 前言 HTTP 各个版本 结构 请求 请求行 请求头 请求体 响应 响应行 响应头 响应体 HTTPS 对称加密 非对称加密 对称和非对称结合加密 CA证书中级大发 证书 对称和非对称结合加密 证书关系链 拓展知识 证书的合法性验
  • STM32的常规芯片容量大小以及 大、小容量STM32芯片之间的差别?

    本文主要讨论STM32的常规芯片容量大小以及 STM32芯片大 小容量之间的差别 STM32大容量芯片是包括高达512K字节的闪存和64K字节的SRAM的芯片为大容量 STM32小容量芯片具体容量见下表 STM32F103xC STM32F
  • nginx+tomcat集群+https

    nginx tomcat集群 一 nginx安装 1 安装nginx需要的依赖包 yum y install gcc zlib zlib devel pcre devel openssl openssl devel 2 下载nginx安装包
  • (附源码)ssm考试题库管理系统 毕业设计 069043

    SSM考试题库管理系统 摘 要 随着计算机办公自动化程度的不断提高 开发各种数据库管理应用软件用于各种工作中能有效地提高工作效率 节省时间 能使学校的教学工作上一个新的台阶 传统的人工命题形成试卷 往往会出现大量的重复劳动 并且形成的试卷因
  • FTP局域网内文件共享

    FTP局域网内文件共享 由于最近公司电脑主机不让插U盘拷贝数据 所以导致在打包软件进行上机测试的时候需要使用聊天软件来进行传输 相对比较麻烦 所以决定使用FTP在局域网内来进行文件的共享 记录一下 一起学习吧 操作步骤目录 共享文件夹设置
  • c++ 内存管理一:初识内存分配工具

    文章目录 前言 1 new 和 delete 2 new 和delete 3 operator new 4 placement new 5 malloc和free 6 allocator 前言 侯捷 c 内存管理学习总结笔记 在C 中 有几