MemoryPool::MemoryPool(size_t BlockSize)
: BlockSize_ (BlockSize)
, SlotSize_ (0)
, firstBlock_ (nullptr)
, curSlot_ (nullptr)
, freeList_ (nullptr)
, lastSlot_ (nullptr)
{}
成员初始化列表 是 C++ 中的一种语法,用于在构造函数的主体执行之前初始化类的成员变量。它位于构造函数声明的冒号后面,并且在构造函数的主体之前完成对成员变量的初始化。
ClassName::ClassName(Arguments)
: member1(value1), member2(value2)
{
// 构造函数的主体
}
ClassName::ClassName(Arguments)
:这是构造函数的声明。: member1(value1), member2(value2)
:成员初始化列表,表示在构造函数体执行之前,初始化类的成员变量 member1
和 member2
。member1(value1), member2(value2)
:每个成员变量通过成员初始化列表传递的参数进行初始化。效率:
初始化顺序:
初始化常量成员或引用成员:
const
成员或引用成员,必须在成员初始化列表中进行初始化,因为它们无法在构造函数体内进行赋值。避免不必要的默认构造和赋值:
假设有一个 Person
类,包含一个常量成员和一个引用成员:
class Person {
private:
const int id; // 常量成员,必须在初始化列表中初始化
std::string name; // 普通成员
int& age; // 引用成员,必须在初始化列表中初始化
public:
Person(int id, std::string name, int& age)
: id(id), name(name), age(age) { // 使用成员初始化列表初始化成员变量
// 构造函数体
}
void display() {
std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
int age = 30;
Person p(1, "John", age);
p.display(); // 输出: ID: 1, Name: John, Age: 30
}
id
:由于 id
是一个常量成员,它必须在构造函数的成员初始化列表中进行初始化。不能在构造函数体内进行赋值。age
:引用成员必须通过初始化列表进行初始化,因为引用必须在创建时绑定到某个对象,不能在构造函数体内重新赋值。name
:name
是一个普通的成员变量,可以通过成员初始化列表或在构造函数体内进行初始化。对于普通的成员变量,如果不在成员初始化列表中初始化,它们会使用默认构造函数进行初始化。例如:
class Example {
private:
int x; // 默认初始化为 0
std::string str; // 默认构造函数创建一个空字符串
public:
Example() {
// x 和 str 会使用默认构造函数初始化
}
};
const
)时,必须在初始化列表中进行初始化,因为常量成员不能在构造函数体内赋值。assert(size > 0);
assert 是一个断言宏,用于在调试时检查条件是否为真。如果条件不满足(即 size <= 0),程序会停止并输出错误信息。通常用于调试阶段,帮助开发人员捕捉不合逻辑的输入值。确保传入的 size 参数大于零,表示内存池中每个槽位的大小必须是正数。否则,初始化操作可能会导致错误或不一致。
std::lock_guard
std::mutex
std::mutex
是 C++11 引入的一个互斥锁类,它用于在多线程环境中保护共享资源。互斥锁(Mutex,Mutual Exclusion)确保在同一时刻只有一个线程可以访问某个共享资源,避免了多个线程同时操作资源时可能发生的竞态条件(race condition)和数据不一致的问题。mutex
时,其他线程无法再获取这个 mutex
,直到持有它的线程释放它。std::mutex mutexForBlock_;
mutexForBlock_
是一个互斥锁对象,用于在多线程环境下保护与内存池相关的操作,防止多个线程同时访问和修改内存池的分配状态(如分配和释放内存块)。std::lock_guard
std::lock_guard
是一个 RAII(Resource Acquisition Is Initialization)风格的锁管理工具。RAII 是 C++ 中的一种常见设计模式,它通过对象生命周期来管理资源(如内存、文件句柄、锁等)。
RAII 的基本思想是:在对象的构造函数中获取资源,在析构函数中释放资源。std::lock_guard
就是按照这一原则来管理 mutex
的,它在构造时获取锁,在析构时自动释放锁。
std::lock_guard<std::mutex> lock(mutexForBlock_);
std::lock_guard lock(mutexForBlock_);
这一行代码的作用是 加锁,并且确保在当前作用域结束时自动解锁。确保在当前作用域内只有一个线程能访问共享的内存池资源。在作用域结束时,lock
对象会被销毁,锁会自动释放,不需要显式调用 unlock
。std::lock_guard
?自动管理锁:
std::lock_guard
是 RAII 风格的锁对象,它的生命周期管理确保了锁能够自动释放。通过使用 std::lock_guard
,你不需要手动调用 unlock
来释放锁。防止死锁:
std::lock_guard
可以确保即使发生异常,锁也会在作用域结束时被自动释放,从而避免死锁情况。std::lock_guard
,开发者可能会忘记显式地释放锁,导致锁长时间占用,影响其他线程的执行,最终造成死锁。简化代码:
std::lock_guard
使得代码更加简洁和安全。你不需要显式调用 mutex.unlock()
,也不需要手动释放锁。std::lock_guard
来管理,减少了出错的机会。std::lock_guard lock(mutexForBlock_);
时:
lock
对象在构造时会自动 获取锁(即调用 mutexForBlock_.lock()
)。lock
对象将一直存在,直到作用域结束。lock
对象被销毁,析构函数会自动 释放锁(即调用 mutexForBlock_.unlock()
)。std::lock_guard
防止死锁:std::lock_guard
确保锁的自动释放,程序就能有效避免死锁。std::lock_guard
不仅简化了锁的管理,还确保了异常安全,因为无论函数是正常返回还是异常退出,锁都会被释放。std::lock_guard
用法下面是一个简单的多线程示例,展示了如何使用 std::mutex
和 std::lock_guard
来同步访问共享资源:
#include
#include
#include
std::mutex mtx; // 互斥锁,用于保护共享资源
void printNumbers(int threadId)
{
// 加锁
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread " << threadId << ": ";
for (int i = 0; i < 5; ++i)
{
std::cout << i << " ";
}
std::cout << std::endl;
} // 锁会在此作用域结束时自动释放
int main()
{
std::thread t1(printNumbers, 1);
std::thread t2(printNumbers, 2);
t1.join();
t2.join();
return 0;
}
t1
和 t2
,它们会执行 printNumbers
函数。printNumbers
函数中,我们使用 std::lock_guard
来保护共享资源(输出流 std::cout
)。每个线程在执行期间都会加锁,确保两个线程不会同时输出内容,从而避免竞态条件。printNumbers
函数执行完后,lock
对象超出作用域,锁会自动释放,其他线程可以继续访问资源。MemoryPool::padPointer
函数的作用:padPointer
函数用于确保一个指针(char* p
)按指定的对齐边界(align
)对齐。指针对齐是指保证指针指向的内存地址满足某个特定的对齐要求。对于某些类型的内存分配,系统要求内存地址按照某个边界对齐(例如 8 字节、16 字节等),以提高内存访问效率。
padPointer
的实现:size_t MemoryPool::padPointer(char* p, size_t align)
{
// align 是槽大小
return (align - reinterpret_cast<size_t>(p)) % align;
}
reinterpret_cast(p)
reinterpret_cast(p)
将 p
转换为 size_t
类型,这是一个无符号整数类型,可以存储内存地址。这样,我们可以对指针的值(即内存地址)进行数学运算。p
是 char*
类型的指针,这意味着 p
指向的是字节级别的内存地址。(align - reinterpret_cast(p)) % align
align
是指定的对齐要求,通常是一个 2 的幂(例如 8 字节、16 字节等)。(reinterpret_cast(p)) % align
计算 p
当前地址对 align
的余数,表示当前内存地址距离最近的对齐边界的偏移量。例如,如果 align
为 8,而当前地址为 5,则 5 % 8
的结果是 5。align - (reinterpret_cast(p)) % align
计算出要跳过的字节数,才能将指针 p
对齐到 align
边界。例如,当前地址为 5,align
为 8,则需要跳过 3 个字节才能到达最近的 8 字节对齐位置。p
按照 align
对齐。内存对齐是现代计算机系统中的一个优化手段,它要求某些数据类型(例如 int
、double
)的内存地址必须是特定字节数的倍数。例如:
当我们在内存池中分配内存时,如果槽位的大小(SlotSize_
)需要特定的对齐,我们就必须确保分配的内存地址满足该对齐要求,否则会影响内存访问性能。
假设我们要分配一个内存槽,且该槽的大小是 SlotSize_
字节。我们需要确保槽的起始地址是 SlotSize_
的倍数。
例如,如果 SlotSize_ = 8
,并且当前的 p
的地址为 5(即 reinterpret_cast
),我们希望将地址调整为下一个 8 字节边界,即 8。因此,函数会计算:
paddingSize = 8 - (5 % 8) = 8 - 5 = 3
这样,p
将需要跳过 3 个字节,才能对齐到 8 字节边界。
假设我们有一个内存地址 p
,并且希望按照 16 字节对齐。
align = 16
:reinterpret_cast(p) = 10
10 % 16 = 10
paddingSize = 16 - 10 = 6
因此,我们需要跳过 6 个字节,才能将地址对齐到 16 字节边界。
align = 8
:reinterpret_cast(p) = 5
5 % 8 = 5
paddingSize = 8 - 5 = 3
因此,我们需要跳过 3 个字节,才能将地址对齐到 8 字节边界。
HashBucket::getMemoryPool(int index)
函数用于获取一个内存池(MemoryPool
)。该函数实现了一个静态数组来存储多个内存池,使用索引 index
来访问和返回对应的内存池。
MemoryPool& HashBucket::getMemoryPool(int index)
{
static MemoryPool memoryPool[MEMORY_POOL_NUM];
return memoryPool[index];
}
static MemoryPool memoryPool[MEMORY_POOL_NUM];
memoryPool
,该数组的大小为 MEMORY_POOL_NUM
,表示一共有多少个内存池。static
关键字确保该数组在程序生命周期内只会被初始化一次,并且它会在整个类的范围内共享。这意味着:
getMemoryPool
时会被创建并初始化,后续的调用不会重新创建它。MemoryPool memoryPool[MEMORY_POOL_NUM]
声明了一个 MemoryPool
类型的数组,每个元素都是一个 MemoryPool
对象。
MEMORY_POOL_NUM
是一个常量,表示内存池的数量。例如,如果 MEMORY_POOL_NUM = 10
,那么数组会包含 10 个 MemoryPool
对象。return memoryPool[index];
memoryPool
中索引为 index
的元素。memoryPool[index]
表示访问数组中的第 index
个 MemoryPool
对象,index
是传入参数,通常会在调用该函数时提供一个有效的索引值。MemoryPool&
MemoryPool&
,即返回 MemoryPool
类型的引用。这意味着调用者将直接引用 memoryPool[index]
,而不是返回该对象的副本。memoryPool
中相应的内存池对象,而无需复制数据,提高了效率。index
参数index
是一个整数参数,指定要访问的内存池的索引值。index
必须在 0
到 MEMORY_POOL_NUM - 1
的范围内,否则可能会引发越界访问。getMemoryPool(0)
会返回第一个内存池,getMemoryPool(1)
会返回第二个内存池,以此类推。假设:
MEMORY_POOL_NUM = 3
memoryPool[0]
、memoryPool[1]
、memoryPool[2]
那么当你调用:
MemoryPool& pool0 = hashBucket.getMemoryPool(0);
此时,pool0
将引用 memoryPool[0]
,即第一个内存池。
同样,当你调用:
MemoryPool& pool1 = hashBucket.getMemoryPool(1);
此时,pool1
将引用 memoryPool[1]
,即第二个内存池。