使用C++实现LRU缓存机制,并解决可能存在的内存泄漏问题

力扣146. LRU 缓存

前些时日,笔者晚上回到宿舍,听到隔壁同学激烈讨论LRU,那时候笔者还不知道LRU是什么,只听到什么哈希表、双向链表,哈希表和双向链表怎么对应诸如此类的东西,讨论甚是激烈。

于是笔者在想,LRU的实现很复杂吗?带着这个疑问,笔者开始了学习。

关于LRU的基本原理,推荐这一篇文章:

【常见缓存算法原理及其C++实现】LRU篇(包含过期时间详解)

这篇文章给出的代码实现好像有些问题,示例代码并不能运行成功,但是又找不出哪里错了,于是笔者去看了力扣的官方实现。

在看官方的代码实现时,在评论区看到官方使用C++实现的LRU缓存存在内存泄漏的问题。在构造函数中new了两个双向链表的节点,即head和tail。在结束时这两个在堆区new出来的对象要删除,并且在新增加双向链表的节点的时候,也是使用new,因此在最后,创建的双向链表要删除,哈希表也要清空。

可以通过在LRUCache类中添加一个析构函数来释放未释放的资源,以避免内存泄漏。析构函数通常用于在对象销毁时执行清理工作。在析构函数中,你可以删除双向链表中的节点,以及删除哈希表中的键值对,并释放由于创建LRUCache对象而分配的内存。

以下是详细注释的带析构函数的完整C++代码实现:

#include <iostream>
#include<unordered_map>
using namespace std;

//双向链表的存储结构
struct DLinkedNode{
    int key, value;
    DLinkedNode* prev;  //前指针
    DLinkedNode* next;  //后指针

    //构造函数
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr){};
    DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr){};
};

//LRUCache类
class LRUCache{
private:
    //双向链表头结点和尾节点
    DLinkedNode* head;
    DLinkedNode* tail;

    //哈希表, key和双向链表节点的地址
    unordered_map<int, DLinkedNode*> cache;

    //大小和容量
    int size;
    int capacity;

    //一些辅助函数

