【C++实战㉜】深入C++动态内存分配:从理论到实战的进阶之路
目录
- 一、动态内存分配的基本概念
- 1.1 动态内存的区域(堆内存)
- 1.2 动态内存分配的函数(new、delete、new []、delete [])
- 1.3 动态内存分配与静态内存分配的区别
- 二、动态内存分配的实战应用
- 2.1 单个对象的动态分配与释放
- 2.2 数组对象的动态分配与释放
- 2.3 动态内存分配的异常处理(bad_alloc 异常)
- 三、动态内存分配的实战技巧
- 3.1 动态内存的对齐问题
- 3.2 自定义 new 与 delete 运算符
- 3.3 内存池的概念与简单实现
- 四、实战项目:自定义内存分配器
- 4.1 项目需求(高效内存分配、减少碎片)
- 4.2 内存分配器代码实现
- 4.3 分配器测试与性能分析
一、动态内存分配的基本概念
1.1 动态内存的区域(堆内存)
在 C++ 程序运行时,内存被划分为不同的区域,以满足不同的存储需求。这些区域包括栈内存、堆内存、静态存储区和常量存储区等。而动态内存分配主要发生在堆内存区域。
- 栈内存:主要用于存储局部变量、函数参数和返回地址等。它的特点是内存分配和释放非常迅速,由系统自动管理。当一个函数被调用时,系统会为该函数的局部变量和参数在栈上分配内存空间;函数执行结束后,这些内存会自动被释放。栈内存的大小通常是有限的,并且在程序运行期间是固定的。例如:
void func() {int a = 10; // 变量a存储在栈内存中int b = 20; // 变量b也存储在栈内存中
}
- 堆内存:是用于动态分配内存的区域,程序可以在运行时根据需要申请和释放内存。与栈内存不同,堆内存的大小通常仅受限于系统的可用内存,并且它的分配和释放需要程序员手动管理。在 C++ 中,我们使用new操作符来在堆上分配内存,使用delete操作符来释放内存。堆内存的分配相对较慢,因为它需要在堆内存空间中查找合适的空闲内存块。而且,如果内存管理不当,容易导致内存泄漏和悬空指针等问题。例如:
void heapExample() {int* ptr = new int(10); // 在堆上分配一个int类型的内存空间,并初始化为10// 使用ptrdelete ptr; // 释放堆上的内存
}
堆内存的分配过程大致如下:操作系统维护一个记录空闲内存地址的链表。当程序通过new操作符申请内存时,系统会遍历该链表,寻找第一个空间大于所申请空间的内存块。然后,将该内存块从空闲链表中删除,并将其分配给程序。由于找到的内存块大小不一定正好等于申请的大小,系统会自动将多余部分重新放入空闲链表。在使用delete释放内存时,系统会将释放的内存块重新加入空闲链表,以便后续再次分配。
1.2 动态内存分配的函数(new、delete、new []、delete [])
在 C++ 中,动态内存分配主要通过new、delete、new[]和delete[]这几个操作符来实现。它们是 C++ 内存管理的核心部分,正确使用它们对于编写高效、稳定的程序至关重要。
- new操作符:用于分配单个对象的内存空间。它会在堆上为对象分配足够的内存,并调用对象的构造函数进行初始化。例如:
int* num = new int(5); // 分配一个int类型的内存空间,并初始化为5
这里,new int(5)首先在堆上分配了一个可以存储int类型数据的内存空间,然后将值5赋给这个内存空间。如果内存分配成功,new会返回一个指向该内存空间的指针;如果分配失败,会抛出std::bad_alloc异常。
- delete操作符:用于释放由new分配的单个对象的内存空间。它会先调用对象的析构函数(如果对象有析构函数),然后释放内存。例如:
delete num; // 释放num指向的内存空间
在使用delete时,必须确保指针指向的是由new分配的内存,否则会导致未定义行为。
- new[]操作符:用于分配一个对象数组的内存空间。它会在堆上为数组中的每个对象分配内存,并依次调用每个对象的构造函数进行初始化。例如:
int* arr = new int[10]; // 分配一个包含10个int类型元素的数组
这里,new int[10]在堆上分配了一段连续的内存空间,足以存储 10 个int类型的数据,并对每个元素进行了默认初始化(对于内置类型int,默认初始化值为 0)。
- delete[]操作符:用于释放由new[]分配的对象数组的内存空间。它会依次调用数组中每个对象的析构函数(如果对象有析构函数),然后释放内存。例如:
delete[] arr; // 释放arr指向的数组内存空间
在释放数组内存时,必须使用delete[],而不是delete,否则只会调用数组中第一个对象的析构函数,而不会调用其他对象的析构函数,从而导致内存泄漏和资源未正确释放的问题。
1.3 动态内存分配与静态内存分配的区别
在 C++ 编程中,内存分配方式主要有动态内存分配和静态内存分配两种,它们在多个方面存在显著差异。
- 分配时间:
- 静态内存分配:在编译阶段就确定了变量或数据结构所需的内存空间。例如,全局变量和静态局部变量在程序启动时就被分配内存,其生命周期持续到程序结束。局部变量在函数被调用时,在栈上进行分配,函数结束时自动释放。例如:
int globalVar; // 全局变量,在编译时分配内存
void func() {static int staticLocalVar; // 静态局部变量,在编译时分配内存int localVar; // 局部变量,在函数调用时在栈上分配内存
}
- 动态内存分配:在程序运行时根据需要使用new或malloc()等操作符动态请求内存。内存的分配和释放时机由程序员在代码中控制。例如:
void dynamicAllocation() {int* ptr = new int; // 在运行时动态分配内存// 使用ptrdelete ptr; // 在运行时释放内存
}
- 内存区域:
- 静态内存分配:主要发生在栈内存和静态存储区。栈内存用于存储局部变量、函数参数等,其内存管理由系统自动完成;静态存储区用于存储全局变量和静态局部变量,这些变量在程序运行期间一直存在。
- 动态内存分配:发生在堆内存区域。堆内存是一个相对灵活的内存空间,其大小仅受限于系统的可用内存,但需要程序员手动管理内存的分配和释放。
- 内存管理方式:
- 静态内存分配:内存的分配和释放由编译器或系统自动管理。例如,局部变量在函数结束时会自动从栈中弹出,内存被释放;全局变量和静态局部变量在程序结束时由系统自动释放内存。
- 动态内存分配:需要程序员手动使用delete(或free(),在 C 语言中)来释放分配的内存。如果不及时释放,会导致内存泄漏,即程序占用的内存不断增加,最终可能耗尽系统内存,导致程序崩溃或系统性能下降。
- 内存大小确定时机:
- 静态内存分配:内存大小在编译时就已经确定,一旦分配,在程序运行期间大小不可改变。例如,定义一个数组int arr[10];,其大小在编译时就固定为 10 个元素的空间,无法在运行时动态调整。
- 动态内存分配:内存大小可以在程序运行时根据实际需求进行调整。例如,可以根据用户输入的大小来动态分配数组内存:
int size;
cin >> size;
int* dynamicArr = new int[size]; // 根据用户输入的size动态分配数组内存
// 使用dynamicArr
delete[] dynamicArr; // 释放动态分配的数组内存
二、动态内存分配的实战应用
2.1 单个对象的动态分配与释放
在 C++ 中,动态分配和释放单个对象是常见的操作,这对于需要在运行时根据实际情况决定对象的创建和销毁非常有用。下面是一个完整的代码示例,展示如何动态分配和释放一个简单的类对象:
#include <iostream>class MyClass {
public:MyClass() {std::cout << "MyClass constructor called." << std::endl;}~MyClass() {std::cout << "MyClass destructor called." << std::endl;}
};int main() {// 动态分配一个MyClass对象MyClass* obj = new MyClass;// 使用obj进行一些操作,例如调用成员函数// obj->someFunction();// 释放动态分配的对象delete obj;return 0;
}
在上述代码中:
- MyClass* obj = new MyClass;:使用new操作符在堆上分配一个MyClass类型的对象,并返回指向该对象的指针obj。此时,MyClass的构造函数会被调用。
- delete obj;:使用delete操作符释放obj指向的内存空间,同时MyClass的析构函数会被调用。
常见错误及避免方法:
- 内存泄漏:如果在分配内存后,没有调用delete释放内存,就会导致内存泄漏。例如:
void memoryLeakExample() {MyClass* obj = new MyClass;// 忘记调用delete obj;
}
为了避免内存泄漏,一定要在不再需要动态分配的对象时,及时调用delete释放内存。
- 悬空指针:当使用delete释放内存后,指针仍然指向已释放的内存地址,就会产生悬空指针。如果后续不小心使用了这个悬空指针,会导致未定义行为。例如:
void danglingPointerExample() {MyClass* obj = new MyClass;delete obj;// obj现在是悬空指针// 错误操作:试图使用悬空指针obj->someFunction();
}
为了避免悬空指针,可以在使用delete后,将指针赋值为nullptr,例如:
void avoidDanglingPointer() {MyClass* obj = new MyClass;delete obj;obj = nullptr;
}
2.2 数组对象的动态分配与释放
在实际编程中,我们经常需要动态分配和释放数组对象。这在处理不确定大小的数据集时非常有用。以下是动态分配和释放数组对象的代码示例:
#include <iostream>class MyClass {
public:MyClass() {std::cout << "MyClass constructor called." << std::endl;}~MyClass() {std::cout << "MyClass destructor called." << std::endl;}
};int main() {int size = 5;// 动态分配一个包含5个MyClass对象的数组MyClass* arr = new MyClass[size];// 使用数组,例如访问数组元素并调用成员函数for (int i = 0; i < size; ++i) {// arr[i].someFunction();}// 释放动态分配的数组delete[] arr;return 0;
}
在这段代码中:
- MyClass* arr = new MyClass[size];:使用new[]操作符在堆上分配一个包含size个MyClass对象的数组,并返回指向数组首元素的指针arr。此时,数组中每个MyClass对象的构造函数都会被调用。
- delete[] arr;:使用delete[]操作符释放arr指向的数组内存空间,同时数组中每个MyClass对象的析构函数都会被调用。
内存分配失败处理及注意事项:
- 内存分配失败处理:当使用new或new[]分配内存时,如果系统无法提供足够的内存,会抛出std::bad_alloc异常。为了处理这种情况,可以使用try-catch块捕获异常,例如:
int main() {int size = 1000000000; // 假设申请一个很大的数组,可能导致内存分配失败MyClass* arr = nullptr;try {arr = new MyClass[size];} catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl;// 可以在这里进行一些错误处理,例如释放其他已分配的资源,或者给出用户提示return 1; // 返回错误代码}// 使用数组if (arr) {// 操作数组delete[] arr;}return 0;
}
- 注意事项:在释放数组内存时,必须使用delete[],而不能使用delete。因为delete只会调用数组中第一个对象的析构函数,而不会调用其他对象的析构函数,从而导致内存泄漏和资源未正确释放。例如:
void wrongDeletion() {MyClass* arr = new MyClass[5];// 错误:使用delete释放数组delete arr;
}
上述代码是错误的,正确的做法是使用delete[] arr;来释放数组内存。
2.3 动态内存分配的异常处理(bad_alloc 异常)
在 C++ 中,当使用new或new[]进行动态内存分配时,如果系统无法满足分配请求,会抛出std::bad_alloc异常。这个异常是从std::exception派生而来,用于表示内存分配失败的情况。理解和正确处理std::bad_alloc异常对于编写健壮的 C++ 程序至关重要。
异常原理:当new操作符尝试在堆上分配内存时,如果没有足够的可用内存,它会抛出std::bad_alloc异常。这通常发生在系统内存不足,或者请求的内存块过大,超出了系统的限制。例如,当程序尝试分配一个非常大的数组,而系统没有足够的连续内存空间时,就会触发这个异常。
捕获处理异常的代码示例:
#include <iostream>
#include <new> // 包含bad_alloc异常的头文件int main() {try {int* largeArray = new int[1000000000]; // 可能会导致内存分配失败// 使用largeArraydelete[] largeArray;} catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl;// 在这里可以进行一些错误处理,比如释放其他已分配的资源// 或者向用户显示友好的错误信息// 也可以记录日志等操作}return 0;
}
在上述代码中:
- try块中包含了可能会抛出std::bad_alloc异常的动态内存分配操作。如果new int[1000000000]成功分配内存,程序会继续执行try块中的后续代码,使用完数组后释放内存。
- 如果内存分配失败,new操作符会抛出std::bad_alloc异常,程序会立即跳转到catch块中执行。在catch块中,通过std::cerr输出错误信息,e.what()返回关于异常的详细描述。然后可以根据具体需求进行错误处理,比如释放其他已分配的资源,或者向用户显示友好的错误信息,也可以记录日志等操作。这样可以避免程序因为内存分配失败而突然崩溃,提高程序的稳定性和用户体验。
三、动态内存分配的实战技巧
3.1 动态内存的对齐问题
在计算机系统中,内存对齐是一种重要的内存管理策略。它要求数据在内存中的存储地址满足特定的规则,通常是数据类型大小的整数倍。例如,一个 4 字节的int类型数据,其存储地址通常是 4 的倍数。这是因为硬件架构和处理器在访问内存时,对数据的对齐方式有一定的要求。
内存对齐主要有两个重要作用。一是提高内存访问性能,现代 CPU 通常一次可以读取多个字节的数据,如 4 个或 8 个字节。如果一个数据项没有正确对齐,那么 CPU 可能需要两次或更多次的内存访问才能完成对该数据项的加载或存储操作,这会显著降低程序运行速度。例如,在 32 位系统上,如果一个 4 字节的整数没有 4 字节对齐,那么访问这个整数时可能会跨越两个不同的内存块,导致性能下降。二是确保平台兼容性,不是所有的硬件平台都能无差别地访问任意地址上的任意数据。某些硬件平台(如某些架构的 CPU)要求特定类型的数据必须从特定的内存地址开始存取,否则可能引发硬件异常或错误。内存对齐可以确保数据在存储时满足这些硬件要求,从而提高程序的兼容性和稳定性。
在 C++ 中,可以通过一些方式来控制内存对齐。例如,使用#pragma pack(n)指令,它可以指定结构体或类的对齐方式为n字节对齐。n必须是 2 的幂次方,如 1、2、4、8、16 等。例如:
#pragma pack(4)
struct MyStruct {char a; // 1 byteint b; // 4 bytesshort c; // 2 bytes
};
#pragma pack() // 恢复默认对齐方式
在上述代码中,MyStruct结构体的对齐方式被指定为 4 字节对齐。char类型的成员a占用 1 字节,为了满足 4 字节对齐,它后面会填充 3 字节;int类型的成员b占用 4 字节,其地址是 4 的倍数;short类型的成员c占用 2 字节,它后面会填充 2 字节,以确保整个结构体的大小是 4 的倍数。这样,MyStruct结构体的大小为 12 字节(1 + 3 + 4 + 2 + 2)。
另外,C++11 引入了alignas关键字,用于更精确地控制变量或类型的对齐方式。例如:
alignas(16) double myDouble; // 确保myDouble以16字节对齐
这里,myDouble变量将以 16 字节对齐,即使double类型本身的默认对齐方式可能是 8 字节。这在一些对内存对齐要求严格的场景,如 SIMD(单指令多数据)编程中非常有用,因为 SIMD 指令通常要求数据以特定的对齐方式存储,以提高指令执行效率。
3.2 自定义 new 与 delete 运算符
在 C++ 中,默认的new和delete运算符提供了基本的内存分配和释放功能。然而,在某些特定场景下,我们可能需要自定义new和delete运算符,以满足更复杂的内存管理需求。比如,在内存分配时进行额外的初始化操作,或者在释放内存时进行资源清理等。
自定义new和delete运算符有两种方式:全局重载和类特定重载。全局重载会影响整个程序中的所有内存分配和释放操作;而类特定重载仅影响特定类的实例。下面是一个类特定重载new和delete运算符的示例:
#include <iostream>
#include <cstdlib>class MyClass {
public:// 自定义new运算符void* operator new(size_t size) {std::cout << "Custom new called, size: " << size << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 自定义delete运算符void operator delete(void* ptr) noexcept {std::cout << "Custom delete called" << std::endl;std::free(ptr);}
};int main() {MyClass* obj = new MyClass(); // 调用自定义的newdelete obj; // 调用自定义的deletereturn 0;
}
在上述代码中,MyClass类重载了new和delete运算符。当使用new MyClass()创建对象时,会调用自定义的new运算符,它首先输出提示信息,然后使用std::malloc分配内存,并在分配失败时抛出std::bad_alloc异常。当使用delete obj释放对象时,会调用自定义的delete运算符,它输出提示信息后,使用std::free释放内存。
需要注意的是,自定义的new和delete运算符应该与标准的行为保持一致,以避免引入难以调试的问题。例如,自定义的new运算符在分配失败时应该抛出std::bad_alloc异常,而自定义的delete运算符应该正确释放内存,并且不能对空指针进行操作。如果重载全局的new和delete,要特别小心,因为它们会影响整个程序的内存分配行为,可能会导致一些意想不到的问题,比如与其他库的内存管理冲突。
3.3 内存池的概念与简单实现
内存池是一种内存分配方式,它在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的主要目的是提高内存分配效率,减少内存碎片的产生。
在传统的内存分配方式中,如使用new和delete操作符,由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片,进而降低性能。例如,假设系统依次分配了 16 字节、8 字节、16 字节、4 字节的内存块,还剩余 8 字节未分配。这时要分配一个 24 字节的空间,虽然操作系统回收了前面的两个 16 字节,总的剩余空间有 40 字节,但是却不能分配出一个连续 24 字节的空间,这就是外碎片问题。而内存池通过预先分配大块内存并在需要时从中分配小块内存,避免了频繁向操作系统请求内存的开销,并且可以更好地控制内存的使用,减少内存碎片的产生。
下面是一个简单的内存池类的设计思路和实现代码:
#include <iostream>
#include <cstdlib>
#include <cassert>class MemoryPool {
public:MemoryPool(size_t blockSize, size_t initialBlocks): blockSize(blockSize), initialBlocks(initialBlocks) {poolSize = blockSize * initialBlocks;pool = std::malloc(poolSize);assert(pool != nullptr);freeList = pool;for (size_t i = 0; i < initialBlocks - 1; ++i) {*(reinterpret_cast<void**>(freeList) + i) = reinterpret_cast<char*>(freeList) + blockSize;}*(reinterpret_cast<void**>(freeList) + initialBlocks - 1) = nullptr;}~MemoryPool() {std::free(pool);}void* allocate() {if (freeList == nullptr) {// 内存池耗尽,可选择扩展内存池或返回错误return nullptr;}void* block = freeList;freeList = *(reinterpret_cast<void**>(freeList));return block;}void deallocate(void* block) {assert(block >= pool && block < reinterpret_cast<char*>(pool) + poolSize);*(reinterpret_cast<void**>(block)) = freeList;freeList = block;}private:size_t blockSize;size_t initialBlocks;size_t poolSize;void* pool;void* freeList;
};
使用示例:
int main() {MemoryPool pool(sizeof(int), 10);int* num1 = static_cast<int*>(pool.allocate());int* num2 = static_cast<int*>(pool.allocate());*num1 = 10;*num2 = 20;std::cout << "num1: " << *num1 << ", num2: " << *num2 << std::endl;pool.deallocate(num1);pool.deallocate(num2);return 0;
}
在上述代码中,MemoryPool类实现了一个简单的内存池。构造函数接受每个内存块的大小blockSize和初始分配的内存块数量initialBlocks,并根据这些参数分配一块连续的内存作为内存池。allocate方法从内存池中分配一个内存块,deallocate方法将释放的内存块重新加入内存池的空闲列表。这个简单的内存池实现适用于分配固定大小内存块的场景,并且没有考虑线程安全等复杂问题,在实际应用中可以根据需求进一步完善。
四、实战项目:自定义内存分配器
4.1 项目需求(高效内存分配、减少碎片)
在许多实际应用场景中,如游戏开发、高性能服务器编程等,对内存分配的效率和内存碎片的控制有着极高的要求。传统的内存分配方式,如使用new和delete操作符,虽然简单易用,但在频繁的内存分配和释放操作中,往往会导致内存碎片的产生,从而降低内存的使用效率和程序的整体性能。
例如,在游戏开发中,可能会频繁地创建和销毁各种游戏对象,如角色、道具、场景元素等。如果每次都使用标准的内存分配方式,随着游戏的运行,内存碎片会逐渐增多,导致后续的内存分配操作变得越来越慢,甚至可能出现内存不足的情况,影响游戏的流畅性和稳定性。同样,在高性能服务器编程中,大量的网络请求和数据处理需要频繁地分配和释放内存,如果内存管理不善,会导致服务器的响应时间变长,吞吐量下降,无法满足高并发的需求。
因此,开发一个自定义内存分配器,实现高效的内存分配和减少内存碎片,具有重要的实际意义。高效的内存分配器能够快速地为程序提供所需的内存,减少内存分配的时间开销,提高程序的运行效率。而减少内存碎片则可以提高内存的利用率,避免因内存碎片过多而导致的内存浪费和内存分配失败的问题,从而增强程序的稳定性和可靠性。
4.2 内存分配器代码实现
下面是一个简单的自定义内存分配器的关键部分代码实现,主要包括内存分配和释放函数。这个内存分配器采用了内存池的思想,预先分配一块较大的内存空间,然后在需要时从这个内存池中分配小块内存,从而减少对操作系统的内存分配请求次数,提高内存分配效率,并减少内存碎片的产生。
#include <iostream>
#include <cstdlib>
#include <cassert>class MemoryAllocator {
public:MemoryAllocator(size_t blockSize, size_t initialBlocks): blockSize(blockSize), initialBlocks(initialBlocks) {poolSize = blockSize * initialBlocks;pool = std::malloc(poolSize);assert(pool != nullptr);freeList = pool;for (size_t i = 0; i < initialBlocks - 1; ++i) {*(reinterpret_cast<void**>(freeList) + i) = reinterpret_cast<char*>(freeList) + blockSize;}*(reinterpret_cast<void**>(freeList) + initialBlocks - 1) = nullptr;}~MemoryAllocator() {std::free(pool);}void* allocate() {if (freeList == nullptr) {// 内存池耗尽,可选择扩展内存池或返回错误return nullptr;}void* block = freeList;freeList = *(reinterpret_cast<void**>(freeList));return block;}void deallocate(void* block) {assert(block >= pool && block < reinterpret_cast<char*>(pool) + poolSize);*(reinterpret_cast<void**>(block)) = freeList;freeList = block;}private:size_t blockSize;size_t initialBlocks;size_t poolSize;void* pool;void* freeList;
};
在这段代码中:
- 构造函数MemoryAllocator(size_t blockSize, size_t initialBlocks)接受每个内存块的大小blockSize和初始分配的内存块数量initialBlocks作为参数。它首先计算出内存池的总大小poolSize,然后使用std::malloc分配一块连续的内存空间作为内存池pool。接着,初始化空闲链表freeList,将内存池中的每个内存块依次连接起来,形成一个空闲链表。
- allocate函数用于从内存池中分配一个内存块。它首先检查空闲链表是否为空,如果为空,表示内存池已耗尽,返回nullptr;否则,从空闲链表中取出第一个内存块(即freeList指向的内存块),将其返回给调用者,并更新freeList指向下一个空闲内存块。
- deallocate函数用于将释放的内存块重新加入内存池的空闲链表。它首先通过assert语句确保要释放的内存块在内存池的范围内,然后将该内存块插入到空闲链表的头部,即让freeList指向该内存块,并将该内存块原来指向的下一个空闲内存块(如果有)赋值给新的freeList的下一个指针。
4.3 分配器测试与性能分析
为了测试自定义内存分配器的功能和性能,我们编写以下测试代码:
#include <iostream>
#include <chrono>int main() {MemoryAllocator allocator(sizeof(int), 100);// 测试功能int* numbers[100];for (int i = 0; i < 100; ++i) {numbers[i] = static_cast<int*>(allocator.allocate());*numbers[i] = i;}for (int i = 0; i < 100; ++i) {std::cout << *numbers[i] << " ";}std::cout << std::endl;for (int i = 0; i < 100; ++i) {allocator.deallocate(numbers[i]);}// 性能分析auto start = std::chrono::high_resolution_clock::now();for (int i = 0; i < 1000000; ++i) {void* block = allocator.allocate();if (block) {allocator.deallocate(block);}}auto end = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();std::cout << "Total time for 1 million allocations and deallocations: " << duration << " microseconds" << std::endl;return 0;
}
在上述测试代码中:
- 首先进行功能测试,通过MemoryAllocator分配 100 个int类型的内存块,对每个内存块进行赋值并打印,然后释放这些内存块,以验证内存分配器的基本功能是否正常。
- 接着进行性能分析,使用std::chrono库来记录时间。通过循环 100 万次进行内存分配和释放操作,计算整个过程所花费的时间,并输出结果。
假设在运行测试代码后,得到的结果为 “Total time for 1 million allocations and deallocations: 50000 microseconds”,即 100 万次分配和释放操作总共花费了 50000 微秒(50 毫秒)。与标准的new和delete操作符相比,在频繁的小内存块分配场景下,自定义内存分配器由于减少了对操作系统的内存分配请求次数,以及更好地管理了内存碎片,其性能可能会有显著提升。例如,使用标准new和delete操作符进行相同的 100 万次小内存块分配和释放操作,可能需要花费 100 毫秒甚至更多的时间。通过这样的性能对比分析,可以直观地展示出自定义内存分配器在特定场景下的优势。