C++内存管理机制(侯捷)笔记1

news2024/12/25 0:34:49

C++内存管理机制(侯捷)

本文是学习笔记,仅供个人学习使用。如有侵权,请联系删除。

参考链接

Youtube: 侯捷-C++内存管理机制

Github课程视频、PPT和源代码: https://github.com/ZachL1/Bilibili-plus

第一讲primitives的笔记
截至2024年1月10日17点09分,花费一天的时间完成《C++内存管理——从平地到万丈高楼》第一讲的学习,主要学习了new delete, array new,placement new,以及allocator的几个设计版本。

文章目录

  • C++内存管理机制(侯捷)
    • 2 内存分配的每一层面
    • 3 四个层面的基本用法
    • 4 基本构件之一new delete expression上
    • 5 基本构件之一new delete expression中
    • 6 基本构件之一new delete expression下
    • 7 Array new
    • 8 placement new
    • 9 重载
    • 10 重载示例(上)
    • 11 重载示例(下)
    • 12 Per class allocator
    • 13 Per class allocator 2
    • 14 Static allocator
    • 15 Macro for static allocator
    • 16 New Handler
    • 后记

2 内存分配的每一层面

C++的 applications可以调用STL,里面会有allocator进行内存分配;也可以使用C++ 基本工具primitives,比如new, new[], new(), ::operator new();还可以使用更底层的malloc和free分配和释放内存。最底层的是系统调用,比如HeapAlloc,VirtualAlloc等。

在这里插入图片描述

HeapAlloc,VirtualAlloc的介绍

HeapAllocVirtualAlloc 是 Windows 操作系统中用于内存分配的两个重要的系统调用。

  1. HeapAlloc:

    • 描述: HeapAlloc 用于在指定的堆中分配一块指定大小的内存。

    • 参数:

      • hHeap: 指定要分配内存的堆的句柄。
      • dwFlags: 分配选项,例如是否可以共享内存等。
      • dwBytes: 要分配的内存大小,以字节为单位。
    • 返回值: 成功时返回分配内存块的指针,失败时返回 NULL

    • 示例:

      HANDLE hHeap = GetProcessHeap();
      LPVOID lpMemory = HeapAlloc(hHeap, 0, 1024); // 在默认堆中分配 1024 字节的内存
      
  2. VirtualAlloc:

    • 描述: VirtualAlloc 用于在进程的虚拟地址空间中分配、保留或提交内存。

    • 参数:

      • lpAddress: 指定欲分配的内存的首选地址。可以指定为 NULL,系统会自动选择合适的地址。
      • dwSize: 要分配的内存大小,以字节为单位。
      • flAllocationType: 分配类型,例如是保留、提交还是同时进行。
      • flProtect: 内存保护选项,指定内存区域的访问权限。
    • 返回值: 成功时返回分配内存块的起始地址,失败时返回 NULL

    • 示例:

      LPVOID lpMemory = VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
      

这两个系统调用都是用于在程序运行时动态分配内存。HeapAlloc 通常与堆相关联,而 VirtualAlloc 则更为底层,允许更多的灵活性,例如手动指定内存地址,控制内存保护选项等。在实际使用中,选择使用哪个取决于具体的需求和使用场景。

C++ memory primitives介绍

在这里插入图片描述

malloc,free和new delete的区别

mallocfreenewdelete 是在C和C++中用于内存管理的操作符和函数。它们之间有一些重要的区别:

  1. mallocfree(C语言):

    • malloc 用于在堆上分配一块指定大小的内存,返回分配内存的首地址。

      int *ptr = (int *)malloc(sizeof(int));
      
    • free 用于释放先前由 malloc 分配的内存。

      free(ptr);
      
    • 在C中,mallocfree 是标准库函数,不涉及构造函数和析构函数,只是简单的内存分配和释放。

  2. newdelete(C++语言):

    • new 用于在堆上分配一块指定大小的内存,并调用对象的构造函数来初始化对象。

      int *ptr = new int;
      
    • delete 用于释放由 new 分配的内存,并调用对象的析构函数。

      delete ptr;
      
    • 在C++中,newdelete 不仅仅是内存分配和释放的操作符,还会处理对象的构造和析构,因此它们更适用于处理对象。

总的来说,主要区别包括:

  • mallocfree 是C语言标准库函数,适用于内存分配和释放,不涉及构造和析构。
  • newdelete 是C++语言中的操作符,用于动态对象的创建和销毁,涉及构造和析构。
  • 在C++中,不应该混合使用 malloc/freenew/delete,因为它们有不同的内存管理方式和处理构造析构的机制。如果你在C++中使用 new,应该使用相应的 delete 来释放内存,而不是 free

3 四个层面的基本用法

对malloc,new,allocator进行测试