    //将一个双向链表中的节点在原来的位置断开,模拟删除操作,其实不是真正删除
    //后续还有相关操作改变它的前后指针
    void removeNode(DLinkedNode* node)
    {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    //只在头部添加节点
    void addToHead(DLinkedNode* node)
    {
       //注意这个函数的实现,要先赋值node的前后指针, 否则可能出现指向错误
        node->prev = head;
        node->next = head->next;

        head->next->prev = node;
        head->next = node;  //这个一定是在最后,否则出现使用贡更新之后的head->next操作其他的
    }


    //将一个节点在原链表中断开,并将它移动到头部节点的下一个,
    void moveToHead(DLinkedNode* node)
    {
        removeNode(node);
       /* head->next = node;
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;  //这样写的逻辑是错的*/
        addToHead(node);
    }

public:
    //构造函数
    LRUCache(int _capacity)
    {
        capacity = _capacity;
        size = 0;

        //初始化头尾指针
        head = new DLinkedNode(-1,-1);
        tail = new DLinkedNode(-1,-1);
        head->next = tail;
        tail->prev = head;
    }

    //析构函数在对象销毁时执行清理工作
    //在析构函数中,可以执行删除双向链表中的节点,删除哈希表中的键值对,并释放由于创建LRUCache对象而分配的内存
    ~LRUCache()
    {
        // 释放双向链表中的节点
        DLinkedNode* node = head->next;
        while(node != tail)
        {
            DLinkedNode* nextNode = node->next;
            delete node;
            node = nextNode;
        }

        //清空哈希表
        cache.clear();

        //释放头尾节点
        delete head;
        delete tail;
    }

    //查找key对应的值, 通过哈希表查找key对应的节点在哪,
    //然后通过removeNode将他在双向链表中的原位置断开,其实是断开前后,不用真正删除这个节点
    //只是与原来的断开,移动到新的位置,这个位置在头结点的下一个位置,只要改变它的前后指针就可
    int get(int key)
    {
        //通过哈希表查找这个不存在cache.count(key) == false
        if (!cache.count(key))
        {
            return -1;
        }

        //如果key存在在哈希表中,返回val值,并把这个节点从原来位置断开,放到头结点后面,通过moveToHead实现
        //这时不用修改哈希表,key对应的地址没变
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }

    //向缓存中放数
    void put(int key, int value)
    {
        //如果这个数在哈希表中不存在,那么新创建一个;在创建的时候,需要考虑缓存是否已经满了
        // 如果存在,更新它的值,并移动到头部

        if (!cache.count(key))  //不存在
        {
            DLinkedNode* node = new DLinkedNode(key, value);
            cache[key] = node;  //创建这个节点并更新哈希表

            //将这个节点放在头部节点的后面
            addToHead(node);
            size++;

            //检查是否缓存已经满了
            if (size > capacity)
            {
                //删除尾节点的前一个节点,使用removeHead,由于这个函数没有真正删除,所以要手动删除
                DLinkedNode *removed = tail->prev;
                removeNode(removed);
                cache.erase(removed->key); //哈希表也要删除
                delete removed;
                size--;
            }
        }
        else  //如果存在,更新它的值,并移动到头部
        {
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }
};



int main()
{
    LRUCache *lruCache = new LRUCache(2);
    lruCache->put(1, 100);
    lruCache->put(2, 200);
    cout<<lruCache->get(1)<<endl;


    lruCache->put(3, 300);
    cout<<lruCache->get(2)<<endl;

    // 销毁对象并释放内存
    delete lruCache;

    return 0;
}

注意上述代码中的put函数,如果在缓存中不存在这个key-value节点,就要创建这个节点,更新哈希表,然后把这个节点放到双向链表中的头结点head的下一个节点。他是先往头部添加节点,然后再判断缓存满了没有,满了就删掉尾节点的前一个节点。那么这样可能存在一个问题,就是说,我创建这个节点的时候缓存已经满了,他可能插入不进去。那么应该先判断缓存是否是满的,然后再向缓存中添加,因此put函数的代码可以改成:

  //向缓存中放数
    void put(int key, int value)
    {
        //如果这个数在哈希表中不存在,那么新创建一个;在创建的时候,需要考虑缓存是否已经满了
        // 如果存在,更新它的值,并移动到头部

        if (!cache.count(key))  //不存在
        {
            DLinkedNode* node = new DLinkedNode(key, value);
            cache[key] = node;  //创建这个节点并更新哈希表

            /*//将这个节点放在头部节点的后面
            addToHead(node);
            size++;*/

            //检查是否缓存已经满了
            if (size >= capacity)
            {
                //删除尾节点的前一个节点,使用removeHead,由于这个函数没有真正删除,所以要手动删除
                DLinkedNode *removed = tail->prev;
                removeNode(removed);
                cache.erase(removed->key); //哈希表也要删除
                delete removed;
                size--;
            }

            //将这个节点放在头部节点的后面
            addToHead(node);
            size++;
        }
        else  //如果存在,更新它的值,并移动到头部
        {
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }

在上述代码中的主函数中,最终使用了delete来销毁对象并释放内存,为了避免忘记这一步操作,可以使用智能指针(例如std::shared_ptr)来管理LRUCache对象中的内存,以确保资源在不再需要时得到释放。这样可以避免手动管理内存,从而减少内存泄漏的风险。

以下是如何修改代码以使用std::shared_ptr来管理LRUCache对象:

#include <iostream>
#include <unordered_map>
#include <memory>
using namespace std;

// 双向链表的存储结构
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;

    // 构造函数
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {};
    DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {};
};

// LRUCache类
class LRUCache {
private:
    // 双向链表头结点和尾节点
    DLinkedNode* head;
    DLinkedNode* tail;

    // 哈希表, key和双向链表节点的地址
    unordered_map<int, DLinkedNode*> cache;

    // 大小和容量
    int size;
    int capacity;

    // 一些辅助函数
    // ...

public:
    // 构造函数
    LRUCache(int _capacity) {
        capacity = _capacity;
        size = 0;
        head = new DLinkedNode(-1, -1);
        tail = new DLinkedNode(-1, -1);
        head->next = tail;
        tail->prev = head;
    }

    // 析构函数
    ~LRUCache() {
        DLinkedNode* node = head->next;
        while (node != tail) {
            DLinkedNode* nextNode = node->next;
            delete node;
            node = nextNode;
        }
        cache.clear();
        delete head;
        delete tail;
    }

    // 其他函数...
};

int main() {
    // 使用智能指针来管理LRUCache对象
    shared_ptr<LRUCache> lruCache = make_shared<LRUCache>(2);
    lruCache->put(1, 100);
    lruCache->put(2, 200);
    cout << lruCache->get(1) << endl;

    lruCache->put(3, 300);
    cout << lruCache->get(2) << endl;

    // 当shared_ptr超出作用域时,析构函数会自动释放内存
    return 0;
}

在上述代码中,我们使用std::shared_ptr来管理LRUCache对象。当shared_ptr超出作用域时,析构函数会自动调用,从而释放LRUCache对象及其内部资源,避免了内存泄漏问题。这是一种更安全和方便的方式来管理资源。

补充知识:

1、使用new创建出来的类对象使用析构函数销毁

使用new创建的类对象可以使用析构函数销毁。在C++中,当你使用new操作符来分配对象的内存时,你需要负责手动释放这些内存以避免内存泄漏。通常,你应该使用delete操作符来销毁对象,并在销毁对象之前调用对象的析构函数来执行清理工作。

以下是示例代码,演示如何使用newdelete以及析构函数来创建和销毁类对象:

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }

    void someMethod() {
        std::cout << "Some method of MyClass" << std::endl;
    }
};

int main() {
    // 使用new创建MyClass对象
    MyClass* obj = new MyClass;

    // 调用对象的方法
    obj->someMethod();

    // 销毁对象并释放内存
    delete obj;

    return 0;
}

在上述示例中,我们使用new创建了MyClass对象,然后在不再需要对象时,使用delete销毁对象并释放内存。在销毁对象时,析构函数会被调用来执行清理工作。这是一种手动管理内存的方式,但需要谨慎使用,以避免内存泄漏和悬挂指针等问题。在实际开发中,建议使用智能指针等现代C++特性来自动管理内存,以减少错误和提高代码的健壮性。

2、在C++中,有哪些方法可以创建类对象?

栈上创建对象: 这是最常见的方式,可以通过直接声明对象来创建,对象的生命周期在当前作用域内。例如:

MyClass obj; // 在栈上创建MyClass对象

堆上创建对象: 使用 new 操作符在堆上分配内存并返回指向对象的指针。需要手动释放内存以避免内存泄漏。例如:

MyClass* obj = new MyClass; // 在堆上创建MyClass对象
// ...
delete obj; // 手动释放内存

使用智能指针: 可以使用 std::shared_ptrstd::unique_ptr 来管理对象的生命周期,从而自动释放内存。例如:

std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
// 不需要手动释放内存,当引用计数为零时,对象会自动销毁

作为成员变量: 类对象可以作为其他类的成员变量。这样,当包含它的类对象被创建时,包含的类对象也会被创建。例如:

class MyClass {
public:
    MyClass() {
        // 构造函数代码
    }
};

class AnotherClass {
private:
    MyClass myObject; // MyClass对象作为AnotherClass的成员
public:
    AnotherClass() {
        // 构造函数代码
    }
};

通过复制或移动: 可以通过复制或移动一个已经存在的对象来创建新对象。这通常涉及到拷贝构造函数或移动构造函数的调用。例如:

MyClass originalObj;
MyClass newObj = originalObj; // 复制构造函数被调用,创建一个新对象

这些是常见的创建类对象的方式,每种方法都有其用途和适用场景。你可以根据需求和对象的生命周期来选择适当的方法。注意要妥善处理内存管理,以避免内存泄漏或悬挂指针等问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LRU 缓存是一种常见的缓存淘汰算法,它的设计思想是将最近最少使用的数据从缓存中淘汰出去,以保证缓存的数据都是热点数据,从而提高缓存的命中率。 下面是一个基于双向链表和哈希表实现LRU 缓存C++ 设计实现: ```cpp #include <unordered_map> struct Node { int key; int value; Node* prev; Node* next; Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; class LRUCache { public: LRUCache(int capacity) : size_(capacity), head_(nullptr), tail_(nullptr) {} int get(int key) { auto iter = cache_.find(key); if (iter == cache_.end()) { return -1; } else { Node* node = iter->second; move_to_head(node); return node->value; } } void put(int key, int value) { auto iter = cache_.find(key); if (iter == cache_.end()) { Node* node = new Node(key, value); add_to_head(node); cache_[key] = node; if (cache_.size() > size_) { Node* tail = remove_tail(); cache_.erase(tail->key); delete tail; } } else { Node* node = iter->second; node->value = value; move_to_head(node); } } private: int size_; Node* head_; Node* tail_; std::unordered_map<int, Node*> cache_; void add_to_head(Node* node) { if (head_ == nullptr) { head_ = node; tail_ = node; } else { node->next = head_; head_->prev = node; head_ = node; } } void move_to_head(Node* node) { if (node == head_) { return; } else if (node == tail_) { tail_ = tail_->prev; tail_->next = nullptr; } else { node->prev->next = node->next; node->next->prev = node->prev; } node->next = head_; head_->prev = node; node->prev = nullptr; head_ = node; } Node* remove_tail() { Node* node = tail_; if (head_ == tail_) { head_ = nullptr; tail_ = nullptr; } else { tail_ = tail_->prev; tail_->next = nullptr; } return node; } }; ``` 在这个实现中,我们使用了一个双向链表保存缓存中的数据,并使用一个哈希表来提高查找效率。在 `get` 操作中,如果缓存中不存在目标数据则返回 -1,否则将目标数据移到链表头部并返回其值。在 `put` 操作中,如果缓存中不存在目标数据则创建一个新的节点并将其添加到链表头部,如果缓存已满则删除链表尾部的节点。如果缓存中已存在目标数据则将其值更新并将其移动到链表头部。 需要注意的是,在 `move_to_head` 和 `remove_tail` 操作中,我们需要判断目标节点是否已经在链表的头部或尾部,以避免对空指针进行操作。此外,在每次操作中,我们还需要更新哈希表中对应节点的指针。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值