C++并发编程学习日记 2025/4/25

C++并发编程学习日记 2025/4/25

今天学习了迭代器的失效问题以及如何通过C++标准库提供的成员方法解决,还学习了通过重载newdelete方法来减少创建/销毁对象的开销,并通过这个方法来编写了一个简单的对象池。


迭代器存在__失效问题__

在插入或者删除数据以后,插入/删除点以后的迭代器全部失效;当插入引起容器扩容时,由于是在其他的非连续的位置开辟了一段内存,整个容器的迭代器都失效了(因为迭代器实质上是对一段连续内存的指针)。

如何解决?需要对插入/删除点的迭代器做更新操作。代码示例如下:

std::vector<int>::iterator it = arr.begin();
 while(it!=arr.end()){
    if (*it % 2 != 0) it = arr.erase(it); //erase或者insert一次以后迭代器就失效了,变成野指针。
    else ++it;
}
for (int v : arr) std::cout << v << " ";
std::cout << std::endl;
it = arr.begin();
for (;it != arr.end();++it) {
    if (*it % 2 == 0) {
        it=arr.insert(it, *it - 1);//it指向原来这个偶数位置的前面,所以后面需要自加一。
        ++it;
    }
}
for (int v : arr) std::cout << v << " ";

C++的vector容器提供了成员方法insert()erase(),这两个方法会返回一个迭代器,帮助用户实现迭代器的更新。

此外也可以完善迭代器的定义,通过定义一个链表结构来保存迭代器,实际上是维持一个不变量防止迭代器被破坏。

重要的重载newdelete

new和delete是对象重要的成员函数,通过重载可以使他高效的开辟内存/释放内存和构造对象/析构对象。newdelete的调用实际上是对重载运算符的成员函数的调用。

newdelete实现的原理:

通过与库函数malloc/free比较考察new/delete实现的原理

  1. malloc按照字节开辟内存,返回的都是void*,需要对返回的内容做类型转换;new开辟内存时需要指定数据类型。
  2. new实际上是基于malloc开辟内存,然后再初始化数据。
  3. malloc开辟内存失败时返回空指针,new开辟内存失败时抛出异常bad_malloc

重载newdelete

根据上述原理我们重载一个简单的运算符,代码示例如下:

//简单数据类型的重载。通过库函数分配/销毁内存,没有进行初始化或者析构函数。
void* operator new(size_t size) {
    void* p = malloc(size);
    if (p == nullptr) throw std::bad_alloc();
    std::cout << "operator address new is " << p << std::endl;
    return p;
}//void*指针直接通过型别推导赋给左值。
void operator delete(void* ptr) {
    std::cout << "operator address delete is " << ptr << std::endl;
    free(ptr);
}
//重载一个数组类型的运算符
void* operator new[](size_t size) {
    void* p = malloc(size);
    if (p == nullptr) throw std::bad_alloc();
    std::cout << "operator address new[] is " << p << std::endl;
    return p;
}
void operator delete[](void* ptr) {
    std::cout << "operator address delete[] is " << ptr << std::endl;
    free(ptr);
}
int main(){
    try{
    int* p = new int;
    delete p;
    int* arr = new int[20];
    delete[] arr;
}
catch (const std::bad_alloc& err) {
    std::cerr << err.what() << std::endl;
}//what()打印错误信息

//Test t(30);
Test *t = new Test();
std::cout << "============" << std::endl;
Test* t1 = new Test[5];
std::cout << "============" << std::endl;
//不能混用 delete t1;,这样只是把t1[0]析构了
delete[] t1;
//不能调用delete[] t,这样会一直析构下去。
delete t;
return 0;
}

我们重载了两种运算符,分别对应单个数据和数组数据。注意我们用打印信息来标识重载后的运算符是否执行,可以通过类似的方法来判定是否有内存泄露。

注意:一般来说newdelete不能混用。但对于编译器内置的简单数据类型或者自定义的非数组类型来说,混用其实没有问题;对于数组类型或者自定义的数组类型,就必须对应:如果用delete析构数组类型,实际上析构的只是arr[0] ;如果用delet[]析构简单类型,就会一直析构下去。


我们通过重载new和delete来实现一个对象池。对象池的思路就是预先分配好一个对象的内存,这个对象是对应数据结构的链表或者数组形式,每一个元素的指针域都指向下一个相邻的元素,在操作时不需要另外构造对象,只需要改变对应元素的数据域,这样极大减少了构造对象的开销;析构时则是用一个指针将想要销毁的对象的指针域指向自己。

代码示例如下:

struct QueueItem {
    QueueItem(T data = T()) :_data(data), _next(nullptr) {}
    //重载一个new和delete方法来减少构造/析构对象带来的巨大开销。这些方法都是静态方法。
    //实现的思路是预先开辟一个链表结构,可以尽量大一点,每一个链表元素的数据域取默认值,
    //指针域指向下一个相邻的链表元素。
    void* operator new(size_t size) {
        if (itempool == nullptr) {
            itempool = (QueueItem*)new char[POOL_ITEM_SIZE * sizeof(QueueItem)];
            QueueItem* p = itempool;
            for (;p < itempool + POOL_ITEM_SIZE - 1;++p) {
                p->_next = p + 1;
            }
            p->_next = nullptr;//初始化链表元素的指针域。
        }
        QueueItem* p = itempool;
        itempool = itempool->_next;
        return p;
    }
    void operator delete(void* ptr) {
        QueueItem* p = (QueueItem*)ptr;
        p->_next = itempool;
        itempool = p;
    }//
    T _data;
    QueueItem* _next;
    static QueueItem* itempool;//在结构体外初始化
    static const long int POOL_ITEM_SIZE = 100000;
};

你可能感兴趣的:(C++并发编程,c++,学习,开发语言)