#include <iostream>
#include <complex>
#include <memory>				 //std::allocator  
#include <ext\pool_allocator.h>	 //欲使用 std::allocator 以外的 allocator, 就得自行 #include <ext/...> 
namespace jj01
{
void test_primitives()
{
	cout << "\ntest_primitives().......... \n";
	
    void* p1 = malloc(512);	//512 bytes
    free(p1);

    complex<int>* p2 = new complex<int>; //one object
    delete p2;             

    void* p3 = ::operator new(512); //512 bytes
    ::operator delete(p3);

//以下使用 C++ 標準庫提供的 allocators。
//其接口雖有標準規格,但實現廠商並未完全遵守;下面三者形式略異。
#ifdef _MSC_VER
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.
    int* p4 = allocator<int>().allocate(3, (int*)0); 
    allocator<int>().deallocate(p4,3);           
#endif
#ifdef __BORLANDC__
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 5 個 ints.
    int* p4 = allocator<int>().allocate(5);  
    allocator<int>().deallocate(p4,5);       
#endif
#ifdef __GNUC__
    //以下兩函數都是 static,可通過全名調用之。以下分配 512 bytes.
    //void* p4 = alloc::allocate(512); 
    //alloc::deallocate(p4,512);   
    
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 7 個 ints.    
	void* p4 = allocator<int>().allocate(7); 
    allocator<int>().deallocate((int*)p4,7);     
	
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 9 個 ints.	
	void* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9); 
    __gnu_cxx::__pool_alloc<int>().deallocate((int*)p5,9);	
#endif
}	
} //namespace

复习C++的语法:临时对象调用成员函数的用法

int* p4 = allocator<int>().allocate(5);  
allocator<int>().deallocate(p4,5);    

这行C++代码涉及到模板、临时对象以及动态内存分配,下面是对该行代码的详细介绍:

int* p4 = allocator<int>().allocate(5);
  1. allocator<int>()

    • allocator 是 C++ 标准库中的分配器类,用于分配和释放内存。
    • <int> 是模板参数,指定了分配器要操作的类型为 int
    • allocator<int>() 创建了一个临时的 allocator 对象。这是一个匿名对象,即在一行代码内创建并使用,不会被显式地命名。这样的对象通常用于执行一些简短的任务而不需要长时间存活。
  2. .allocate(5)

    • allocateallocator 类的成员函数,用于分配指定数量的未初始化内存块,并返回指向第一个元素的指针。
    • 5 是要分配的元素数量。
  3. int* p4

    • int* 声明了一个指向整数的指针。
    • p4 是指针变量的名称。

综合起来,该行代码的作用是使用 allocator<int> 创建了一个临时的分配器对象,并调用其 allocate 函数分配了大小为 5 * sizeof(int) 字节的内存块。然后,将分配的内存块的首地址赋给 int* p4,使 p4 成为该内存块的指针。请注意,这里的内存块中的元素并没有被初始化,如果需要使用这些元素,必须进行相应的初始化操作。这样的代码在某些特殊场景下,例如需要手动管理内存的情况,可能会用到。

对#ifdef后面的_MSC_VER__BORLANDC____GNUC__进行解释:

这段代码使用了条件编译指令 #ifdef,根据不同的编译器选择性地编译不同的代码块。以下是对这些宏的解释:

  1. #ifdef _MSC_VER

    • _MSC_VER 是 Microsoft Visual Studio 编译器的宏,它的值对应于编译器的版本号。
    • 如果代码正在使用 Microsoft Visual Studio 编译器编译,那么这个条件为真,相关的代码块会被编译。
    • 在这个条件下,使用 allocator<int> 对象分配和释放内存。
  2. #ifdef __BORLANDC__

    • __BORLANDC__ 是 Borland 编译器的宏,用于检查是否正在使用 Borland 编译器。
    • 如果代码正在使用 Borland 编译器编译,那么这个条件为真,相关的代码块会被编译。
    • 在这个条件下,使用 allocator<int> 对象分配和释放内存。
  3. #ifdef __GNUC__

    • __GNUC__ 是 GNU 编译器(例如,gcc 和 g++)的宏。
    • 如果代码正在使用 GNU 编译器编译,那么这个条件为真,相关的代码块会被编译。
    • 在这个条件下,有两个不同的分配和释放内存的方式:
      • 一个使用 allocator<int> 对象。
      • 另一个使用 GNU C++ 库中的 __gnu_cxx::__pool_alloc<int>() 对象。

总体来说,这段代码在不同的编译环境下,使用不同的方式进行内存分配和释放。这样的条件编译可以使代码适应不同的编译器和环境。

下图中的__GNUC__的版本是2.9,它里面的分配器叫做alloc

在这里插入图片描述

下图是__GNUC__版本4.9的分配器,叫做__gnu_cxx::__pool_alloc指的是上面的alloc分配器

在这里插入图片描述

4 基本构件之一new delete expression上

new expression

new的过程:调用operator new分配内存(内部调用mlloc函数),指针转型,然后调用构造函数。

下图中右上角operator new的实现,这是vc98版本的源代码,里面调用malloc函数,如果malloc无法分配内存,就一直在while循环中,它调用_callnewh,这是一个new handler用于处理内存失败的情况,比如释放一部分内存等。

_callnewh 不是 C++ 标准中的函数,而是可能是用户定义的一个函数。通常情况下,这类函数的名字以 _new_handler 结尾,用于处理内存分配失败的情况。

在 C++ 中,当 new 表达式无法分配所需的内存时,会调用用户指定的 new_handler 函数。new_handler 是一个函数指针,指向一个用户定义的函数,其原型通常为:

typedef void (*new_handler)();

这个函数可以尝试释放内存、扩大内存池,或者执行其他操作来尝试解决内存不足的问题。如果 new_handler 能够成功处理内存不足的情况,它返回;如果不能处理,它可以选择抛出异常或者终止程序。

在这里插入图片描述

