MemoryPool.cpp的疑难点

成员初始化列表来初始化类的成员变量

MemoryPool::MemoryPool(size_t BlockSize)
    : BlockSize_ (BlockSize)
    , SlotSize_ (0)
    , firstBlock_ (nullptr)
    , curSlot_ (nullptr)
    , freeList_ (nullptr)
    , lastSlot_ (nullptr)
{}

成员初始化列表(Member Initialization List)

成员初始化列表 是 C++ 中的一种语法,用于在构造函数的主体执行之前初始化类的成员变量。它位于构造函数声明的冒号后面,并且在构造函数的主体之前完成对成员变量的初始化。

基本语法
ClassName::ClassName(Arguments) 
    : member1(value1), member2(value2) 
{
    // 构造函数的主体
}
  • ClassName::ClassName(Arguments):这是构造函数的声明。
  • : member1(value1), member2(value2):成员初始化列表,表示在构造函数体执行之前,初始化类的成员变量 member1member2
  • member1(value1), member2(value2):每个成员变量通过成员初始化列表传递的参数进行初始化。
为什么使用成员初始化列表?
  1. 效率

    • 使用成员初始化列表初始化成员变量,比在构造函数体内进行赋值更加高效。
    • 对于某些复杂类型的成员,成员初始化列表可以避免先调用默认构造函数再进行赋值的步骤,从而提高性能。
  2. 初始化顺序

    • 成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是在成员初始化列表中出现的顺序。因此,成员初始化列表的顺序必须与类定义中成员的声明顺序一致,否则可能会导致逻辑错误。
  3. 初始化常量成员或引用成员

    • 对于 const 成员或引用成员,必须在成员初始化列表中进行初始化,因为它们无法在构造函数体内进行赋值。
  4. 避免不必要的默认构造和赋值

    • 对于非内置类型的成员变量(如类类型成员),在构造函数体内赋值可能会导致先调用默认构造函数,然后再执行赋值操作。使用成员初始化列表,避免了这种额外的操作。
例子

假设有一个 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:引用成员必须通过初始化列表进行初始化,因为引用必须在创建时绑定到某个对象,不能在构造函数体内重新赋值。
  • 普通成员 namename 是一个普通的成员变量,可以通过成员初始化列表或在构造函数体内进行初始化。
构造函数中的默认初始化

对于普通的成员变量,如果不在成员初始化列表中初始化,它们会使用默认构造函数进行初始化。例如:

class Example {
private:
    int x;            // 默认初始化为 0
    std::string str;  // 默认构造函数创建一个空字符串

public:
    Example() {
        // x 和 str 会使用默认构造函数初始化
    }
};

什么时候使用成员初始化列表?

  1. 当类中有常量成员(const)时,必须在初始化列表中进行初始化,因为常量成员不能在构造函数体内赋值。
  2. 当类中有引用成员时,也必须在初始化列表中进行初始化,因为引用成员在对象创建时必须绑定到某个有效的对象。
  3. 当你希望避免默认构造和赋值,尤其是对于类成员变量的复杂类型,初始化列表提供了一种高效的方式。
  4. 为了确保初始化顺序,即按照成员变量声明的顺序初始化。

assert(size > 0);

assert 是一个断言宏,用于在调试时检查条件是否为真。如果条件不满足(即 size <= 0),程序会停止并输出错误信息。通常用于调试阶段,帮助开发人员捕捉不合逻辑的输入值。确保传入的 size 参数大于零,表示内存池中每个槽位的大小必须是正数。否则,初始化操作可能会导致错误或不一致。


std::lock_guard lock(mutexForBlock_);

1. std::mutex
  • std::mutex 是 C++11 引入的一个互斥锁类,它用于在多线程环境中保护共享资源。互斥锁(Mutex,Mutual Exclusion)确保在同一时刻只有一个线程可以访问某个共享资源,避免了多个线程同时操作资源时可能发生的竞态条件(race condition)和数据不一致的问题。
  • 当一个线程持有一个 mutex 时,其他线程无法再获取这个 mutex,直到持有它的线程释放它。
std::mutex mutexForBlock_;
  • mutexForBlock_ 是一个互斥锁对象,用于在多线程环境下保护与内存池相关的操作,防止多个线程同时访问和修改内存池的分配状态(如分配和释放内存块)。
