解决 LRU 缓存中的"堆使用后释放"问题
力扣:https://leetcode.cn/problems/lru-cache/?envType=study-plan-v2\&envId=top-100-liked
一、问题背景
最近在实现一个 LRU(Least Recently Used)缓存时,遇到了一个棘手的问题。LRU 缓存是一种常见的缓存淘汰策略,用于在缓存容量满时,淘汰最长时间未被使用的数据。我的实现使用了 list
和 unordered_map
,但在测试时,AddressSanitizer 报告了一个"堆使用后释放"(heap-use-after-free)的错误。这个问题让我困扰了很久,但最终我找到了解决方法,并从中学习到了很多。奇怪的是,同样的代码,牛客测试可以通过,力扣不能通过。
二、问题描述
LRU 缓存的实现中,list
用于维护访问顺序,unordered_map
用于快速查找。list
存储键值对 (key, value)
,unordered_map
存储键到 list
中节点的迭代器。这样可以快速找到某个键对应的节点,并将其移动到 list
的头部或从尾部移除。
然而,在测试时,AddressSanitizer 报告了一个错误:
ERROR: AddressSanitizer: heap-use-after-free on address 0x503000002304 at pc 0x555c98686c24 bp 0x7ffeacde9f10 sp 0x7ffeacde9f08
READ of size 4 at 0x503000002304 thread T0
这个错误表明代码试图访问一个已经被释放的内存地址。问题的根本原因是 unordered_map
中的迭代器在某些情况下失效了。
三、问题分析
(一)get
方法中的问题
在 get
方法中,代码试图通过 unordered_map
找到某个键对应的节点,并将其移动到 list
的头部:
cpp
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));
问题在于,datalists.erase(mp[key])
会移除 list
中的节点,导致 mp[key]
指向的迭代器失效。如果后续代码试图通过 mp[key]
访问节点,就会导致访问已释放内存的错误。
(二)put
方法中的问题
在 put
方法中,当缓存已满时,会移除 list
的尾部节点:
cpp
mp.erase(datalists.back().first);
datalists.pop_back();
这里,datalists.pop_back()
会释放尾部节点的内存。如果在此之前有某个键的迭代器指向了这个尾部节点,那么这个迭代器就会失效。
四、解决方法
(一)修复 get
方法
在 get
方法中,移除节点后,需要更新 unordered_map
中的迭代器,确保它指向新的节点:
cpp
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();
通过 mp[key] = datalists.begin();
,将迭代器更新为指向新插入的节点。
(二)修复 put
方法
在 put
方法中,移除尾部节点后,需要确保 unordered_map
中没有指向已释放节点的迭代器:
cpp
if (mp.find(key) != mp.end()) {
datalists.erase(mp[key]);
} else if (datalists.size() == cap) {
mp.erase(datalists.back().first);
datalists.pop_back();
}
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();
通过 mp[key] = datalists.begin();
,将迭代器更新为指向新插入的节点。
五、完整的修复代码
以下是修复后的完整代码:
cpp
class LRUCache {
public:
int cap;
list<pair<int, int>> datalists;
unordered_map<int, list<pair<int, int>>::iterator> mp;
LRUCache(int capacity) {
cap = capacity;
}
int get(int key) {
if (mp.find(key) != mp.end()) {
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();
return value;
} else {
return -1;
}
}
void put(int key, int value) {
if (mp.find(key) != mp.end()) {
datalists.erase(mp[key]);
} else if (datalists.size() == cap) {
mp.erase(datalists.back().first);
datalists.pop_back();
}
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();
}
};
六、测试用例
为了验证修复后的代码是否正确,我运行了以下测试用例:
cpp
LRUCache cache(2);
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得 key=2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得 key=1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
运行结果表明,修复后的代码能够正确处理各种情况,没有再出现"堆使用后释放"的错误。
七、总结
通过这次经历,我深刻认识到在使用 list
和 unordered_map
时,必须确保迭代器的有效性。在修改 list
的内容后,要及时更新 unordered_map
中的迭代器,避免访问已释放的内存。同时,我也学会了如何使用 AddressSanitizer 来检测内存问题,这对我今后的开发工作非常有帮助。
希望这篇文章能帮助到遇到类似问题的开发者。