当使用 new 运算符创建对象时,整个过程包括以下几个步骤:

  1. 调用 operator new 分配内存

    • operator new 是一个用于动态内存分配的函数,它负责分配指定大小的内存块,并返回该内存块的指针。在内部,operator new 可能调用标准库的 malloc 或其他分配函数来完成实际的内存分配。
    • 例如:
      void* rawMemory = operator new(sizeof(MyClass));
      
  2. 指针转型

    • 分配得到的原始内存的指针是 void* 类型的,因为 operator new 并不知道具体分配的是什么类型的对象。因此,需要将 void* 转换为所需类型的指针。
    • 例如:
      MyClass* objPtr = static_cast<MyClass*>(rawMemory);
      
  3. 调用构造函数

    • 通过转型后的指针,调用对象的构造函数进行初始化。这是为了确保对象在分配内存后能够执行必要的初始化操作。

    • 例如:

      new (objPtr) MyClass(/* constructor arguments */);
      
    • 上述语法使用了定位 new (placement new)运算符,它将在指定的内存位置调用构造函数。这允许我们在已分配的内存中构造对象,而不是在默认构造函数中执行内存分配。

综合来说,使用 new 运算符的过程涉及内存分配、指针类型转换和构造函数的调用,确保对象能够正确地初始化。在对象的生命周期结束时,还需要通过 delete 运算符释放内存,并调用对象的析构函数。

继续补充定位new运算符:

定位 new 运算符是 C++ 中的一种特殊形式的内存分配运算符,用于在指定的内存位置上创建对象。它的语法如下:

new (pointer) Type(initializer);

其中:

  • pointer 是指定的内存地址,通常是一个指针。
  • Type 是要创建的对象的类型。
  • initializer 是可选的构造函数参数,用于初始化对象。

定位 new 主要用于以下情况:

  1. 在预分配的内存块中创建对象

    void* memory = operator new(sizeof(MyClass)); // 分配内存
    MyClass* obj = new (memory) MyClass(/* constructor arguments */); // 在指定内存位置创建对象
    
  2. 在数组中创建对象

    void* memory = operator new[](sizeof(MyClass) * 5); // 分配数组内存
    MyClass* objArray = new (memory) MyClass[5]; // 在数组内存中创建对象
    
  3. 用于自定义的内存管理
    定位 new 允许程序员有更多的控制权,可以通过指定特定的内存地址,使用自定义的内存管理策略。

需要特别注意的是,使用定位 new 运算符后,必须手动调用对象的析构函数来释放资源,否则可能导致内存泄漏。示例如下:

obj->~MyClass(); // 手动调用析构函数
operator delete(memory); // 手动释放内存

在现代 C++ 中,为了避免手动管理内存和调用析构函数,更推荐使用智能指针、标准容器以及 RAII(资源获取即初始化)等方式,以提高代码的可维护性和安全性。

5 基本构件之一new delete expression中

delete的动作:先调用析构函数,然后释放内存。

operator delete里面调用free。

在这里插入图片描述

6 基本构件之一new delete expression下

构造函数ctor和析构函数dtor直接调用

在这里插入图片描述

下面显示不能直接调用构造函数,而只有编译器会进行隐式调用。调用时在vc6编译通过,在GCC中编译失败。

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 'jj02::A::A' directly
  								
//!	A::A(5);	  				//in VC6 : ctor. this=0013FF60 id=5
                      			//         dtor. this=0013FF60  	
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly
  								//         [Note] for a function-style cast, remove the redundant '::A'

测试代码如下:

#include <iostream>
#include <string>
//#include <memory>				 //std::allocator  

namespace jj02
{
	
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()
{
	cout << "\ntest_call_ctor_directly().......... \n";	
	
    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 只因為上一行被 remark 起來嘛.  
    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 'jj02::A::A' directly
  								
//!	A::A(5);	  				//in VC6 : ctor. this=0013FF60 id=5
                      			//         dtor. this=0013FF60  	
  								//in GCC : [Error] cannot call constructor 'jj02::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 

  	//simulate new
  	void* p = ::operator new(sizeof(A));  
  	cout << "p=" << p << endl; 	//p=000307A8
  	pA = static_cast<A*>(p);
//!	pA->A::A(2);				//in VC6 : ctor. this=000307A8 id=2
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly  	
  	
  	cout << pA->id << endl;     //in VC6 : 2
  								//in GCC : 0  	

  	//simulate delete
  	pA->~A();					//dtor. this=000307A8 
  	::operator delete(pA);		//free()
}	
} //namespace

7 Array new

Complex* pca = new Complex[3]; // 调用三次构造函数
delete[] pca; // 唤起三次析构函数, 这是正确的

string* psa = new string[3];
delete psa; // 唤起一次析构函数,这是错误的

对于 delete[] pca;,编译器会通过运行时信息来确定需要释放的内存块的数量,因为在动态数组分配时,会在数组的前面(通常)记录数组的大小信息。这个信息通常被称为“cookie”或“size”,它使得 delete[] 知道要调用多少次析构函数,并释放相应数量的内存。这种信息是由编译器自动管理的。

但是,当你使用 delete pca; 尝试删除一个使用 new[] 分配的数组时,会导致未定义的行为,因为编译器可能无法正确获取数组的大小信息。这样的操作可能会导致内存泄漏和未能正确调用对象的析构函数。因此,为了正确的内存管理,应该始终使用相匹配的 newdeletenew[]delete[] 运算符。

在这里插入图片描述

下面右侧的图是new int[10]的内存布局,其中灰色的表示具体的数据,橙色的是debug模式下添加的内存,最上面和最下面的两个0x61(61H)是cookie,记录整体内存分配的大小。61H实际上是60H,表示内存分配的大小,后面1H意思是占用最后一位,表示内存分配出去。浅蓝色的pad表示补齐,填补到16的倍数。

