整个 STL 的操作对象(所有的数值)都 存放在容器之内,而容器 一定需要 配置空间以置放资料
空间 不一定是指内存,空间也可以是 磁盘或其它辅助存储介质
1、空间适配器的标准接口
1)allocator::value_type
2)allocator::pointer
3)allocator::const_pointer
4)allocator::reference
5)allocator::const_reference
6)allocator::size_type
7)allocator::difference_type
8)allocator::rebind
一个嵌套的类模板。class rebind<U>
拥有唯一成员 other,那是一个 typedef,代表 allocator<U>
自定义内存分配器相关。具体来说,它用于为不同类型的对象分配内存
#include <iostream>
#include <memory> // 包含 std::allocator
// 定义一个自定义分配器
template<typename T>
struct MyAllocator {
using value_type = T;
// 分配内存
T* allocate(std::size_t n) {
std::cout << "Allocating " << n << " element(s) of size " << sizeof(T) << '\n';
return static_cast<T*>(::operator new(n * sizeof(T))); // 见下面
}
// 释放内存
void deallocate(T* p, std::size_t n) {
std::cout << "Deallocating " << n << " element(s)\n";
::operator delete(p);
}
// 使用 rebind 将分配器转换为不同类型
template<typename U>
struct rebind {
using other = MyAllocator<U>;
};
};
int main() {
// 使用 MyAllocator<int> 分配器
MyAllocator<int> intAlloc;
// 为 5 个 int 分配内存
int* p = intAlloc.allocate(5);
// 为分配的内存赋值
for (int i = 0; i < 5; ++i) {
p[i] = i * 10;
std::cout << "p[" << i << "] = " << p[i] << '\n';
}
// 释放 int 类型的内存
intAlloc.deallocate(p, 5);
// 通过 rebind 将 int 分配器转换为 double 分配器
MyAllocator<int>::rebind<double>::other doubleAlloc;
// 为 3 个 double 分配内存
double* q = doubleAlloc.allocate(3);
// 为分配的内存赋值
for (int i = 0; i < 3; ++i) {
q[i] = i * 0.5;
std::cout << "q[" << i << "] = " << q[i] << '\n';
}
// 释放 double 类型的内存
doubleAlloc.deallocate(q, 3);
return 0;
}
rebind 机制允许将 MyAllocator<int>
转换为 MyAllocator<double>
,从而可以使用相同的分配器 为不同类型的对象分配内存
static_cast<T*>
是一种强制类型转换,将操作数的类型转换为 T*
,即指向类型 T 的指针。在这个上下文中,需要把从 operator new 返回的 void*
指针转换为 T*
operator new 与 new:内存分配 和 对象构造的分离
new 运算符不仅仅负责内存分配,还会调用对象的构造函数。因此,new T[n] 不仅会为 n 个对象分配内存,还会自动调用 每个对象的构造函数
但是在某些场景下,可能 只想分配内存而不构造对象,例如 在自定义分配器或某些低级内存操作中。通过 ::operator new
,可以只执行内存分配,而不会 调用构造函数,之后 再通过其他方式手动构造对象(比如通过placement new),避免不必要的对象初始化
9)allocator::allocator()
default constructor
10)allocator::allocator(const allocator&)
copy constructor
11)template <class U>
allocator::allocator (const allocator <U>
&)
泛化的 copy constructor
12)allocator::~allocator()
补:const 成员函数中的变量自动变为 const
class MyClass {
public:
int getValue() const {
value = 10; // 错误:不能在 const 成员函数中修改成员变量
return value;
}
private:
int value;
};
void setValue(int v) {
value = v;
}
private:
int value;
};
由于 this 指针在 const 成员函数中变为指向 const 的指针,因此通过 this 访问的对象的成员变量也隐式变为了 const。这意味着你无法修改它们
如果你真的需要在 const 成员函数中修改某个变量,可以使用 mutable 关键字来声明这个变量。mutable 允许成员变量在 const 成员函数中被修改
class MyClass {
public:
int getValue() const {
counter++; // 允许修改 mutable 成员
return value;
}
private:
int value;
mutable int counter; // 这个变量可以在 const 函数中修改
};
auto discountFunc = [discountRate](int price) mutable {
mutable 关键字:因为 discountRate 是按值捕获的,通常情况下,lambda 内不能修改它。如果要在 lambda 内修改 discountRate 的值(即使它按值捕获),mutable 关键字允许这种操作
13)const_pointer allocator::address(const_reference x) const
返回某个 const 对象的地址。算式 a.address(x) 等同于 &x
14)pointer allocator::allocate(size_type n, const void* = 0)
配置空间,足以存储 n 个 T 对象。第二参数是个提示。实现上可能会利用它来增进区域性(如果程序 在某个时间访问了某个内存地址,很可能不久之后 会访问相邻的地址),或完全忽略之
假设 你正在使用标准容器(如 std::vector)并且希望 在内存中分配一个新的元素块。由于 vector 中的元素通常是连续存储的(保持空间局部性),那么 当重新分配新空间时,内存分配器 可能希望分配到 尽量接近现有数据块的区域。hint 参数 可以提供一个指针指向之前分配的内存区域,告诉分配器 “如果可能的话,分配新的空间尽量接近这个区域”
15)void allocator::deallocate(pointer p, size_type n)
归还先前配置的空间
16)size_type allocator::max_size() const
返回 可成功配置的最大量
17)void allocator::construct(pointer p, const T& x)
等同于 new(void*) p) T(x)
placement new(定位 new)用法:用来在一块预先分配好的内存上构造对象。它与普通的 new 不同,普通 new 会同时分配内存并构造对象,而 placement new 只构造对象,不分配内存
基本语法:new (pointer) Type(constructor_arguments);
pointer 是已经分配好的内存地址,表示你希望在这块内存上构造对象
Type 是你要构造的对象类型
constructor_arguments 是对象的构造函数参数
new(void* p) T(x) 的含义是:在 p 指向的那块内存上,用 x 作为构造函数参数,构造一个类型为 T 的对象
书中的 p 的类型不应该是 const void*,因为 placement new 需要修改 p 指向的内存来存储新对象,而 const 意味着你承诺不会修改这个内存
#include <iostream>
#include <new> // for placement new
class MyClass {
public:
MyClass(int x) : value(x) {
std::cout << "Constructor called with value: " << value << std::endl;
}
~MyClass() {
std::cout << "Destructor called for value: " << value << std::endl;
}
int value;
};
int main() {
// 1. 分配一块内存,用于存储 MyClass 对象
void* memory = ::operator new(sizeof(MyClass));
// 2. 使用 placement new 在这块内存上构造一个 MyClass 对象
MyClass* obj = new (memory) MyClass(42);
// 3. 使用对象
std::cout << "Value: " << obj->value << std::endl;
// 4. 手动调用析构函数
obj->~MyClass();
// 5. 释放内存
18)void allocator::destroy(pointer p)
等同于 p->~T()
1.1 设计一个简单的空间配置器,JJ::allocator
#ifndef _JJALLOC_
#define _JJALLOC_
#include <new> // for placement new
#include <cstddef>
// for ptrdiff_t(专门用于存储两个指针之间的差值,保证能够保存这个差值的正确范围), size_t
#include <cstdlib> // for exit()
#include <climits> // for UINT_MAX
#include <iostream> // for cerr
namespace JJ {
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
// 将 n 转换为 difference_type,是为了确保能够在指针运算 或 元素计数时处理负数(在某些情况下可能有意义,例如当 n 为负时表示错误)
set_new_handler(0); // 重置 C++ 的内存分配失败处理程序
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
cerr << "out of memory" << endl;
exit(1); // 用于终止程序的执行,并返回一个状态码给操作系统
// 状态码 0 通常表示成功终止。
// 非零状态码(如 1)通常表示程序遇到了错误或异常,未能成功完成任务
}
return tmp;
}
template <class T>
inline void _deallocate(T* buffer){
::operator delete(buffer);
// 只接收一个指向被释放内存的指针(ptr),并不需要知道分配的大小
// 通常情况下,内存管理器已经保存了分配的大小信息,因此在释放内存时不需要再次提供 size
// 从 C++14 开始,C++ 标准引入了一种重载的 operator delete,可以接受一个 size_t 类型的大小参数,用于某些场景,例如更高效的内存管理或调试场景
// 然而,标准库中默认的 operator new 和 operator delete 并不总是需要使用这种形式。编译器和运行时通常能够管理内存大小的相关信息,并能够自动根据需要选择合适的版本
}
template <class T1, class T2>
inline void _construct(T1* p, const T2& value) {
new(p) T1(value); // placement new
}
template <class T>
inline void _destroy(T* ptr) {
ptr->~T();
}
template <class T>
class allocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
// rebind allocator of type U
template <class U>
struct rebind {
typedef allocator<U> other;
};
pointer allocate(size_type n, const void* hint=0) {
// hint=0:这是一个可选参数,通常在内存分配中被用作提示,帮助分配器做出某种优化决策
return _allocate((difference_type)n, (pointer)0);
// 通过传入 (pointer)0,其实传递了一个空指针(即 T* 类型的 nullptr)
}
void deallocate(pointer p, size_type n) {
_deallocate(p);}
void construct(pointer p, const T& value) {
_construct(p, value);
}
void destroy(pointer p) {
_destroy(p); }
pointer address(reference x) {
return (pointer)&x; }
// 将 &x 转换为 pointer 类型。根据上下文,pointer 是 T* 的别名。因此,这一步是类型转换,将 &x(类型为 T*)转换为 pointer
// 其实两者类型相同,都是 T*,所以这一步转换是多余的,但它确保了代码的通用性和可读性
const_pointer const_address(const_reference x) {
return (const_pointer)&x;
}
size_type max_size() const {
return size_type(UINT_MAX/sizeof(T));
}
}; // end of namespace JJ
#endif // _JJALLOC_
set_new_handler 是一个标准库函数,它允许 程序员设置一个自定义的处理程序,当 new 操作符无法分配内存时 会调用这个处理程序
如果没有可用的内存,new 操作符会抛出 std::bad_alloc 异常。但在某些情况下,程序员 可能希望提供自己的处理机制来应对这种情况,这就是 set_new_handler 的作用
set_new_handler(0);
这行代码的意思是将内存分配失败时的处理程序重置为 “空处理程序”,也就是说,如果之后调用 new 时分配失败,程序不会调用任何处理程序
如果 new 失败,则执行程序员手动检查分配结果的逻辑(如在 if (tmp == 0)
之后调用 exit(1))
#include "jjalloc.h"
using namespace std;
int main()
{
int ia[5] = {
0,1,2,3,4};
unsigned int i;
vector<int, JJ::allocator<int> > iv(ia, ia+5);
}
2、具备次配置力 (sub-allocation) 的 SGI 空间配置器
SGI STL 的配置器 与众不同,也与标准规范不同,其名称是 alloc 而非 allocator,并且不接受任何参数。如果 要在程序中明白采用 SGI 配置器,则不能采用标准写法:
vector<int, std::allocator<int>> iv;
必须这么写:
vector<int, std::alloc> iv; // in GCC
每一个容器 都已经指定其缺省的空间配置器为 alloc。例如下面的 vector 声明:
template <class T, class Alloc = alloc> // 缺省使用 alloc 为配置器
class vector {
... };
2.1 SGI 标准的空间配置器,std::allocator
虽然 SGI 也定义有一个符合部分标准、名为 allocator 的配置器,但 SGI 自己从未用过它,也不建议我们使用。主要原因是 因为效率不佳,只把 C++ 的 ::operator new 和 ::operator delete 做一层薄薄的包装而已
template <class T>
inline T* allocate(ptrdiff_t size, T*) {
set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
cerr << "out of memory" << endl;
exit(1);
}
return tmp;
}
template <class T>
inline void deallocate(T* buffer) {
::operator delete(buffer);
}
pointer allocate(size_type n) {
return ::allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p) {
::deallocate(p); }
2.2 SGI 特殊的空间配置器,std::alloc
习惯的 C++ 内存配置操作和释放操作是这样的:
class Foo {
...};
Foo* pf = new Foo; // 配置内存,然后构造对象
delete pf; // 将对象析构,然后释放内存
这其中的 new 算式内含两阶段操作:(1) 调用 ::operator new 配置内存;(2) 调用 Foo::Foo() 构造对象内容。delete 算式 也内含两阶段操作:(1) 调用 Foo::~Foo() 对象析构;(2) 调用 ::operator delete 释放内存
STL allocator 决定 将这两阶段操作分开来。内存配置操作由 alloc:allocate() 负责,内存释放操作由 alloc:deallocate() 负责;对象构造操作由 ::construct() 负责,对象析构操作由 ::destroy() 负责
配置器定义于 <memory>
之中,SGI <memory>
包含以下两个文件:
#include <stl_alloc.h> // 负责内存空间的配置与释放
#include <stl_construct.h> // 负责对象内容的构造与析构
2.3 构造和析构基本工具:construct() 和 destroy()
#include <new.h> // 欲使用 placement new,需先包含此文件
template <class T1, class T2>
inline void construct(T1* p, const T2& value)
{
new (p) T1(value); // placement new;调用 T1::T1(value);
}
// 以下是 destroy() 第一版本,接受一个指针
template <class T>
inline void destroy(T* pointer)
{
pointer->~T(); // 调用 析构函数 ~T()
// 通常,析构函数会自动调用,比如当对象超出作用域或显式调用 delete 时
// 但是,在某些情况下,特别是手动管理内存时(如使用 placement new),你可能需要手动调用析构函数来销毁对象
// placement new 是一种特殊的内存分配方式,它允许在一块已分配的内存上“构造”对象
// 由于 placement new 并不分配新的内存,只是将对象放在指定的内存上,因此当你不再需要这个对象时,你需要手动调用析构函数来清理对象
// 销毁对象和释放内存也是分开的操作
}
// 以下是 destroy() 第二版本,接受两个迭代器。此函数设法找出元素的数值型别,
// 进而利用 __type_traits<> 求取最适当措施
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last){
__destroy(first, last, value_type(first));
}
// 判断元素的数值型别(value type)是否有 trivial destructor
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}
// 如果元素的数值型别(value type)有 non-trivial destructor
// 如果类型 T 没有平凡的析构函数,那么需要显式地调用每个对象的析构函数
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for (;first < last;++first)
destroy(&*first);
// &*first 的意思是对解引用结果(即迭代器指向的对象)取它的地址。这个操作将返回一个指向 first 所指向对象的指针
}
// 如果元素的数值型别(value type)有 trivial destructor
// 如果类型 T 具有平凡的析构函数,那么不需要做任何事情,因为编译器会自动处理销毁,调用 __destroy_aux 的 __true_type 版本
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator,__true_type) {
}
// 以下是 destroy() 第二版本针对迭代器为 char* 和 wchar_t* 的特化版
inline void destroy(char*, char*)
inline void destroy(wchar_t*, wchar_t*) {
}
通过类型萃取(type traits)来推断类型 T 是否具有平凡的析构函数(trivial destructor),并据此决定如何销毁对象
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
:
类型萃取(type traits)是一种用于获取类型信息的技术,它允许程序在编译时通过模板元编程来判断某个类型是否具有特定特性。例如,判断一个类型是否有平凡的构造函数、析构函数、拷贝构造函数等
has_trivial_destructor 是 __type_traits<T>
中的一个类型成员,用来标识类型 T 的析构函数是否是平凡的,trivial_destructor 最终将是一个类型,它要么是 __true_type,要么是 __false_type,具体取决于类型 T 是否具有平凡的析构函数
平凡的析构函数(trivial destructor) 是指 编译器生成的析构函数,通常不执行任何特别的操作,比如 释放资源或调用其他析构函数。对于诸如基本类型(int、char、float 等)或 没有任何动态分配的类,析构函数通常是平凡的。
如果一个类型 有平凡的析构函数,则销毁对象时 不需要显式调用析构函数,因为编译器 不需要执行任何实际操作
typename 的作用是告诉编译器 __type_traits<T>::has_trivial_destructor
是一个类型,而不是一个静态成员或其他实体
假设 有一个需要手动销毁对象的场景,destroy 用于显式调用对象的析构函数:
#include <iostream>
class MyClass {
public:
MyClass(int x) : value(x) {
std::cout << "Constructor called for " << value << std::endl; }
~MyClass() {
std::cout << "Destructor called for " << value << std::endl; }
private:
int value;
};
int main() {
// 假设这里有一段原始内存,已经使用 placement new 构造了对象
MyClass* buffer = (MyClass*)::operator new(3 * sizeof(MyClass));
// 使用 placement new 构造对象
for (int i = 0; i < 3; ++i) {
new (&buffer[i]) MyClass(i + 1);
}
// 销毁对象
for (int i = 0; i < 3; ++i) {
destroy(&buffer[i]);
}
// 释放内存
::operator delete(buffer);
return 0;
}
template <class T>
void destroy(T* pointer) {
pointer->~T(); // 显式调用析构函数
}
2.4 空间的配置与释放,std::alloc
看完 内存配置后的 对象构造行为 和 内存释放前的 对象析构行为,现在 来看看内存的配置和释放
1、对象构造前的 空间配置 和 对象析构后的 空间释放,由 <stl_alloc.h> 负责,SGI 对此的设计哲学如下:
- 向 system heap(系统的堆) 要求空间
- 考虑多线程状态
- 考虑内存不足时的应变措施
- 考虑过多 “小型区块” 可能造成的内存碎片问题
2、程序向操作系统的堆(heap)内存区域 请求 动态内存分配。堆是操作系统 为应用程序提供的一块内存区域,允许程序 在运行时动态分配和释放内存
栈内存:通常用于 局部变量和函数调用。栈上的内存分配 是自动的,作用域结束时 自动释放,分配速度非常快,但内存的生命周期是固定的
堆内存:允许程序员在运行时 通过手动方式申请和释放内存。堆内存的大小 仅受系统内存的限制。分配和释放的时机由程序员决定,因此更灵活,但也更复杂,因为 需要手动管理这块内存的生命周期
3、C++ 的内存配置基本操作是 ::operator new(),内存释放基本操作是 ::operator delete()。这两个全局函数相当于 C 的 malloc() 和 free() 函数。SGI 正是以 malloc() 和 free() 完成内存的配置与释放
考虑到 小型区块所可能造成的内存破碎问题,SGI 设计了 双层级配置器,第一级配置器 直接使用 malloc() 和 free(),第二级配置器 则视情况采用不同的策略
当配置区块超过 128 bytes 时,视之为 “足够大”,便调用第一级配置器;当配置区块小于 128 bytes 时,视之为 “过小”,为了降低额外负担,便采用复杂的 内存池(memory pool) 整理方式,而不再求助于第一级配置器。整个设计究竟 只开放第一级配置器,或是 同时开放第二级配置器,取决于 __USE_MALLOC 是否被定义(SGI STL 并未定义 __USE_MALLOC)
4、内存池(Memory Pool)是一种高效的内存管理技术,用于 从一块预先分配的大内存块中(称为 “池”)按需分配较小的内存块。相比 频繁向操作系统请求内存,内存池 能显著提高内存分配的效率,尤其是在 频繁分配和释放小块内存的场景下
__USE_MALLOC:是一个典型的宏,通常用于 控制内存分配器 是否直接使用操作系统的 malloc 函数来分配内存。如果定义了 __USE_MALLOC,那么内存分配器在需要内存时,可能会 直接调用 malloc 来从操作系统获取内存
5、无论 alloc 被定义为 第一级或第二级配置器,SGI 还为它 再包装一个接口
template<class T, class Alloc>
class simple_alloc {
public:
static T *allocate(size_t n)
{
return 0 == n? 0 : (T*)Alloc::allocate(n*sizeof(T)); }
static T *allocate(void)
{
return (T*)Alloc::allocate(sizeof(T)); }
static void deallocate(T *p, size_t n)
{
if (0 != n) Alloc::deallocate(p, n * sizeof(T)); }
static void deallocate(T *p)
{
Alloc::deallocate(p, sizeof(T)); }
};
其内部 四个成员函数 其实都是单纯的转调用,调用 传递给配置器(可能是第一级 也可能是第二级)的成员函数。这个接口使配置器的配置单位从 bytes 转为 个别元素的大小(sizeof(T))。SGI STL 容器全都使用这个 simple_alloc 接口
template <class T, class Alloc = alloc> // 缺省使用 alloc 为配置器
class vector {
protected:
// 专属之空间配置器,每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
void deallocate() {
if (...)
data_allocator::deallocate(start, end_of_storage - start);
}
...
};
2.5 第一级配置器 _malloc_alloc_template 剖析
#if 0
#include <new>
#define __THROW_BAD_ALLOC throw bad_alloc
// #if 0:表示条件编译语句的开始,0 表示条件为假,因此这一部分的代码不会被编译
// 也就是说,编译器会跳过这部分代码。如果条件是 #if 1 或其他条件为真的情况,则代码会被编译
// #define __THROW_BAD_ALLOC throw bad_alloc:定义了宏 __THROW_BAD_ALLOC,它的作用是当内存分配失败时抛出 bad_alloc 异常
#elif !defined(__THROW_BAD_ALLOC)
#include <iostream.h>
#define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
// #elif !defined(__THROW_BAD_ALLOC):这部分的意思是,如果没有定义 __THROW_BAD_ALLOC 宏(即 __THROW_BAD_ALLOC 未被定义),那么执行下面的代码
// 如果条件为真:
// #include <iostream.h>:
// 会包含旧的 C++ 输入输出头文件 iostream.h(注意:iostream.h 是 C++ 早期标准使用的头文件,现在更常用的是 <iostream>)
// #define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1):
// 定义了宏 __THROW_BAD_ALLOC,这个宏定义了内存不足时的行为。在这个宏中,内存不足时会向标准错误流 cerr 输出 "out of memory",然后使用 exit(1) 来退出程序,状态码为 1,表示异常退出
#endif
template <int inst>
class __malloc_alloc_template {
private:
// 以下都是函数指针,所代表的函数将用来处理内存不足的情况
// oom : out of memory.
static void *oom_malloc(size_t);
// static:表示该函数是类的静态成员,静态成员函数属于类 而不是类的某个实例,因此可以在 没有实例化类的情况下调用
// void *:这是返回类型,oom_malloc(size_t):这是函数声明,表示该函数接受一个 size_t 类型的参数
static void *oom_realloc(void , size_t);
static void (*__malloc_alloc_oom_handler)();
public:
static void *allocate(size_t n)
{
void *result = malloc(n); // 第一级配置器直接使用 malloc()
// 以下无法满足需求时,改用 oom_malloc()
if (0 == result) result = oom_malloc(n);
return result;
}
static void deallocate(void p, size_t /* n */)
{
free(p); // 第一级配置器直接使用 free()
}
static void *reallocate(void p, size_t /* old_sz */, size_t new_sz)
{
void *result = realloc(p, new_sz); // 第一级配置器直接使用 realloc()
// 以下无法满足需求时,改用 oom_realloc()
if (0 == result) result = oom_realloc(p, new_sz);
return result;
}
// 以下仿真 C++ 的 set_new_handler()。换句话说,你可以通过它
// 指定你自己的 out-of-memory handler
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = _malloc_alloc_oom_handler;
_malloc_alloc_oom_handler = f;
return(old);
}
// malloc_alloc out-of-memory handling
// 初始值为 0 有待端设定
template <int inst>
void (*__malloc_alloc_template<inst>::_malloc_alloc_oom_handler)() = 0;
// 定义了一个模板类 __malloc_alloc_template 中的静态成员函数 oom_malloc
// 它的作用是在内存分配失败的情况下,通过调用特定的处理函数尝试释放内存并重新分配
// 数返回类型是 void*,表示返回一个指向已分配内存的指针。
// 参数 size_t n 代表希望分配的内存大小
template <int inst>
void *__malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (*my_malloc_handler)();
// 定义了一个函数指针 my_malloc_handler,它指向一个无参数、无返回值的函数。这个指针用来保存当前处理内存分配失败的回调函数
void *result;
for (;;) {
// 不断尝试释放、配置、再释放、再配置…
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) {
__THROW_BAD_ALLOC; }
// 如果 my_malloc_handler 是空指针(即没有定义处理内存不足的函数),程序将执行 __THROW_BAD_ALLOC
(*my_malloc_handler)(); // 调用处理函数,企图释放内存
// 调用函数指针 my_malloc_handler 所指向的函数,这个函数是专门用来处理内存不足的
result = malloc(n); // 再次尝试配置内存,如果经过处理函数的努力,系统释放了足够的内存,这次 malloc 可能成功
// 如果 malloc 成功,result 将是一个非空指针。此时返回这个指针,即返回成功分配的内存块的地址
if (result) return(result);
}
}
static void (*set_malloc_handler(void (*f)()))()
这部分定义了 set_malloc_handler 函数的签名:
void (*f)()
:表示参数 f 是一个函数指针,指向的函数无参数、无返回值。这意味着你可以传递一个函数,该函数将在内存分配失败时被调用
static:表示这个函数具有文件作用域,仅在当前文件中可见
void (*)()
:表示返回类型是一个函数指针,指向的函数也无参数、无返回值。这意味着 set_malloc_handler 会返回一个指向函数的指针
这个函数接受一个 指向无参、无返回值函数的指针,并返回 同样类型的函数指针
void (* old)() = _malloc_alloc_oom_handler;
_malloc_alloc_oom_handler:假设这是一个全局变量,它存储当前的“内存不足”处理函数_malloc_alloc_oom_handler 是一个函数指针,它指向在内存分配失败时 被调用的函数
将当前的 _malloc_alloc_oom_handler 的值存储在 old 变量中,以便返回之前的处理函数
_malloc_alloc_oom_handler = f;
return(old);
第一级配置器以 malloc(), free(), realloc() 等 C 函数执行实际的内存配置、释放、重配置操作,并实现出类似 C++ new-handler 的机制。不能直接运用 C++ new-handler 机制,因为它并非使用 ::operator new 来配置内存
用户可以设置一个内存不足时的处理函数 void my_oom_handler()
1、C++ 中的 new-handler 机制是专门为 ::operator new 函数设计的,旨在处理内存分配失败的情况
所谓 C++ new handler 机制是,你可以要求系统在内存配置需求 无法被满足时,调用一个你所指定的函数。换句话说,一旦 ::operator new 无法完成任务,在丢出 std::bad_alloc 异常状态之前,会先调用由客户端指定的处理例程。该处理例程通常即被称为 new-handler。new-handler 解决内存不足的做法有特定模式
#include <iostream>
#include <new> // 包含 new_handler 相关的头文件
#include <cstdlib>
// 自定义的内存不足处理函数
void my_new_handler() {
std::cerr << "Memory allocation failed! Attempting to free resources..." << std::endl;
// 这里可以添加释放资源的逻辑
}
int main() {
// 设置自定义的 new-handler
std::set_new_handler(my_new_handler);
try {
// 尝试分配大量内存,故意导致分配失败
int* arr = new int[1000000000]; // 尝试分配大量内存
// 使用分配的内存
// ...
delete[] arr; // 释放内存
} catch (const std::bad_alloc& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
2、类似 C++ new-handler 的机制:
假设 需要一个内存分配器类,负责内存的分配和管理。这个类应该能够在内存分配失败时调用一个处理函数
#include <cstdlib>
#include <iostream>
#include <functional>
template <int inst>
class MyAllocator {
private:
static void (*oom_handler)();
public:
static void *allocate(size_t n) {
void *result = std::malloc(n);
if (!result) {
// 内存分配失败,调用处理函数
if (oom_handler) {
oom_handler();
} else {
// 如果没有设置处理函数,则抛出异常或处理错误
throw std::bad_alloc();
}
// 再次尝试分配内存
result = std::malloc(n);
}
return result;
}
static void set_oom_handler(void (*handler)()) {
oom_handler = handler;
}
};
// 初始化静态成员
template <int inst>
void (*MyAllocator<inst>::oom_handler)() = nullptr;
设置一个内存不足时的处理函数
void my_oom_handler() {
std::cerr << "Memory allocation failed, attempting to free resources..." << std::endl;
// 这里可以添加代码来释放不必要的资源
}
用户在使用分配器之前,需要设置处理函数,并可以使用该分配器进行内存分配:
int main() {
MyAllocator<0>::set_oom_handler(my_oom_handler);
try {
void *ptr = MyAllocator<0>::allocate(1024);
// 0 是一个模板参数,表示 MyAllocator 类的特定实例
// 使用模板参数可以定义 多个不同的分配器。例如,MyAllocator<0> 和 MyAllocator<1> 可以拥有不同的行为、策略或内部状态
// 使用分配的内存
// ...
std::free(ptr); // 记得释放内存
} catch (const std::bad_alloc &e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
SGI 以 malloc 而非 ::operator new 来配置内存,因此,SGI 不能直接使用 C++ 的 set_new_handler(),必须仿真一个类似的 set_malloc_handler()
3、SGI 第一级配置器的 allocate() 和 realloc() 都是在调用 malloc() 和 realloc() 不成功后,改调用 oom_malloc() 和 oom_realloc()。后两者都有内循环,不断调用 “内存不足处理函数”,期望在某次调用之后,获得足够的内存而 圆满完成任务。但如果 “内存不足处理函数” 并未被客户端设定,oom_malloc() 和 oom_realloc() 便调用 __THROW_BAD_ALLOC,丢出 bad_alloc 异常信息,或利用 exit(1) 硬生生中止程序
记住,设计 “内存不足处理函数” 是客户端的责任
2.6 第二级配置器 default_alloc_template 剖析
1、第二级配置器 多了一些机制,避免太多小额区块 造成内存的碎片。小额区块 带来的其实 不仅是内存碎片,配置时的额外负担 也是一个大问题。额外负担 永远无法避免,毕竟系统要靠这多出来的空间 来管理内存。但是区块愈小,额外负担所占的比例 就愈大,愈显得浪费
SGI 第二级配置器的做法是,如果区块够大,超过 128 bytes 时,就移交 第一级配置器处理。当区块小于 128 bytes 时,则以 内存池 管理,此法 又称为次层配置:每次配置一大块内存,并维护对应的自由链表(free-lists)。下次若再有 相同大小的内存需求,就直接从 free-lists 中拨出。如果客户端 释还 小额区块,就由配置器回收到 free-lists 中
为了方便管理,SGI 第二级配置器 会主动将任何小额区块的内存需求量 上调至 8 的倍数(例如客户端要求 30 bytes,就自动调整为 32 bytes),并维护 16 个 free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes 的小额区块。free-lists 的节点结构如下:
// union 允许在同一块内存中存储不同的数据类型,但每次只能存储其中的一种类型
union obj {
union obj free_list_link;
char client_data[1]; /* The client sees this. */
};
由于 union 的特性,允许通过不同的视角 访问同一片内存。基于 union 的这种特性,obj 可以被解释为一个结构体,其中:
从第一个字段的角度看,obj 可以被视为一个指针,指向另一个相同类型的对象,从而形成链表结构
从第二个字段的角度看,obj 可以被视为一个指向实际内存块的指针,用于访问或操作该块中的数据
这种 “一物二用” 的方式充分利用了 union 的内存共享机制,使得在 同时维持链表结构和存储数据时,避免了 因维护链表指针而产生的额外内存开销(不可能同时出现)。这种设计 有效地节省了内存,同时提供了 灵活的内存管理方式
enum {
_ALIGN = 8}; // 小型区块的上调边界
enum {
_MAX_BYTES = 128}; // 小型区块的上限
enum {
_NFREELISTS = _MAX_BYTES/_ALIGN}; // free-lists 个数
// 使用 enum 定义的值是在编译时就确定的常量,不占用内存空间,它们直接被替换为相应的值。这意味着它们不会在程序运行时消耗额外的内存
// 相比之下,使用 const int(特别是在早期 C++ 标准中),编译器可能会为每个 const 变量分配内存(虽然现代编译器对此进行了优化)
// 使用 enum 定义常量时,这些常量只会在当前的作用域中生效。如果你在类或函数中定义了这些 enum,它们不会污染全局命名空间
// 相反,使用 #define 或全局变量(例如 const int),可能会导致命名冲突或不可预期的行为
// 以下是第二级配置器
// 注意,无 “模板型别参数(typename)”,且第二参数完全没派上用场
// 第一参数用于多线程环境下。本书不讨论多线程环境
template <bool threads, int inst>
class __default_alloc_template {
private:
// ROUND_UP() 将 bytes 上调至 8 的倍数
// 对齐(alignment) 是指将内存地址调整为某个固定大小的倍数,通常是为了提高内存访问的效率或满足硬件要求
// ((bytes) + _ALIGN - 1):可以确保即使 bytes 是未对齐的,最终结果 也会至少大于或等于下一个对齐边界
// & -(_ALIGN - 1):这是通过按位与(&)操作实现对齐。
// -(_ALIGN - 1) 的结果是 _ALIGN 边界的负数,它通过按位与操作将 bytes 舍入到最接近的 _ALIGN 倍数
// 例如,如果 _ALIGN = 8,则 -(_ALIGN - 1) 变成 -7,其二进制表示为 1111...11111000(假设32位系统)。这个掩码用于舍去低 3 位(_ALIGN - 1 是 7,对应的低 3 位是 111),从而得到一个 8 字节对齐的数值
static size_t ROUND_UP(size_t bytes) {
return ((bytes) + _ALIGN-1) & -(_ALIGN - 1);
}
private:
union obj {
// free-lists 的节点构造
union obj *free_list_link;
char client_data[1]; // The client sees this.
};
private:
// 16 个 free-lists
static volatile obj *free_list[_NFREELISTS];
// static 关键字表示该变量的生命周期是从程序开始到结束
// 如果在类成员中使用,意味着 free_list 是一个类的静态成员,即它与类的所有对象实例共享,所有实例都访问相同的 free_list 数组
// volatile 关键字表示指针指向的对象可能会被程序之外的因素(如硬件中断、多个线程等)改变,编译器不能对其进行优化
// 这告诉编译器不要假设这个变量的值是稳定的,每次访问时都必须从内存中读取最新的值,而不是从寄存器中读取缓存的值
// 以下函数根据区块大小,决定使用第 n 号 free-list。n 从 1 起算
static size_t FREE_LIST_INDEX(size_t bytes) {
return ((bytes) + _ALIGN-1)/_ALIGN - 1;
}
// 返回一个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free list
static void *refill(size_t nj);
// 配置一大块空间,可容纳 nobjs 个大小为 "size" 的区块
// 如果配置 nobjs 个区块有所不便,nobjs 可能会降低
static char *chunk_alloc(size_t size, int &nobjs);
// Chunk allocation state
static char *start_free; // 内存池起始位置。只在 chunk_alloc() 中变化
static char *end_free; // 内存池结束位置。只在 chunk_alloc() 中变化
static size_t heap_size;
public:
static void * allocate(size_t n) {
/* 详述于后 */ }
static void deallocate(void *p, size_t n) {
/* 详述于后 */ }
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
// 以下是 static data member 的定义与初始化设定
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj * volatile
__default_alloc_template<threads, inst>::free_list[_NFREELISTS] =
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
2.7 空间配置函数 allocate()
身为一个配置器,__default_alloc_template 拥有配置器的标准接口函数 allocate()。此函数 首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free_list。如果 free_list 之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数边界,然后调用 refill(),准备为 free_list 重新填充空间
// n must be > 0
static void * allocate(size_t n)
{
obj * volatile * my_free_list;
obj * result;
// 大于 128 就调用第一级配置器
if (n > (size_t) _MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
// 寻找 16 个 free lists 中适当的一个
my_free_list = free_list + FREE_LIST_INDEX(n);
// FREE_LIST_INDEX(n):计算给定大小的内存 n 应该使用哪一个 free list
result = *my_free_list;
if (result == 0) {
// 没找到可用的 free list,准备重新填充 free list
void *r = refill(ROUND_UP(n));
return r;
}
// 调整 free list
*my_free_list = result -> free_list_link; // 用于在链表中遍历和维护空闲内存块
return (result);
};
2.9 重新填充 free lists
当 allocate() 发现 free list 中没有可用块了时,就调用 refill(),准备为 free list 重新填充空间。新的空间 将取自内存池(经由 chunk_alloc() 完成)。缺省取得 20 个新节点(新区块),但万一内存池空间不足,获得的节点数(区块数)可能小于 20:
// 返回一个大小为 n 的对象,并且有时候会为适当的 free list 增加节点
// 假设 n 已经适当上调至 8 的倍数
// 重新填充(refill)某个大小为 n 字节的内存块的空闲链表(free list),当链表没有足够的可用空间时调用
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
// threads 用于指示是否启用线程安全机制,inst 是另一个模板参数,可能用于区分不同的分配器实例
{
int nobjs = 20; // 计划分配的内存块数量
// 调用 chunk_alloc(),尝试取得 nobjs 个区块作为 free list 的新节点
// 注意参数 nobjs 是 引用传递(可能返回更少数量的区块)
char *chunk = chunk_alloc(n, nobjs); // 尝试分配 nobjs 个大小为 n 字节的区块
obj *volatile *my_free_list;
obj *result;
obj *current_obj, *next_obj;
int i;
// 如果只获得一个区块,这个区块就分配给调用者用,free list 无新节点
// 意味着没有多余的区块可以放入空闲链表
if (1 == nobjs) return(chunk);
// 找到大小为 n 字节的空闲链表的位置,后续步骤将把新分配的区块加入到这个链表中
my_free_list = free_list + FREELIST_INDEX(n);
// 以下在 chunk 空间内建立 free list
result = (obj *)chunk; // 这一块准备返回给客户端
// 以下导引free list 指向新配置的空间(取自内存池)
// 让 my_free_list 指向分配的第一个可用块之后的那个块,即第一个区块(chunk + n)作为空闲链表的新头。
// 这个链表的第一个块已经分配给客户端,剩余的块开始组成新的链表节点
*my_free_list = next_obj = (obj *)(chunk + n);
// 以下将 free list 的各节点串接起来
// current_obj 是当前区块,next_obj 是下一个区块的地址。每次循环将当前区块的 free_list_link 指针指向下一个区块
// 当循环到倒数第一个块时,链表的最后一个节点(current_obj)的 free_list_link 设置为 0,表示这是链表的终点
for (i = 1; ; i++) {
// 从1 开始,因为第0 个将返回给客户端
current_obj = next_obj;
next_obj = (obj *)((char *)next_obj + n);
if (nobjs - 1 == i) {
current_obj -> free_list_link = 0;
break;
}
else {
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
2.10 内存池(memory pool)
从内存池中取空间 给 free list 使用,是 chunk_alloc() 的工作:
// 假设 size 已经适当地上调至 8 的倍数
// 注意参数 nobjs 是 pass by reference
// 负责从内存池中分配一系列 size 大小的内存块
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::
chunk_alloc(size_t size, int& nobjs)
{
char * result;
size_t total_bytes = size * nobjs;
size_t bytes_left = end_free - start_free; // 内存池剩余空间
if (bytes_left >= total_bytes) {
// 内存池剩余空间完全满足需求量
result = start_free;
start_free += total_bytes;
return(result);
}
} else if (bytes_left >= size) {
// 内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块,调整 nobjs 为实际可分配的块数
nobjs = bytes_left/size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return(result);
} else {
// 内存池剩余空间连一个区块的大小都无法提供
// 如果内存池的剩余空间不足以分配一个块,就需要从堆上请求新的内存
// 请求至少两倍于当前需要的 total_bytes,并且加上一个调整量 ROUND_UP(heap_size >> 4),这个调整量可能是为了优化内存分配,避免频繁的内存请求
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
// 以下试着让内存池中的残余零头还有利用价值
if (bytes_left > 0) {
// 内存池内还有一些零头,先配给适当的free list
// 如果内存池中仍有一些剩余的空间(称为“零头”),虽然不足以分配一个块,仍可以将这些零碎的内存放入对应大小的 free list(空闲链表)中,供以后分配使用
// 首先寻找适当的free list,my_free_list 是指向对应大小的空闲链表的指针,FREELIST_INDEX(bytes_left) 用来找到适当的空闲链表
obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
// 调整free list,将内存池中的残余空间编入,将当前零散内存链接到 free list 的链表头之前
((obj *)start_free)->free_list_link = *my_free_list;
// 更新 my_free_list,使其指向新的链表头。即 my_free_list(空闲链表的头指针)现在指向刚刚插入的零散内存块(start_free)
*my_free_list = (obj *)start_free;
}
// 补充内存池,尝试通过 malloc 从堆中分配 bytes_to_get 大小的新内存
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free) {
// heap空间不足,malloc()失败
int i;
// 试着检视我们手上拥有的东西.这不会造成伤害.我们不打算尝试配置较小的块,
// 因为那在多进程(multi-process)机器上容易导致灾难
// 搜寻适当的free list
// 所谓适当是指 “尚有未用区块,且区块够大”之free list
for (i = size; i <= _MAX_BYTES; i += __ALIGN) {
// 遍历从 size 到最大块大小 _MAX_BYTES 的空闲链表,检查是否有可用的空闲块
// _ALIGN: 内存对齐大小,通常也是一个预定义的常量,比如 8 字节或 16 字节。这个值决定了内存块大小的步进(递增量)
my_free_list = free_list + FREELIST_INDEX(i);
// free_list 是维护不同大小的空闲内存块的链表数组,每个链表维护特定大小的内存块
p = my_free_list;
if (0 != p) {
// free list内尚有未用区块
// 调整free list以释出未用区块,将链表中的第一个节点(即 p)从链表中移除,因为这块内存即将分配给当前的请求
*my_free_list = p->free_list_link;
start_free = (char *)p;
end_free = start_free + i;
// 递归调用自己,因为现在已经有足够的内存空间(从 free list 回收的内存),需要重新执行分配逻辑来正确分配 nobjs 个块
return(chunk_alloc(size, nobjs));
// 注意,任何残余零头最终将被编入适当的free-list中备用
}
}
end_free = 0; // 如果出现意外(山穷水尽,到处都没内存用了)
// 调用第一级配置器,看看 out-of-memory 机制能否尽力
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
// 这会导致抛出异常(exception),或内存不足的情况获得改善
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
// 递归调用自己,因为现在已经有足够的内存空间(从 free list 回收的内存),需要重新执行分配逻辑来正确分配 nobjs 个块
return(chunk_alloc(size, nobjs));
}
}
上述的 chunk_alloc() 函数以 end_free – start_free 来判断内存池的水量。如果水量充足,就直接调出 20 个区块返回给 free list 。如果水量 不足以提供 20 个区块,但还足够 供应一个以上的区块,就拨出这不足 20 个区块的空间出去。这时候其 引用传入 的 nobjs 参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,此时便需利用 malloc() 从 heap 中配置内存,为内存池注人活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量
假设程序一开始,客户端就调用 chunk_alloc(32,20)
,于是 malloc() 配置 40 个 32 bytes 区块(1个区块都满足不了,heap_size为0,使用malloc处理),其中第 1 个交出,另 19 个交给 free_list[3] 维护,余 20 个留给内存池
假设系统使用 8 字节对齐(_ALIGN = 8),并且内存块大小最大为 _MAX_BYTES = 128,则 free_list 的大小可能是 (128 / 8) = 16,对应管理以下大小的内存块:
free_list[0] 管理 8 字节大小的内存块
free_list[1] 管理 16 字节大小的内存块
free_list[2] 管理 24 字节大小的内存块
free_list[3] 管理 32 字节大小的内存块
依此类推,直到 free_list[15] 管理 128 字节的内存块
接下来 客户端调用 chunk_alloc(64,20)
,此时 free_list[7] 空空如也,必须向内存池要求支持。内存池只够供应 (32*20) / 64 = 10 个 64 bytes 区块,就把这 10 个区块返回,第 1 个交出,余 9 个由 free_list[7] 维护。此时内存池全空。接下来再调用 chunk_alloc(96, 20)
,此时 free_list[11] 空空如也,必须向内存池要求支持,而内存池 此时也是空的,于是以 malloc() 配置 40+n(附加量)个 96 bytes 区块,其中第 1 个交出,另 19 个交给 free_list[11] 维护,余 20+n(附加量)个区块留给内存池…
万一山穷水尽,整个 system heap 空间都不够了(以至无法为 内存池注人活水源头),malloc() 行动失败,chunk_alloc() 就四处寻找 有无“尚有未用区块,且区块够大”之 free lists 。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器 也是使用 malloc() 来配置内存,但它有 out-of-memory 处理机制(类似 new-handler 机制, oom_malloc():内存不足处理),或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则发出 bad_alloc 异常
SGI 容器通常以这种方式来使用配置器,将一个模板类与一个可替换的内存配置器(allocator)结合起来
template <class T, class Alloc = alloc> // 缺省使用 alloc 为配置器
class vector {
public:
typedef T value_type;
...
protected:
// 专属之空间配置器,每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
...
};
其中第二个 template 参数所接受的缺省参数 alloc,可以是第一级配置器,也可以是第二级配置器。不过,SGI STL 已经把它设为第二级配置器
使用第一级配置器的例子:
// 第一级配置器的实现
#include <cstdlib> // for malloc and free
#include <new> // for bad_alloc
#include <iostream> // for debugging output
// 定义一个简单的第一级配置器
template <typename T>
class FirstLevelAllocator {
public:
// 类型定义,方便后续使用
typedef T value_type;
// 分配内存
T* allocate(size_t n) {
T* result = static_cast<T*>(std::malloc(n * sizeof(T)));
if (!result) {
throw std::bad_alloc(); // 如果分配失败,抛出异常
}
std::cout << "Allocated " << n * sizeof(T) << " bytes using FirstLevelAllocator\n";
return result;
}
// 释放内存
void deallocate(T* p, size_t n) {
std::free(p);
std::cout << "Deallocated " << n * sizeof(T) << " bytes using FirstLevelAllocator\n";
}
};
// 使用自定义第一级配置器的 vector
#include <vector>
// 定义一个简单的 Vector 类,支持使用自定义配置器
// 这是一个简单的 vector 实现,接受一个自定义的内存配置器(默认为 std::allocator<T>)。通过传递不同的配置器类型,我们可以决定内存分配的方式
template <typename T, typename Alloc = std::allocator<T>>
class MyVector {
public:
typedef T value_type;
typedef Alloc allocator_type;
typedef T* pointer;
// 使用自定义的配置器
explicit MyVector(size_t n) {
data_allocator = allocator_type();
start = data_allocator.allocate(n); // 使用自定义配置器分配内存
finish = start + n;
std::cout << "Vector of size " << n << " created.\n";
}
~MyVector() {
data_allocator.deallocate(start, finish - start); // 释放内存
std::cout << "Vector destroyed.\n";
}
private:
pointer start; // 指向起始位置的指针
pointer finish; // 指向结束位置的指针
allocator_type data_allocator; // 使用自定义的分配器
};
int main() {
// 使用默认配置器(std::allocator)
MyVector<int> default_vec(10);
// 使用自定义的第一级配置器
MyVector<int, FirstLevelAllocator<int>> custom_vec(10);
return 0;
}
输出
Vector of size 10 created.
Allocated 40 bytes using FirstLevelAllocator
Deallocated 40 bytes using FirstLevelAllocator
Vector destroyed.
当 创建 custom_vec 时,调用了自定义的第一级配置器,它使用 malloc 分配了 40 个字节(10 个 int,每个 int 通常是 4 字节)。
当 custom_vec 被销毁时,它使用 free 释放了 40 个字节
default_vec 使用的是标准的 std::allocator,并没有显示任何调试输出,因为标准的分配器不会输出调试信息
3、内存基本处理工具
STL 定义有五个全局函数,作用于未初始化空间上。前两个函数是 用于构造的 construct() 和 用于析构的 destroy(),另三个函数是 uninitialized_copy(), uninitialized_fill(), uninitialized_fill_n(),分别对应于高层次函数 copy()、fill()、fill_n()——这些都是 STL 算法。如果你要使用本节的三个低层次函数,应该包含 <memory>
,不过 SGI 把它们实际定义于 <stl_uninitialized>
3.1 uninitialized_copy
template <class InputIterator, class ForwardIterator>
ForwardIterator uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result);
uninitialized_copy() 使 能够将内存的配置 与 对象的构造行为分开来。如果 作为输出目的地 [result, result + (last - first)]
范围内的 每一个迭代器都指向未初始化区域,则 uninitialized_copy() 会使用 拷贝构造函数,给 输入来源 [first,last) 范围内的每一个对象 产生一份复制品,放进输出范围范围中
即:针对输入范围内的每一个迭代器 i,该函数会调用 construct(&*(result + (i - first), *i)
(result 是输出迭代器,指向一块尚未初始化的内存区域,表示将要把对象复制到的目标地址的起始位置;construct() 是一个辅助函数 或 函数模板,用于在指定的内存地址上 构造对象,通常是 调用对象的构造函数(特别是拷贝构造函数),产生 *i 的复制品,放置于 输出范围的相对位置上
容器的全区间构造函数(range constructor)通常以两个步骤完成:
- 配置内存区块,足以包含范围内的所有元素
- 使用 uninitialized_copy(),在该内存区块上构造元素
uninitialized_copy() 要么“构造出所有必要元素”,要么 (当有任何一个 copy constructor 失败时) “不构造任何东西”
3.2 uninitialized_fill
template <class ForwardIterator, class T>
void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x);
uninitialized_fill() 也能够使我们 将内存配置与对象的构造行为分离来。如果 [first, last) 范围内的每个迭代器 都指向未初始化的内存,那么 uninitialized_fill() 会在该范围内产生 x(上式第三参数)的复制品
uninitialized_fill() 会针对操作范围内的每个迭代器 i,调用 construct(&*i, x)
,在 i 所指之处产生 x 的复制品
与 uninitialized_copy() 一样,uninitialized_fill() 必须 要么产生出所有必要元素,要么不产生任何元素。如果有任何一个 copy constructor 丢出异常,uninitialized_fill(),必须能够 将已产生的所有元素析构掉
3.3 uninitialized_fill_n
template <class ForwardIterator, class Size, class T>
ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x);
uninitialized_fill_n() 能够使 将内存配置与对象构造行为分离开来。它会为指定范围内的所有元素 设定相同的初值
面对 [first,first+n) 范围内的每个迭代器 i,uninitialized_fill_n() 会调用 construct(&*i, x)
,在对应位置处产生 x 的复制品
uninitialized_fill_n() 也是 要么产生所有必要的元素,否则就不产生任何元素。如果有任何一个 copy constructor 丢出异常,uninitialized_fill_n() 必须析构已产生的所有元素
迭代器 first 指向欲初始化空间的起始处
n 表示欲初始化空间的大小
x 表示初值
首先萃取出迭代器 first 的 value type,然后判断该型别是否为 POD 型别:
template <class ForwardIterator, class Size, class T, class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T&x, T1*)
{
typedef typename __type_traits<T1>::is_POD_type is_POD;
return _uninitialized_fill_n_aux(first, n, x, is_POD());
}
POD 意指 Plain Old Data;也就是标量型别 或 传统的 C struct 型别,可以被认为是“普通的”或“简单的”对象类型,因而可以采用一些更高效的操作方式
POD 类型具有以下特性:
1)标量类型或传统的 C struct 类型:
标量类型包括基础的内置类型,例如 int、char、float、指针等
传统的 C 结构体类型是指 只包含 POD 类型成员的 struct,没有复杂的类特性(如构造函数、析构函数、虚函数等)
2)无用户定义的构造函数和析构函数:
POD 类型必须拥有 平凡的构造函数 和 平凡的析构函数
平凡的构造函数 指的是 编译器生成的默认构造函数,不执行任何特殊操作(如初始化或分配资源)
平凡的析构函数 也是由编译器生成,不进行任何清理操作(如释放资源)
3)平凡的拷贝构造函数和赋值操作符:
POD 类型的拷贝构造函数和赋值操作符 也必须是平凡的,这意味着 它们可以通过逐位复制 来实现
逐位复制意味着内存中的每一位 都可以直接复制,而不需要调用任何特殊的构造、析构、拷贝或赋值逻辑
4)没有虚函数或虚基类:
POD 类型 不能拥有虚函数或虚基类,原因是虚函数表 会破坏它们的内存布局,使它们不再是“简单”的数据类型
5)标准布局类型:
POD 类型的布局在内存中是标准的,这意味着它们的内存布局与 C 语言的结构体是兼容的。
这使得 POD 类型可以与 C 代码无缝协作,也使得它们可以通过 memcpy、memmove 等低级内存操作来进行高效复制
为什么 POD 类型重要?
POD 类型的重要性在于它们允许使用一些非常高效的操作,因为编译器知道这些类型的行为是简单的,符合传统的内存模型。例如:
高效的内存初始化:
对于 POD 类型,可以通过 memset 或者 memcpy 这样的低级别函数来初始化或复制对象,因为这些对象不需要特殊的构造函数 或 析构函数来管理其生命周期
例如,初始化 POD 类型的数组时,可以直接将内存块初始化为 0,代替调用构造函数,这非常高效
高效的对象拷贝:
由于 POD 类型的拷贝操作是平凡的,可以直接使用内存复制 来完成,不需要 调用拷贝构造函数。对于非 POD 类型,这样的操作会变得复杂,因为它们可能涉及资源分配和管理
POD 型别必然拥有 平凡的构造函数、析构函数、拷贝构造函数和赋值运算符,因此,可以对 POD 型别采用最有效率的初值填写手法,而对 non-POD 型别采取最保险的做法:
// 如果是 POD 型别,执行流程就会转进到以下函数。这是藉由 函数模板的参数推导机制而得
template <class ForwardIterator, class Size, class T>
inline ForwardIterator
__uninitialized_fill_n_aux(ForwardIterator first, Size n,
const T& x, __true_type) {
return fill_n(first, n, x); // 交由高阶函数执行
}
// 如果不是 POD 型别,执行流程就会转进到以下函数。这是藉由 函数模板的参数推导机制而得
template <class ForwardIterator, class Size, class T>
ForwardIterator
fill_n_aux(ForwardIterator first, Size n,
const T& x, __false_type) {
ForwardIterator cur = first;
// 将原本该有的异常处理(exception handling)省略
for ( ; n > 0; --n, ++cur)
construct(&*cur, x);
return cur;
}
(2) unitialized_copy
- 迭代器 first 指向输入端的起始位置
- 迭代器 last 指向输入端的结束位置(前闭后开区间)
- 迭代器 result 指向输出端(欲初始化空间)的起始处
template <class InputIterator, class ForwardIterator>
inline ForwardIterator
unitialized_copy(InputIterator first, InputIterator last, ForwardIterator result) {
return _unitialized_copy(first, last, result, value_type(result));
// 利用 value_type() 取出 first 的 value type
}
首先萃取出迭代器 result 的 value type,然后判断 该型别是否为 POD 型别:
template <class InputIterator, class ForwardIterator, class T>
inline ForwardIterator
__unitialized_copy(InputIterator first, InputIterator last, ForwardIterator result, T*) {
typedef typename __type_traits<T>::is_POD_type is_POD;
return _unitialized_copy_aux(first, last, result, is_POD());
// 利用 is_POD() 所获得的结果,让编译器做参数推导
}
可以对 POD 型别采用最有效率的初值填写手法,而对 non-POD 型别采取最保险的做法:
// 如果是 POD 型别,执行流程就会转进到以下函数。这是藉由 函数模板 的参数推导机制而得
template <class InputIterator, class ForwardIterator>
inline ForwardIterator
__uninitialized_copy_aux(InputIterator first, InputIterator last,
ForwardIterator result, __true_type) {
return copy(first, last, result); // 调用 STL 算法 copy()
}
// 如果是 non-POD 型别,执行流程就会转进到以下函数。这是藉由 函数模板 的参数推导机制而得
template <class InputIterator, class ForwardIterator>
ForwardIterator
__uninitialized_copy_aux(InputIterator first, InputIterator last,
ForwardIterator result, __false_type) {
ForwardIterator cur = result;
// 将原本该有的异常处理(exception handling)省略
for ( ; first != last; ++first, ++cur)
construct(&*cur, *first); // 必须一个一个元素地构造,无法批量进行
return cur;
}
针对 char* 和 wchar_t* 两种型别,可以采用最具效率的做法 memmove(直接移动内存内容,直接操作内存,复制的是内存块而不是逐个处理每个字符,避免了循环或其他复杂的控制结构)来执行复制行为。因此 SGI 为这两种型别 设计一份特化版本
// 以下是针对 const char* 的特化版本
inline char* uninitialized_copy(const char* first, const char* last, char* result) {
memmove(result, first, last - first);
return result + (last - first);
}
// 以下是针对 const wchar_t* 的特化版本
inline wchar_t* uninitialized_copy(const wchar_t* first, const wchar_t* last, wchar_t* result) {
memmove(result, first, sizeof(wchar_t) * (last - first));
return result + (last - first);
}
(3) uninitialized_fill
- 迭代器 first 指向输出端(欲初始化空间)的起始处
- 迭代器 last 指向输出端(欲初始化空间)的结束处(前闭后开区间)
- x 表示初值
template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x){
__uninitialized_fill(first, last, x, value_type(first));
}
这个函数的进行逻辑是,首先萃取出迭代器 first 的 value type,然后判断该型别是否为 POD 型别:
template <class ForwardIterator, class T, class T1>
inline void _uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x, T1*){
typedef typename __type_traits<T1>::is_POD_type is_POD;
__uninitialized_fill_aux(first, last, x, is_POD());
}
可以对 POD 型别采用最有效率的初值填写手法,而对 non-POD 型别采取最保险的做法:
// 如果是 POD 型别,执行流程就会转进到以下函数。这是藉由 函数模板 的参数推导机制而得
template <class ForwardIterator, class T>
inline void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __true_type) {
fill(first, last, x); // 调用 STL 算法 fill()
}
// 如果是 non-POD型别,执行流程就会转进到以下函数.这是藉由 function template 的参数推导机制而得
template <class ForwardIterator, class T>
void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __false_type) {
ForwardIterator cur = first;
// 为求阅读顺畅,以下将原本该有的异常处理(exception handling)省略
for ( ; cur != last; ++cur)
construct(&*cur, x); // 必须一个一个元素地构造,无法批量进行
}