南京电子商务网站建设,华为官方手机商城,seo对各类网站的作用,网站建设开发五行属性C内存管理机制#xff08;侯捷#xff09;
本文是学习笔记#xff0c;仅供个人学习使用。如有侵权#xff0c;请联系删除。
参考链接
Youtube: 侯捷-C内存管理机制
Github课程视频、PPT和源代码: https://github.com/ZachL1/Bilibili-plus
第一讲primitives的笔记 截至…C内存管理机制侯捷
本文是学习笔记仅供个人学习使用。如有侵权请联系删除。
参考链接
Youtube: 侯捷-C内存管理机制
Github课程视频、PPT和源代码: https://github.com/ZachL1/Bilibili-plus
第一讲primitives的笔记 截至2024年1月10日17点09分花费一天的时间完成《C内存管理——从平地到万丈高楼》第一讲的学习主要学习了new delete array newplacement new以及allocator的几个设计版本。 文章目录 C内存管理机制侯捷2 内存分配的每一层面3 四个层面的基本用法4 基本构件之一new delete expression上5 基本构件之一new delete expression中6 基本构件之一new delete expression下7 Array new8 placement new9 重载10 重载示例上11 重载示例下12 Per class allocator13 Per class allocator 214 Static allocator15 Macro for static allocator16 New Handler后记 2 内存分配的每一层面
C的 applications可以调用STL里面会有allocator进行内存分配也可以使用C 基本工具primitives比如new, new[], new(), ::operator new()还可以使用更底层的malloc和free分配和释放内存。最底层的是系统调用比如HeapAllocVirtualAlloc等。 HeapAllocVirtualAlloc的介绍
HeapAlloc 和 VirtualAlloc 是 Windows 操作系统中用于内存分配的两个重要的系统调用。 HeapAlloc: 描述: HeapAlloc 用于在指定的堆中分配一块指定大小的内存。 参数: hHeap: 指定要分配内存的堆的句柄。dwFlags: 分配选项例如是否可以共享内存等。dwBytes: 要分配的内存大小以字节为单位。 返回值: 成功时返回分配内存块的指针失败时返回 NULL。 示例: HANDLE hHeap GetProcessHeap();
LPVOID lpMemory HeapAlloc(hHeap, 0, 1024); // 在默认堆中分配 1024 字节的内存VirtualAlloc: 描述: VirtualAlloc 用于在进程的虚拟地址空间中分配、保留或提交内存。 参数: lpAddress: 指定欲分配的内存的首选地址。可以指定为 NULL系统会自动选择合适的地址。dwSize: 要分配的内存大小以字节为单位。flAllocationType: 分配类型例如是保留、提交还是同时进行。flProtect: 内存保护选项指定内存区域的访问权限。 返回值: 成功时返回分配内存块的起始地址失败时返回 NULL。 示例: LPVOID lpMemory VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);这两个系统调用都是用于在程序运行时动态分配内存。HeapAlloc 通常与堆相关联而 VirtualAlloc 则更为底层允许更多的灵活性例如手动指定内存地址控制内存保护选项等。在实际使用中选择使用哪个取决于具体的需求和使用场景。
C memory primitives介绍 mallocfree和new delete的区别
malloc、free、new 和 delete 是在C和C中用于内存管理的操作符和函数。它们之间有一些重要的区别 malloc 和 freeC语言 malloc 用于在堆上分配一块指定大小的内存返回分配内存的首地址。 int *ptr (int *)malloc(sizeof(int));free 用于释放先前由 malloc 分配的内存。 free(ptr);在C中malloc 和 free 是标准库函数不涉及构造函数和析构函数只是简单的内存分配和释放。 new 和 deleteC语言 new 用于在堆上分配一块指定大小的内存并调用对象的构造函数来初始化对象。 int *ptr new int;delete 用于释放由 new 分配的内存并调用对象的析构函数。 delete ptr;在C中new 和 delete 不仅仅是内存分配和释放的操作符还会处理对象的构造和析构因此它们更适用于处理对象。
总的来说主要区别包括
malloc 和 free 是C语言标准库函数适用于内存分配和释放不涉及构造和析构。new 和 delete 是C语言中的操作符用于动态对象的创建和销毁涉及构造和析构。在C中不应该混合使用 malloc/free 和 new/delete因为它们有不同的内存管理方式和处理构造析构的机制。如果你在C中使用 new应该使用相应的 delete 来释放内存而不是 free。
3 四个层面的基本用法
对mallocnewallocator进行测试
#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 bytesfree(p1);complexint* p2 new complexint; //one objectdelete p2; void* p3 ::operator new(512); //512 bytes::operator delete(p3);//以下使用 C 標準庫提供的 allocators。
//其接口雖有標準規格但實現廠商並未完全遵守下面三者形式略異。
#ifdef _MSC_VER//以下兩函數都是 non-static定要通過 object 調用。以下分配 3 個 ints.int* p4 allocatorint().allocate(3, (int*)0); allocatorint().deallocate(p4,3);
#endif
#ifdef __BORLANDC__//以下兩函數都是 non-static定要通過 object 調用。以下分配 5 個 ints.int* p4 allocatorint().allocate(5); allocatorint().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 allocatorint().allocate(7); allocatorint().deallocate((int*)p4,7); //以下兩函數都是 non-static定要通過 object 調用。以下分配 9 個 ints. void* p5 __gnu_cxx::__pool_allocint().allocate(9); __gnu_cxx::__pool_allocint().deallocate((int*)p5,9);
#endif
}
} //namespace复习C的语法临时对象调用成员函数的用法
int* p4 allocatorint().allocate(5);
allocatorint().deallocate(p4,5); 这行C代码涉及到模板、临时对象以及动态内存分配下面是对该行代码的详细介绍
int* p4 allocatorint().allocate(5);allocatorint() allocator 是 C 标准库中的分配器类用于分配和释放内存。int 是模板参数指定了分配器要操作的类型为 int。allocatorint() 创建了一个临时的 allocator 对象。这是一个匿名对象即在一行代码内创建并使用不会被显式地命名。这样的对象通常用于执行一些简短的任务而不需要长时间存活。 .allocate(5) allocate 是 allocator 类的成员函数用于分配指定数量的未初始化内存块并返回指向第一个元素的指针。5 是要分配的元素数量。 int* p4 int* 声明了一个指向整数的指针。p4 是指针变量的名称。
综合起来该行代码的作用是使用 allocatorint 创建了一个临时的分配器对象并调用其 allocate 函数分配了大小为 5 * sizeof(int) 字节的内存块。然后将分配的内存块的首地址赋给 int* p4使 p4 成为该内存块的指针。请注意这里的内存块中的元素并没有被初始化如果需要使用这些元素必须进行相应的初始化操作。这样的代码在某些特殊场景下例如需要手动管理内存的情况可能会用到。
对#ifdef后面的_MSC_VER__BORLANDC____GNUC__进行解释
这段代码使用了条件编译指令 #ifdef根据不同的编译器选择性地编译不同的代码块。以下是对这些宏的解释 #ifdef _MSC_VER _MSC_VER 是 Microsoft Visual Studio 编译器的宏它的值对应于编译器的版本号。如果代码正在使用 Microsoft Visual Studio 编译器编译那么这个条件为真相关的代码块会被编译。在这个条件下使用 allocatorint 对象分配和释放内存。 #ifdef __BORLANDC__ __BORLANDC__ 是 Borland 编译器的宏用于检查是否正在使用 Borland 编译器。如果代码正在使用 Borland 编译器编译那么这个条件为真相关的代码块会被编译。在这个条件下使用 allocatorint 对象分配和释放内存。 #ifdef __GNUC__ __GNUC__ 是 GNU 编译器例如gcc 和 g的宏。如果代码正在使用 GNU 编译器编译那么这个条件为真相关的代码块会被编译。在这个条件下有两个不同的分配和释放内存的方式 一个使用 allocatorint 对象。另一个使用 GNU C 库中的 __gnu_cxx::__pool_allocint() 对象。
总体来说这段代码在不同的编译环境下使用不同的方式进行内存分配和释放。这样的条件编译可以使代码适应不同的编译器和环境。
下图中的__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 运算符创建对象时整个过程包括以下几个步骤 调用 operator new 分配内存 operator new 是一个用于动态内存分配的函数它负责分配指定大小的内存块并返回该内存块的指针。在内部operator new 可能调用标准库的 malloc 或其他分配函数来完成实际的内存分配。例如void* rawMemory operator new(sizeof(MyClass));指针转型 分配得到的原始内存的指针是 void* 类型的因为 operator new 并不知道具体分配的是什么类型的对象。因此需要将 void* 转换为所需类型的指针。例如MyClass* objPtr static_castMyClass*(rawMemory);调用构造函数 通过转型后的指针调用对象的构造函数进行初始化。这是为了确保对象在分配内存后能够执行必要的初始化操作。 例如 new (objPtr) MyClass(/* constructor arguments */);上述语法使用了定位 new placement new运算符它将在指定的内存位置调用构造函数。这允许我们在已分配的内存中构造对象而不是在默认构造函数中执行内存分配。
综合来说使用 new 运算符的过程涉及内存分配、指针类型转换和构造函数的调用确保对象能够正确地初始化。在对象的生命周期结束时还需要通过 delete 运算符释放内存并调用对象的析构函数。
继续补充定位new运算符
定位 new 运算符是 C 中的一种特殊形式的内存分配运算符用于在指定的内存位置上创建对象。它的语法如下
new (pointer) Type(initializer);其中
pointer 是指定的内存地址通常是一个指针。Type 是要创建的对象的类型。initializer 是可选的构造函数参数用于初始化对象。
定位 new 主要用于以下情况 在预分配的内存块中创建对象 void* memory operator new(sizeof(MyClass)); // 分配内存
MyClass* obj new (memory) MyClass(/* constructor arguments */); // 在指定内存位置创建对象在数组中创建对象 void* memory operator new[](sizeof(MyClass) * 5); // 分配数组内存
MyClass* objArray new (memory) MyClass[5]; // 在数组内存中创建对象用于自定义的内存管理 定位 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. this000307A8 id1
cout pA-id endl; //1
//! pA-A::A(3); //in VC6 : ctor. this000307A8 id3//in GCC : [Error] cannot call constructor jj02::A::A directly//! A::A(5); //in VC6 : ctor. this0013FF60 id5// dtor. this0013FF60 //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_stringchar has no member named string
//! pstr-~string(); //crash -- 其語法語意都是正確的, crash 只因為上一行被 remark 起來嘛. cout str *pstr endl;//------------A* pA new A(1); //ctor. this000307A8 id1cout pA-id endl; //1
//! pA-A::A(3); //in VC6 : ctor. this000307A8 id3//in GCC : [Error] cannot call constructor jj02::A::A directly//! A::A(5); //in VC6 : ctor. this0013FF60 id5// dtor. this0013FF60 //in GCC : [Error] cannot call constructor jj02::A::A directly// [Note] for a function-style cast, remove the redundant ::Acout pA-id endl; //in VC6 : 3//in GCC : 1 delete pA; //dtor. this000307A8 //simulate newvoid* p ::operator new(sizeof(A)); cout p p endl; //p000307A8pA static_castA*(p);
//! pA-A::A(2); //in VC6 : ctor. this000307A8 id2//in GCC : [Error] cannot call constructor jj02::A::A directly cout pA-id endl; //in VC6 : 2//in GCC : 0 //simulate deletepA-~A(); //dtor. this000307A8 ::operator delete(pA); //free()
}
} //namespace7 Array new
Complex* pca new Complex[3]; // 调用三次构造函数
delete[] pca; // 唤起三次析构函数 这是正确的string* psa new string[3];
delete psa; // 唤起一次析构函数这是错误的对于 delete[] pca;编译器会通过运行时信息来确定需要释放的内存块的数量因为在动态数组分配时会在数组的前面通常记录数组的大小信息。这个信息通常被称为“cookie”或“size”它使得 delete[] 知道要调用多少次析构函数并释放相应数量的内存。这种信息是由编译器自动管理的。
但是当你使用 delete pca; 尝试删除一个使用 new[] 分配的数组时会导致未定义的行为因为编译器可能无法正确获取数组的大小信息。这样的操作可能会导致内存泄漏和未能正确调用对象的析构函数。因此为了正确的内存管理应该始终使用相匹配的 new 和 delete 或 new[] 和 delete[] 运算符。 下面右侧的图是new int[10]的内存布局其中灰色的表示具体的数据橙色的是debug模式下添加的内存最上面和最下面的两个0x6161H是cookie记录整体内存分配的大小。61H实际上是60H表示内存分配的大小后面1H意思是占用最后一位表示内存分配出去。浅蓝色的pad表示补齐填补到16的倍数。 下面的Demo中有3个int型的成员变量所以每个Demo对象的大小是12B每个int型是 4B 8 placement new
placement new允许我们将对象建构在allocated memory中。
#includenew
char* buf new char[sizeof(Complex) * 3]; // 已经分配了内存
Complex* pc new(buf)Complex(1, 2); // 把上面分配的内存位置传进来delete[] buf;其中Complex* pc new(buf)Complex(1, 2);这句话会被编译器转换为下图中的123三行分别调用operator new和上文看到的不同这里需要第二个参数表示位置这个函数只是传回这个位置不再分配内存指针转型调用构造函数。
这种用法被称为 “placement new”它允许程序员在指定的内存位置上创建对象。这通常用于特殊的内存管理场景例如在预分配的内存池中创建对象。 9 重载
C应用程序分配内存的途径
一个类里面可重载operator new 和 operator delete从而改变内存的分配机制 C容器分配内存的途径
容器会走分配器分配器会调用::operator new和::operator delete底层也是调用malloc和free。 在 C 中容器使用分配器Allocator来进行内存分配和释放。分配器是负责管理容器内部元素内存的组件。下面是容器分配内存的一般途径 容器使用分配器 C 容器如 std::vector、std::list、std::map 等通常使用分配器来分配和释放内存。分配器是容器的一部分负责处理元素的内存分配和释放操作。 分配器调用 ::operator new 和 ::operator delete 分配器的实现通常会调用全局作用域下的 ::operator new 来分配内存并在需要释放内存时调用 ::operator delete。::operator new 和 ::operator delete 是 C 中的全局内存分配和释放函数。它们底层可能调用标准库的 malloc 和 free。 底层可能调用 malloc 和 free malloc 和 free 是 C 标准库中的内存分配和释放函数用于分配和释放原始的、未构造的内存块。C 的 ::operator new 和 ::operator delete 可能在底层调用这些函数。
总体来说C 容器使用分配器来管理内存而分配器可能在其实现中调用 ::operator new 和 ::operator delete从而涉及到底层的内存分配函数 malloc 和 free。这种设计允许用户自定义容器的内存管理行为以适应不同的需求。用户可以通过提供自定义分配器来实现特定的内存分配策略。
重载全局的::operator new 和::operator delete
由于是全局的函数这个影响很大。 在一个类中重载operator new和operator delete
类内部重载之后编译器会来调用 同样的道理也可以重载operator new[] 和operator delete[] 10 重载示例上
类内部重载上述4种操作的接口如下所示 测试上述一个类中重载newdelete new[], delete[]的用法
下图右侧可以看到在GNU C4.9版本中构造是从上到下析构是从下到上。 下面是使用全局new和delete的测试 11 重载示例下
placement new的重载new() 和 delete() placement new的重载第一参数必须是size_t类型 对于如何区分operator new 和 placement new就要看调用的时候怎么用了。在调用时编译器会根据传递给 new 表达式的参数来匹配适当的重载版本。 测试代码
#include vector //for testnamespace 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(sizeextra);}//(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(sizeextra);}//(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::FooFoo* 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 new 和 operator delete 来实现。
下面简要描述一下内存池的概念并提供一个简单的示意图 内存池概念 内存池是一块预先分配的内存区域它被划分为多个小块每个小块可以被分配给程序使用。内存池通常由一个或多个链表、堆栈或其他数据结构来管理以追踪哪些内存块是空闲的哪些是已分配的。内存池的目的是减少因频繁的内存分配和释放而引起的性能开销。 示意图 ------------------------------------
| Memory Pool |
------------------------------------
| Free Block 1 | Free Block 2 |
-----------------------------------
| Allocated Block 1 |
------------------------------------
| Free Block 3 | Free Block 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_castBlock*(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_castvoid*(allocatedBlock);}void deallocate(void* ptr) {// 释放内存到内存池Block* block static_castBlock*(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. CPrimer 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 newsize_t chunk screenChunk * size;freeStore p reinterpret_castScreen*(new char[chunk]);//將分配得來的一大塊 memory 當做 linked list 般小塊小塊串接起來for (; p ! freeStore[screenChunk-1]; p)p-next p1;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_castScreen*(p))-next freeStore;freeStore static_castScreen*(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 i0; i N; i)p[i] new Screen(i); //輸出前 10 個 pointers, 用以比較其間隔 for (int i0; i 10; i) cout p[i] endl; for (int i0; i N; i)delete p[i];
}
} //namespace13 Per class allocator 2
设计类里面的allocator的第二版本
这里和第一版本的最大不同是设计上采用union
在C中union 是一种特殊的数据结构允许在相同的内存位置存储不同类型的对象。它的每个成员共享相同的内存空间只能同时使用一个成员。union 提供了一种有效利用内存的方式。
struct AirplaneRep {unsigned long miles; // 4Bchar 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 handler。new handler 可以执行一些操作例如释放一些已分配的内存、尝试扩展堆的大小、选择性地抛出异常或者执行其他用户定义的操作。
使用 set_new_handler 函数设置 new handler
#include new
#include iostreamvoid 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]; // 如果分配失败会调用 customNewHandlerreturn 0;
}在上述示例中通过 set_new_handler 函数设置了一个自定义的 new handler即 customNewHandler。当 new 操作符在尝试分配非常大的内存块时失败会调用这个自定义的 new handler。
注意事项
new handler 是全局的一旦设置会在程序的生命周期内一直有效直到被其他 set_new_handler 覆盖。如果 new handler 返回new 操作符会再次尝试分配内存如果还失败则再次调用 new handler。这个过程会一直重复直到 new handler 抛出异常或者不返回例如调用 std::terminate()。在 C11 及以后的版本中可以使用 std::get_new_handler 获取当前的 new handler以便在需要时进行保存和恢复。
使用 new handler 可以提供一些灵活性允许程序员在内存分配失败的情况下采取定制的操作而不是默认的行为即抛出 std::bad_alloc 异常。 set_new_handler的例子 在C中default 和 delete 是两个特殊的关键字用于指定和控制类的特殊成员函数的生成和使用。 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; // 析构函数
};delete delete 用于删除特殊成员函数或其他函数的默认实现使得在某些情况下编译器将会拒绝生成默认实现。 通过 delete可以防止某些函数的调用或者阻止生成某些函数。 示例 class NoCopyClass {
public:NoCopyClass() default; // 默认构造函数// 禁止复制构造和复制赋值NoCopyClass(const NoCopyClass) delete;NoCopyClass operator(const NoCopyClass) delete;
};在上述示例中通过 delete 明确禁止了复制构造和复制赋值使得这个类无法被复制。
这两个关键字的使用使得代码更加明确能够更好地表达程序员的意图并且在一些情况下可以帮助编译器进行更好的优化。default 和 delete 主要用于提高代码的清晰性、安全性和性能。
下图中的一句话defaultdelete可以用于operator new/ new[] operator delete/ delete[]以及它们的重载。 对operator new/ new[] operator delete/ delete[]的测试使用default, delete 后记
花费一天的时间完成《C内存管理——从平地到万丈高楼》第一讲的学习对C内存分配有了浅显的认识希望跟着侯捷老师后续的讲解逐渐加深对C内存分配的理解。