在这里插入图片描述

下面的Demo中有3个int型的成员变量,所以每个Demo对象的大小是12B(每个int型是 4B)

在这里插入图片描述

8 placement new

placement new允许我们将对象建构在allocated memory中。

#include<new>
char* buf = new char[sizeof(Complex) * 3]; // 已经分配了内存
Complex* pc = new(buf)Complex(1, 2); // 把上面分配的内存位置传进来

delete[] buf;

其中Complex* pc = new(buf)Complex(1, 2);这句话会被编译器转换为下图中的1,2,3三行,分别调用operator new(和上文看到的不同,这里需要第二个参数,表示位置,这个函数只是传回这个位置,不再分配内存),指针转型,调用构造函数。

这种用法被称为 “placement new”,它允许程序员在指定的内存位置上创建对象。这通常用于特殊的内存管理场景,例如在预分配的内存池中创建对象。

在这里插入图片描述

9 重载

C++应用程序,分配内存的途径

一个类里面可重载operator new 和 operator delete,从而改变内存的分配机制

在这里插入图片描述

C++容器分配内存的途径

容器会走分配器,分配器会调用::operator new和::operator delete,底层也是调用malloc和free。

在这里插入图片描述

在 C++ 中,容器使用分配器(Allocator)来进行内存分配和释放。分配器是负责管理容器内部元素内存的组件。下面是容器分配内存的一般途径:

  1. 容器使用分配器

    • C++ 容器(如 std::vectorstd::liststd::map 等)通常使用分配器来分配和释放内存。分配器是容器的一部分,负责处理元素的内存分配和释放操作。
  2. 分配器调用 ::operator new::operator delete

    • 分配器的实现通常会调用全局作用域下的 ::operator new 来分配内存,并在需要释放内存时调用 ::operator delete
    • ::operator new::operator delete 是 C++ 中的全局内存分配和释放函数。它们底层可能调用标准库的 mallocfree
  3. 底层可能调用 mallocfree

    • mallocfree 是 C 标准库中的内存分配和释放函数,用于分配和释放原始的、未构造的内存块。C++ 的 ::operator new::operator delete 可能在底层调用这些函数。

总体来说,C++ 容器使用分配器来管理内存,而分配器可能在其实现中调用 ::operator new::operator delete,从而涉及到底层的内存分配函数 mallocfree。这种设计允许用户自定义容器的内存管理行为,以适应不同的需求。用户可以通过提供自定义分配器来实现特定的内存分配策略。

重载全局的::operator new 和::operator delete

由于是全局的函数,这个影响很大。

在这里插入图片描述

在一个类中重载operator new和operator delete

类内部重载之后,编译器会来调用

在这里插入图片描述

同样的道理,也可以重载operator new[] 和operator delete[]

在这里插入图片描述

10 重载示例(上)

类内部重载上述4种操作的接口如下所示:

在这里插入图片描述

测试上述一个类中重载new,delete, new[], delete[]的用法

下图右侧可以看到,在GNU C++4.9版本中构造是从上到下,析构是从下到上。

在这里插入图片描述

下面是使用全局new和delete的测试

在这里插入图片描述

11 重载示例(下)

placement new的重载,new() 和 delete()

在这里插入图片描述

placement new的重载第一参数必须是size_t类型

在这里插入图片描述

对于如何区分operator new 和 placement new,就要看调用的时候怎么用了。在调用时,编译器会根据传递给 new 表达式的参数来匹配适当的重载版本。

在这里插入图片描述

测试代码

#include <vector>  //for test

namespace jj07
{

class Bad { };
class Foo
{
public:
  	Foo() { cout << "Foo::Foo()" << endl;  }
  	Foo(int) { 
	   			cout << "Foo::Foo(int)" << endl;  
	           	// throw Bad();  
			 }

  	//(1) 這個就是一般的 operator new() 的重載 
  	void* operator new(size_t size) {
		cout << "operator new(size_t size), size= " << size << endl;
    	return malloc(size);  
  	}

  	//(2) 這個就是標準庫已經提供的 placement new() 的重載 (形式)
	//    (所以我也模擬 standard placement new 的動作, just return ptr) 
  	void* operator new(size_t size, void* start) { 
	  	cout << "operator new(size_t size, void* start), size= " << size << "  start= " << start << endl;
    	return start;
  	}

  	//(3) 這個才是嶄新的 placement new 
  	void* operator new(size_t size, long extra) { 
	  	cout << "operator new(size_t size, long extra)  " << size << ' ' << extra << endl;
    	return malloc(size+extra);
  	}

  	//(4) 這又是一個 placement new 
  	void* operator new(size_t size, long extra, char init) { 
	  	cout << "operator new(size_t size, long extra, char init)  " << size << ' ' << extra << ' ' << init << endl;
    	return malloc(size+extra);
  	}
  	
   	//(5) 這又是一個 placement new, 但故意寫錯第一參數的 type (它必須是 size_t 以滿足正常的 operator new) 
//!  	void* operator new(long extra, char init) { //[Error] 'operator new' takes type 'size_t' ('unsigned int') as first parameter [-fpermissive]
//!	  	cout << "op-new(long,char)" << endl;
//!    	return malloc(extra);
//!  	} 	