2. 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
3. 为什么使用 std::lock_guard
  • 自动管理锁

    • std::lock_guard 是 RAII 风格的锁对象,它的生命周期管理确保了锁能够自动释放。通过使用 std::lock_guard,你不需要手动调用 unlock 来释放锁。
    • 这有助于避免因为异常或错误导致锁无法释放,从而引发死锁(deadlock)等问题。
  • 防止死锁

    • 使用 std::lock_guard 可以确保即使发生异常,锁也会在作用域结束时被自动释放,从而避免死锁情况。
    • 如果没有使用 std::lock_guard,开发者可能会忘记显式地释放锁,导致锁长时间占用,影响其他线程的执行,最终造成死锁。
  • 简化代码

    • std::lock_guard 使得代码更加简洁和安全。你不需要显式调用 mutex.unlock(),也不需要手动释放锁。
    • 通过 RAII,锁的获取和释放都交由 std::lock_guard 来管理,减少了出错的机会。
4. 如何工作?
  • 当执行 std::lock_guard lock(mutexForBlock_); 时:
    1. lock 对象在构造时会自动 获取锁(即调用 mutexForBlock_.lock())。
    2. 一旦进入当前作用域,lock 对象将一直存在,直到作用域结束。
    3. 作用域结束时,lock 对象被销毁,析构函数会自动 释放锁(即调用 mutexForBlock_.unlock())。
5.如何使用 std::lock_guard 防止死锁:
  • RAII 原则:如果每个线程都遵循统一的锁定顺序,并通过 std::lock_guard 确保锁的自动释放,程序就能有效避免死锁。
  • 使用 std::lock_guard 不仅简化了锁的管理,还确保了异常安全,因为无论函数是正常返回还是异常退出,锁都会被释放。
6. 示例:std::lock_guard 用法

下面是一个简单的多线程示例,展示了如何使用 std::mutexstd::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;
}
解释:
  • 我们创建了两个线程 t1t2,它们会执行 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;
}
1. reinterpret_cast(p)
  • reinterpret_cast(p)p 转换为 size_t 类型,这是一个无符号整数类型,可以存储内存地址。这样,我们可以对指针的值(即内存地址)进行数学运算。
  • pchar* 类型的指针,这意味着 p 指向的是字节级别的内存地址。
2. (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 对齐。

边界对齐的基本概念:

内存对齐是现代计算机系统中的一个优化手段,它要求某些数据类型(例如 intdouble)的内存地址必须是特定字节数的倍数。例如:

  • 8 字节对齐:数据类型的地址必须是 8 的倍数。
  • 16 字节对齐:数据类型的地址必须是 16 的倍数。

当我们在内存池中分配内存时,如果槽位的大小(SlotSize_)需要特定的对齐,我们就必须确保分配的内存地址满足该对齐要求,否则会影响内存访问性能。

对齐的原理:

假设我们要分配一个内存槽,且该槽的大小是 SlotSize_ 字节。我们需要确保槽的起始地址是 SlotSize_ 的倍数。

例如,如果 SlotSize_ = 8,并且当前的 p 的地址为 5(即 reinterpret_cast(p) = 5),我们希望将地址调整为下一个 8 字节边界,即 8。因此,函数会计算:

paddingSize = 8 - (5 % 8) = 8 - 5 = 3

这样,p 将需要跳过 3 个字节,才能对齐到 8 字节边界。

示例:

假设我们有一个内存地址 p,并且希望按照 16 字节对齐。

1. 当前地址为 10,align = 16
  • reinterpret_cast(p) = 10
  • 10 % 16 = 10
  • paddingSize = 16 - 10 = 6

因此,我们需要跳过 6 个字节,才能将地址对齐到 16 字节边界。

2. 当前地址为 5,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];
}
1. 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 对象。
2. return memoryPool[index];
  • 这个返回语句返回数组 memoryPool 中索引为 index 的元素。
  • memoryPool[index] 表示访问数组中的第 indexMemoryPool 对象,index 是传入参数,通常会在调用该函数时提供一个有效的索引值。
3. 返回类型 MemoryPool&
  • 函数的返回类型是 MemoryPool&,即返回 MemoryPool 类型的引用。这意味着调用者将直接引用 memoryPool[index],而不是返回该对象的副本。
  • 通过引用返回,调用者可以修改 memoryPool 中相应的内存池对象,而无需复制数据,提高了效率。
4. index 参数
  • index 是一个整数参数,指定要访问的内存池的索引值。index 必须在 0MEMORY_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],即第二个内存池。

你可能感兴趣的:(内存池,c++)