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

这篇具有很好参考价值的文章主要介绍了c++ 内存管理一:初识内存分配工具。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

在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运算符,或实现自己的内存池等高级技术来实现。这些自定义的工具可以根据特定应用的需求和场景来提供更灵活和高效的内存管理方式。后续章节会详细介绍。文章来源地址https://www.toymoban.com/news/detail-514436.html

到了这里,关于c++ 内存管理一:初识内存分配工具的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++初阶】内存管理 && 初识模板

    C/C++的内存分布主要分为 栈区、堆区、数据段和代码段,还有内存映射段。 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

    2024年02月04日
    浏览(36)
  • C++基础(三) —— 内存分配

    主要采用链表结构 使用了一个名叫page的结构体管理物理内存,结构体中包括了页的大小、页的状态以及指向相邻页的指针。 Linux内核使用这些指针来构建了一个逻辑链表,当需要分配内存的时候,会从链表中查找第一个空闲页并把它标记为已使用。 释放内存的时候,会把相

    2024年02月08日
    浏览(28)
  • C++中内存的分配

    一个由C/C++编译的程序占用的内存分为以下几个部分     1、栈区(stack)—   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等。   2、堆区(heap)   —   一般由程序员分配释放,   若程序员不释放,程序结束时可能由OS回收。    3、全局区(静态区)

    2024年02月10日
    浏览(28)
  • C++ 指针进阶:动态分配内存

    malloc 是 stdlib.h 库中的函数,原型为 void *__cdecl malloc(size_t _Size); : 作用 : malloc 函数沿空闲链表(位于内存 堆空间 中)申请一块满足需求的内存块,将所需大小的内存块分配给用户剩下的返回到链表上; 并返回指向该内存区的首地址的指针,意该指针的类型为 void * ,因此

    2024年02月05日
    浏览(29)
  • C++类和动态内存分配

    C++能够在程序运行时决定内存的分配,而不是只在编译阶段,因此,就可以根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存,C++使用new和delete运算符来动态控制内存,但是,在类中使用这些运算符会导致许多新的问题,在这种情况下,析构函数就是必不可

    2024年04月16日
    浏览(32)
  • 【大数据】Flink 内存管理(四):TaskManager 内存分配(实战篇)

    《 Flink 内存管理 》系列(已完结),共包含以下 4 篇文章: Flink 内存管理(一):设置 Flink 进程内存 Flink 内存管理(二):JobManager 内存分配(含实际计算案例) Flink 内存管理(三):TaskManager 内存分配(理论篇) Flink 内存管理(四):TaskManager 内存分配(实战篇) 😊

    2024年03月13日
    浏览(39)
  • 从 malloc 分配大块内存失败 来简看 linux 内存管理

    应用进程 malloc 返回了null,但是观察到的os 的free内存还有较大的余量 ,很奇怪为什么会这样? 不可能是oom导致的(当然也没有 os 的oom 日志),free还有余量,系统也没有cgroup的应用隔离。 我们linux上使用的库函数 malloc 基本都是用glibc库实现的malloc函数(当然如果binary 链接

    2024年02月08日
    浏览(37)
  • 【C\C++】内存分配 和 动态内存管理方式

    如上图所示:在C/C++中,有几个重要的内存区域,每个区域都有不同的意义和用途。我们从内存分配的角度来分析C++各个内存区域的含义: 栈(Stack) :栈是用于 存储 局部变量、函数参数以及函数调用信息 的内存区域 。它的特点是 自动分配和释放 ,并且遵循 后进先出的原

    2024年02月08日
    浏览(33)
  • C++ 类的内存分配是怎么样的?

    首先通过一段代码来引入动态内存分配的主题。一个名为 StringBad 的类以及一个功能更强大的 String 类。 介绍一下这些定义,第一个char指针来表示一段字符串,这就意味着类声明没有为字符串本身分配存储空间。而是要在构造函数里通过new来为字符串分配空间,这就避免了在

    2024年03月24日
    浏览(25)
  • 【Linux 内核源码分析】内存管理——Slab 分配器

    在Linux内核中,伙伴分配器是一种内存管理方式,以页为单位进行内存的管理和分配。但是在内核中,经常会面临结构体内存分配问题,而这些结构体的大小通常是小于一页的。如果使用伙伴分配器来分配这些小内存,将造成很大的内存浪费。因此,为了解决这个问题,Sun公

    2024年02月22日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包