    //以下是搭配上述 placement new 的各個 called placement delete. 
	//當 ctor 發出異常,這兒對應的 operator (placement) delete 就會被喚起. 
	//應該是要負責釋放其搭檔兄弟 (placement new) 分配所得的 memory.  
  	//(1) 這個就是一般的 operator delete() 的重載 
  	void operator delete(void*,size_t)
  	{ cout << "operator delete(void*,size_t)  " << endl;  }

	//(2) 這是對應上述的 (2)  
  	void operator delete(void*,void*)
  	{ cout << "operator delete(void*,void*)  " << endl;  }

	//(3) 這是對應上述的 (3)  
  	void operator delete(void*,long)
  	{ cout << "operator delete(void*,long)  " << endl;  }

	//(4) 這是對應上述的 (4)  
	//如果沒有一一對應, 也不會有任何編譯報錯 
  	void operator delete(void*,long,char)
  	{ cout << "operator delete(void*,long,char)  " << endl; }
  	
private:
  	int m_i;
};


//-------------	
void test_overload_placement_new()
{
	cout << "\n\n\ntest_overload_placement_new().......... \n";
	
  	Foo start;  //Foo::Foo

  	Foo* p1 = new Foo;           //op-new(size_t)
  	Foo* p2 = new (&start) Foo;  //op-new(size_t,void*)
  	Foo* p3 = new (100) Foo;     //op-new(size_t,long)
  	Foo* p4 = new (100,'a') Foo; //op-new(size_t,long,char)

  	Foo* p5 = new (100) Foo(1);     //op-new(size_t,long)  op-del(void*,long)
  	Foo* p6 = new (100,'a') Foo(1); //
  	Foo* p7 = new (&start) Foo(1);  //
  	Foo* p8 = new Foo(1);           //
  	//VC6 warning C4291: 'void *__cdecl Foo::operator new(unsigned int)'
  	//no matching operator delete found; memory will not be freed if
  	//initialization throws an exception
}
} //namespace	

basic_string使用new(extra)扩充申请量

重载了operator new,其实是placement new。因为用法为new(extra) Rep;

在这里插入图片描述

12 Per class allocator

内存池是一种用于管理和分配内存的机制,它可以提高内存分配的效率,减少内存碎片,并降低动态内存分配的开销。在 C++ 中,内存池通常通过重载 operator newoperator delete 来实现。

下面简要描述一下内存池的概念,并提供一个简单的示意图:

  1. 内存池概念

    • 内存池是一块预先分配的内存区域,它被划分为多个小块,每个小块可以被分配给程序使用。
    • 内存池通常由一个或多个链表、堆栈或其他数据结构来管理,以追踪哪些内存块是空闲的,哪些是已分配的。
    • 内存池的目的是减少因频繁的内存分配和释放而引起的性能开销。
  2. 示意图

    +------------------------------------+
    |            Memory Pool             |
    +------------------------------------+
    |   Free Block 1   |   Free Block 2  |
    +------------------+-----------------+
    |   Allocated Block 1                |
    +------------------------------------+
    |   Free Block 3   |   Free Block 4  |
    +------------------+-----------------+
    
    • 上面的示意图展示了一个简单的内存池,其中包含多个内存块,有一些是空闲的,有一些是已经分配给程序使用的。
    • 每个内存块的大小可能不同,取决于内存池的设计。
    • 空闲的内存块可以通过链表或其他数据结构连接在一起,以便快速分配。
  3. 内存池的操作

    • 当程序需要分配内存时,内存池会从空闲块中选择一个合适的块分配给程序。
    • 当程序释放内存时,将相应的内存块标记为空闲,并重新加入空闲块链表,以便下次分配使用。
  4. 自定义内存池的示例

    class MemoryPool {
    private:
        struct Block {
            size_t size;
            Block* next;
        };
    
        Block* freeList;
    
    public:
        MemoryPool(size_t poolSize) {
            // 初始化内存池
            void* memory = ::operator new(poolSize);
            freeList = static_cast<Block*>(memory);
            freeList->size = poolSize;
            freeList->next = nullptr;
        }
    
        void* allocate(size_t size) {
            // 从内存池中分配内存
            if (!freeList || freeList->size < size) {
                // 内存不足,或者没有空闲块,可以根据实际情况扩展内存池
                return nullptr;
            }
    
            Block* allocatedBlock = freeList;
            freeList = freeList->next;
            return static_cast<void*>(allocatedBlock);
        }
    
        void deallocate(void* ptr) {
            // 释放内存到内存池
            Block* block = static_cast<Block*>(ptr);
            block->next = freeList;
            freeList = block;
        }
    };
    

    上述示例为了简洁,省略了一些内存池的管理细节,实际的内存池实现可能需要更复杂的数据结构和算法。

引入内存池的考量:

很多new要分配内存,用malloc分配一大块(内存池),然后分成小块,减少malloc的调用次数。

另外,想要减少cookie的用量。

下面是对类Screen进行内存设置的例子,这里是设计的第一版本

在Screen类中引入一个指针next,它的大小是4B,用于串联链表。如下图所示

再看一下delete操作,把指针p回收到单向链表中,放到链表的头指针位置

在这里插入图片描述

看看怎么使用自定义的内存分配模式:

如下图右侧所示,左边间隔8表示每个Screen对象内存分配的大小为8B,说明每个Screen分配的时候没有cookie。

右边间隔16,表示每个Screen对象内存分配的大小为16B,这是因为对象分配的时候上下加了cookie,最上面和最下面的cookie大小共为8B。

在这里插入图片描述

下面是测试代码

#include <cstddef>
#include <iostream>
namespace jj04
{
//ref. C++Primer 3/e, p.765
//per-class allocator 

class Screen {
public:
    Screen(int x) : i(x) { };
    int get() { return i; }

    void* operator new(size_t);
    void  operator delete(void*, size_t);	//(2)
//! void  operator delete(void*);			//(1) 二擇一. 若(1)(2)並存,會有很奇怪的報錯 (摸不著頭緒) 
	    
private:
    Screen* next;
    static Screen* freeStore;
    static const int screenChunk;
private:
    int i;
};
Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;

void* Screen::operator new(size_t size)
{
  Screen *p;
  if (!freeStore) {
      //linked list 是空的,所以攫取一大塊 memory
      //以下呼叫的是 global operator new
      size_t chunk = screenChunk * size;
      freeStore = p =
         reinterpret_cast<Screen*>(new char[chunk]);
      //將分配得來的一大塊 memory 當做 linked list 般小塊小塊串接起來
      for (; p != &freeStore[screenChunk-1]; ++p)
          p->next = p+1;
      p->next = 0;
  }
  p = freeStore;
  freeStore = freeStore->next;
  return p;
}


//! void Screen::operator delete(void *p)		//(1)
void Screen::operator delete(void *p, size_t)	//(2)二擇一 
{
  //將 deleted object 收回插入 free list 前端
  (static_cast<Screen*>(p))->next = freeStore;
  freeStore = static_cast<Screen*>(p);
}

//-------------
void test_per_class_allocator_1()
{	
	cout << "\ntest_per_class_allocator_1().......... \n";	
		
   	cout << sizeof(Screen) << endl;		//8	

size_t const N = 100;
Screen* p[N];	

   	for (int i=0; i< N; ++i)
   	     p[i] = new Screen(i);         

	//輸出前 10 個 pointers, 用以比較其間隔 
   	for (int i=0; i< 10; ++i)  	   
		cout << p[i] << endl;     
    
   	for (int i=0; i< N; ++i)
   	     delete p[i];     	
}
} //namespace

13 Per class allocator 2

设计类里面的allocator的第二版本

这里和第一版本的最大不同是设计上采用union

在C++中,union 是一种特殊的数据结构,允许在相同的内存位置存储不同类型的对象。它的每个成员共享相同的内存空间,只能同时使用一个成员。union 提供了一种有效利用内存的方式。

struct AirplaneRep {
	unsigned long miles;  // 4B
	char type;  // 1B
    // 由于对齐,这5B会变成8B
};
union {
	AirplaneRep rep;
	Airplane* next;
}

在这里插入图片描述

下面是重载operator delete,然后是测试结果

间隔8和间隔16解释同allocator, 1。

在这里插入图片描述

14 Static allocator

下面是内存分配的第三版本

从软件工程的角度看,上面的operator new和operator delete对于不同 类都要重载,明显不是一个好的解法,下面是将allocator抽象成一个类。

allocator类中定义allocate和deallocate函数,用于分配和回收。

下图中右侧是具体的实现,这里每次分配CHUNK个大小的一大块,然后切割成小块,用链表串起来。

在这里插入图片描述

具体的类进行内存分配的时候,只需要调用allocator即可

在这里插入图片描述

具体的测试如下:由于上面的CHUNK设置为5,可以看到下图右侧部分,每5个对象的内存空间是连续的(间隔都是一个对象的大小),而每个大块之间是不连续的。

在这里插入图片描述

15 Macro for static allocator

下面是类分配内存的第四个版本:使用macro。

把allocator的部分拿出来用宏来定义

在C++中,宏(macro)是一种预处理指令,用于在编译过程中执行文本替换。宏通常通过 #define 关键字定义,并在代码中通过宏名称来调用。它们是一种简单的文本替换机制,可以用于创建常量、函数替代、条件编译等。

在宏定义的末尾使用反斜杠是为了告诉编译器该宏定义将在下一行继续。如果在宏定义的最后一行没有使用反斜杠,那么编译器会认为宏定义结束了。

在这里插入图片描述

标准库中的allocator

其中一种分配器有16条自由链表,来应对不同大小的块分配,不同的大小的类对象,分配到不同的链表中。

在这里插入图片描述

16 New Handler

new handler

new handler 是一个与 C++ 内存分配和 new 操作符相关的概念。它是一个函数指针,指向一个用户定义的函数,该函数负责处理 new 操作符无法满足内存分配请求时的情况。

new 操作符无法分配所需的内存时,它会调用与之关联的 new handlernew handler 可以执行一些操作,例如释放一些已分配的内存、尝试扩展堆的大小、选择性地抛出异常,或者执行其他用户定义的操作。

使用 set_new_handler 函数设置 new handler

#include <new>
#include <iostream>

void customNewHandler() {
    std::cerr << "Memory allocation failed! Custom new handler called." << std::endl;
    std::terminate();  // 终止程序或者执行其他处理
}

int main() {
    std::set_new_handler(customNewHandler);

    // 尝试分配大块内存
    int* arr = new int[1000000000000];  // 如果分配失败,会调用 customNewHandler

    return 0;
}

在上述示例中,通过 set_new_handler 函数设置了一个自定义的 new handler,即 customNewHandler。当 new 操作符在尝试分配非常大的内存块时失败,会调用这个自定义的 new handler

注意事项:

  • new handler 是全局的,一旦设置,会在程序的生命周期内一直有效,直到被其他 set_new_handler 覆盖。
  • 如果 new handler 返回,new 操作符会再次尝试分配内存,如果还失败,则再次调用 new handler。这个过程会一直重复,直到 new handler 抛出异常或者不返回(例如调用 std::terminate())。
  • 在 C++11 及以后的版本中,可以使用 std::get_new_handler 获取当前的 new handler,以便在需要时进行保存和恢复。

使用 new handler 可以提供一些灵活性,允许程序员在内存分配失败的情况下采取定制的操作,而不是默认的行为(即抛出 std::bad_alloc 异常)。

在这里插入图片描述

set_new_handler的例子

在这里插入图片描述

在C++中,=default=delete 是两个特殊的关键字,用于指定和控制类的特殊成员函数的生成和使用。

  1. =default

    • =default 用于显式要求编译器生成默认实现的特殊成员函数(默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符、析构函数)。
    • 通过 =default,可以确保编译器生成的函数符合默认行为,并且可以通过显式声明而不定义的方式,将这些函数声明为“使用默认实现”。
    • 示例:
      class MyClass {
      public:
          // 使用默认实现的特殊成员函数
          MyClass() = default;  // 默认构造函数
          MyClass(const MyClass&) = default;  // 复制构造函数
          MyClass(MyClass&&) = default;  // 移动构造函数
          MyClass& operator=(const MyClass&) = default;  // 复制赋值运算符
          MyClass& operator=(MyClass&&) = default;  // 移动赋值运算符
          ~MyClass() = default;  // 析构函数
      };
      
  2. =delete

    • =delete 用于删除特殊成员函数或其他函数的默认实现,使得在某些情况下,编译器将会拒绝生成默认实现。

    • 通过 =delete,可以防止某些函数的调用,或者阻止生成某些函数。

    • 示例:

      class NoCopyClass {
      public:
          NoCopyClass() = default;  // 默认构造函数
      
          // 禁止复制构造和复制赋值
          NoCopyClass(const NoCopyClass&) = delete;
          NoCopyClass& operator=(const NoCopyClass&) = delete;
      };
      
    • 在上述示例中,通过 =delete 明确禁止了复制构造和复制赋值,使得这个类无法被复制。

这两个关键字的使用使得代码更加明确,能够更好地表达程序员的意图,并且在一些情况下,可以帮助编译器进行更好的优化。=default=delete 主要用于提高代码的清晰性、安全性和性能。

下图中的一句话:=default,=delete可以用于operator new/ new[], operator delete/ delete[]以及它们的重载。

在这里插入图片描述

对operator new/ new[], operator delete/ delete[]的测试:使用=default, = delete

在这里插入图片描述

后记

花费一天的时间完成《C++内存管理——从平地到万丈高楼》第一讲的学习,对C++内存分配有了浅显的认识,希望跟着侯捷老师后续的讲解,逐渐加深对C++内存分配的理解。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

N卡可以点亮但是A卡无法点亮故障解决记录

目录 关键词平台说明一、故障现象二、排查过程三、根本原因四、措施3.1进入boot后更改CSM启动为下图所示。3.2更改PCIE自动为3.0 关键词 英伟达、AMD、显卡、无法点亮 平台说明 项目Value主板铭瑄 TZZ_H61ITX 2.5GCPU12400f显卡RX6600 RTX4060附加设备PCIE 延长线–显卡 一…

C#入门篇(一)

变量 顾名思义就是变化的容器&#xff0c;即可以用来存放各种不同类型数值的一个容器 折叠代码 第一步&#xff1a;#region 第二步&#xff1a;按tab键 14种数据类型 有符号的数据类型 sbyte&#xff1a;-128~127 short&#xff1a;-32768~32767 int&#xff1a;-21亿多~21亿多…

尝试OmniverseFarm的最基础操作

目标 尝试OmniverseFarm的最基础操作。本地机器作为Queue和Agent&#xff0c;同时在本地提交任务。 主要参考了官方文档&#xff1a; Farm Queue — Omniverse Farm latest documentation Farm Agent — Omniverse Farm latest documentation Farm Examples — Omniverse Far…

微机原理常考简答题总结

一&#xff0c;8086和8088这两个微处理器在结构上有什么异同&#xff1f; &#xff08;1&#xff09;共同点&#xff1a;内部均由EU、BIU组成&#xff0c;结构基本相同&#xff1b;寄存器等功能部件均为16位&#xff1b;内部数据通路为16位&#xff1b;指令系统相同。 &#x…

让测试人头疼的web自动化之验证码识别彻底解决方案

验证码识别解决方案 对于web应用程序来讲&#xff0c;处于安全性考虑&#xff0c;在登录的时候&#xff0c;都会设置验证码&#xff0c;验证码的类型种类繁多&#xff0c;有图片中辨别数字字母的&#xff0c;有点击图片中指定的文字的&#xff0c;也有算术计算结果的&#xff…

vsCode输出控制台中文乱码解决

在tasks.json里的args中添加 "-fexec-charsetGBK", // 处理mingw中文编码问题 "-finput-charsetUTF-8",// 处理mingw中文编码问题

现代操作系统复习笔记【核心考点知识+重点复习题】

文章目录 一、核心考点基础知识第一章 概述1、操作系统的基本概念、基本功能2、分时系统、批处理系统、实时系统的主要特征3、用户接口、系统调用过程4、单到与多道程序技术5、操作系统虚拟机体系结构6、CPU工作模式&#xff1b;7、部分课后习题 第二章 进程与线程1、进程的基本…

Tsmaster使用笔记整理

选择厂商 根据你所选择的CAN分析仪的厂商&#xff0c;确定你的厂商设备设置。 我一般会选择PEAK&#xff0c;和 ZLG多一点&#xff0c;其他的没有用过。除了上图中的&#xff0c;市面上的CAN分析仪还有CANanlyst、广成科技、创芯科技等&#xff0c;但它们都不能在Tsmaster上使…

如何利用 NFTScan Portfolio 功能分析钱包 NFT 持仓

随着 NFT 市场的扩大和投资者的增加&#xff0c;追踪和管理大量 NFT 资产正变得越来越复杂&#xff0c;无论是新手还是资深投资者&#xff0c;都需要借助实时的 NFT 数据作为判断依据。因此&#xff0c;一个能够全面分析 NFT 钱包持仓的工具就显得尤为重要。帮助投资者掌握自身…

Linux C/C++ 显示NIC流量统计信息

NIC流量统计信息是由操作系统维护的。当数据包通过NIC传输时&#xff0c;操作系统会更新相关的计数器。这些计数器记录了数据包的发送和接收数量、字节数等。通过读取这些计数器&#xff0c;我们可以获得关于网络流量的信息。 为什么需要这些信息? 可以使用这些信息来监控网络…

Linux的网络服务DHCP

一.了解DHCP服务 1.1 DHCP定义 DHCP&#xff08;动态主机配置协议&#xff09;是一个局域网的网络协议。指的是由服务器控制一段IP地址范围&#xff0c;客户机登录服务器时就可以自动获得服务器分配的IP地址和子网掩码。默认情况下&#xff0c;DHCP作为Windows Server的一个服…

【Python】使用tkinter设计开发Windows桌面程序记事本(2)

上一篇&#xff1a;【Python】使用tkinter设计开发Windows桌面程序记事本&#xff08;1&#xff09;-CSDN博客 下一篇&#xff1a; 作者发炎 此代码模块是继承上一篇文章的代码模块的基础上开始设计开发的。 如果不知道怎么新建"记事本项目"文件夹&#xff0c;请参…

程序员试用期转正工作总结

一、试用期工作总结 在公司的三个月试用期中&#xff0c;我完成了以下工作&#xff1a; 完成了XX个功能模块的开发&#xff0c;包括XX模块、XX模块和XX模块。参与了XX个项目的开发和上线&#xff0c;其中XX项目、XX项目和XX项目是我主导的。优化了现有系统的性能&#xff0c;特…

跟着我学Python进阶篇:02.面向对象(上)

往期文章 跟着我学Python基础篇&#xff1a;01.初露端倪 跟着我学Python基础篇&#xff1a;02.数字与字符串编程 跟着我学Python基础篇&#xff1a;03.选择结构 跟着我学Python基础篇&#xff1a;04.循环 跟着我学Python基础篇&#xff1a;05.函数 跟着我学Python基础篇&#…

计算机体系结构----寄存器重命名/Tomasulo算法

前情提要 计分板算法可看我写的博文计算机体系结构----计分板&#xff08;scoreboard&#xff09;算法 Tomasulo算法的核心是寄存器重命名&#xff08;register renaming&#xff09;&#xff1b;通过寄存器重命名&#xff0c;可彻底消除WAR/WAW冲突&#xff0c;计分板算法中…

2024--Django平台开发-Django知识点(五)

day05 django知识点 今日概要&#xff1a; 中间件 【使用】【源码】cookie 【使用】【源码 - Django底层请求本质】session【使用】【源码 - 数据库请求周期中间件】 1.中间件 1.1 使用 编写类&#xff0c;在类型定义&#xff1a;process_request、process_view、process_…

C++重新认知:inline关键字

一、为什么要使用inline关键字 在C中.&#xff0c;为了解决频繁调用小函数造成大量消耗栈空间的问题&#xff0c;引进了inline关键字&#xff0c;inline修饰的函数为内敛函数&#xff08;频繁大量的使用栈空间会造成栈溢出&#xff09;。 在这段代码中&#xff0c;每次执行for…

RT-Thread:SPI万能驱动 SFUD 驱动Flash W25Q64,通过 STM32CubeMX 配置 STM32 SPI 驱动

关键词&#xff1a;SFUD,FLASH,W25Q64&#xff0c;W25Q128&#xff0c;STM32F407 说明&#xff1a;RT-Thread 系统 使用 SPI万能驱动 SFUD 驱动 Flash W25Q64&#xff0c;通过 STM32CubeMX 配置 STM32 SPI 驱动。 提示&#xff1a;SFUD添加后的存储位置 1.打开RT-Thread Sett…

【obj To 3DTiles 格式转换】 可以自定义经纬高、属性表等参数 (一)

目录 0 引言1 3DTiles数据2 objTo3DTiles2.1 工具的安装2.1.1 拓展&#xff1a;Node.js 和 npm 2.2 工具的使用2.2.1 输出成瓦片数据2.2.2 输出带有坐标参数的瓦片数据 3 查看3DTiles数据 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;Cesiumfor…

强化学习9——免模型预测算法介绍(蒙特卡洛方法和时步差分方法)

对于大部分情况来说&#xff0c;环境是未知的&#xff0c;也就是说状态转移概率未知&#xff0c;对于这种情况的算法称为免模型预测算法。免模型算法与环境不断交互学习&#xff0c;但是需要大量的运算。 蒙特卡洛方法 蒙特卡罗方法通过重复随机抽选&#xff0c;之后运用统计…