fclose() 的 free() / delete / delete[] / realloc() 无效?
Invalid free() / delete / delete[] / realloc() for fclose()?
我尝试 run/compile Linux64 上的 OpenTibia 服务器。小调整,编译,一切似乎都很好。然而,Valgrind 说:
==32360== Invalid free() / delete / delete[] / realloc()
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
==32360== by 0x41CF8D: FileLoader::~FileLoader() (fileloader.cpp:49)
==32360== by 0x45DB1B: Items::loadFromOtb(std::string) (itemloader.h:232)
==32360== by 0x4067D7: main (otserv.cpp:564)
==32360== Address 0x8126590 is 0 bytes inside a block of size 568 free'd
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
==32360== by 0x41D268: FileLoader::openFile(char const*, bool, bool) (fileloader.cpp:92)
==32360== by 0x45DB00: Items::loadFromOtb(std::string) (items.cpp:230)
==32360== by 0x4067D7: main (otserv.cpp:564)
现在代码开始了,对于 FileLoader(尤其是析构函数):
/*somewhere in the header*/
FILE* m_file;
FileLoader::FileLoader() {
m_file = NULL;
m_buffer = new unsigned char[1024];
//cache, some cache data
memset(m_cached_data, 0, sizeof(m_cached_data));
}
FileLoader::~FileLoader() {
if(m_file){
fclose(m_file);
m_file = NULL;
}
delete[] m_buffer;
for(int i = 0; i < CACHE_BLOCKS; i++){
if(m_cached_data[i].data)
delete m_cached_data[i].data;
}
}
bool FileLoader::openFile(const char* filename, bool write, bool caching /*= false*/){
if(write) {/*unimportant*/}
else {
unsigned long version;
m_file = fopen(filename, "rb");
if(m_file){
fread(&version, sizeof(unsigned long), 1, m_file);
if(version > 0){/*version is 0*/}
else{
if(caching){
m_use_cache = true;
fseek(m_file, 0, SEEK_END);
int file_size = ftell(m_file);
m_cache_size = min(32768, max(file_size/20, 8192)) & ~0x1FFF;
}
return true;
}
}
else{
m_lastError = ERROR_CAN_NOT_OPEN;
return false;
}
}
}
ItemLoader 只是 FileLoader 的扩展:
class ItemLoader : public FileLoader {/*Overrides nothing*/};
现在到项目中的功能:
int Items::loadFromOtb(std::string file) {
ItemLoader f;
if(!f.openFile(file.c_str(), false, true)){return f.getError();}
//...Loading, processing, reading from file and stuff...
//delete &f; //I tried this but didn't change anything
return ERROR_NONE;
}
问题是,Valgrind 是否指向 fclose 或其他问题?
另请注意,该应用程序使用 libboost(如果这有任何关系的话)。
我尽量做到具体
看起来 FileLoader
的存在是为了做 caching/buffering 的形式。使用 FILE*
或 IOStreams 时无需自己进行缓冲,它们会为您完成。 FileLoader
在其上添加另一个缓冲区。
您可能希望重构 FileLoader
以放弃所有缓冲,只为您的 类 提供序列化功能,同时将所有 I/O 和相关缓冲委托给 FILE*
或IOStreams.
Vagrind 直接向您展示问题——您在同一个 FILE
描述符上调用 fclose
两次:
==32360== Invalid free() / delete / delete[] / realloc()
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
second call ==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
--------->> ==32360== by 0x41CF8D: FileLoader::~FileLoader() (fileloader.cpp:49)
==32360== by 0x45DB1B: Items::loadFromOtb(std::string) (itemloader.h:232)
==32360== by 0x4067D7: main (otserv.cpp:564)
==32360== Address 0x8126590 is 0 bytes inside a block of size 568 free'd
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
first call ==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
--------->> ==32360== by 0x41D268: FileLoader::openFile(char const*, bool, bool) (fileloader.cpp:92)
==32360== by 0x45DB00: Items::loadFromOtb(std::string) (items.cpp:230)
==32360== by 0x4067D7: main (otserv.cpp:564)
第二次调用是在第 49 行的析构函数中,第一次是在第 92 行的 openFile 中。
房间里的大象:Ahrrr。这是C++。为什么要使用文件? fstream 将为您处理这些废话(好吧......其中一些)而不会掉入 C 的剩余部分。
关于错误。
在我看来,Valgrind 说程序正在关闭 m_file 两次。不是个好主意。
为什么程序关闭 m_file 两次?最有可能的答案是 FileLoader 析构函数被调用了两次。
在这里我进入了未定义的领域,因为我所能做的就是从字里行间推断出问题中遗漏的信息。甚至无法关闭此问题并将其指向 What is The Rule of Three?,因为我们无法确定。它可以因为不清楚和无法回答而关闭,所以在它之前...
以下是我的假设:
- m_file 是 FileLoader 的 class 成员。如果不是并且它是全局的,那么这是一个不值得尝试修复的糟糕设计。
- 正在复制
//...Loading, processing, reading from file and stuff...
f
中的某处。请给 f
一个真实的描述性名称。您节省的调试时间可能是您自己的。
- FileLoader 和 ItemLoader 违反了三原则。如果你不知道我在说什么,请阅读上面 link 关于三的规则。严重地。阅读。即使我错了,也请阅读。
这是怎么发生的:
假设您有函数 void doesStuff(ItemLoader loader)
,它在 //...Loading, processing, reading from file and stuff...
的黑洞中被调用。注意 loader 的传递值。这意味着它将被复制并成为一个临时变量,其生命周期受函数范围的限制。
因为 FileLoader 不符合三规则,loader
获取 f
的 m_file
以及 FileLoader 拥有的任何其他成员的副本。
doesStuff
做东西和 returns。 loader
超出范围并被销毁。 ~FileLoader() 被调用。 m_file
不为空,文件已关闭且 m_file
设置为空。顺便说一下,没有必要将它设置为空。它即将消失。
我们 return 调用函数,其中 f
现在有一个无效的 FILE 指针,因为副本刚刚关闭了文件。
如何检验理论:
将 std::cout << "~FileLoader() called. m_file = " << m_file << std::endl;
放在 FileLoader 的析构函数的顶部,以查看它现在被调用的次数与您认为打开它的次数相比。
修复:
使您的对象符合三规则或使它们不可复制并始终通过引用传递。
使 FileLoader 规则三兼容是一项非常重要的工作。 FILE
指针并且不能很好地复制,这让你玩弱指针游戏以确保吸盘在每个人都完成之前不会关闭。
这也是 deleting the copy constructor and assignment operator 的一个很好的例子,因此无法复制 FileLoader。这样编译器可以在你试图做一些愚蠢的事情时警告你,比如当你想通过引用传递时通过值传递。
fstream
根本不复制,防止你在第一时间陷入这种混乱。但如果您不知道删除的函数是什么,它确实会给出神秘的错误消息流。
这里有一段测试代码来展示我认为正在发生的事情:
#include <iostream>
#include <cstdio>
class FILETest
{
public:
FILE* filep;
FILETest(): filep(NULL)
{
std::cout << "FILETest constructor" << std::endl;
}
~FILETest()
{
std::cout << "FILETest destructor" << std::endl;
}
};
void func(FILETest t)
{
(void)t;
}
int main(int argc, char** argv)
{
(void) argc;
(void) argv;
FILETest t;
func(t);
return 0;
}
以及 fstream
如何防止这种情况发生:
#include <iostream>
#include <fstream>
class fstreamtest
{
public:
std::fstream file;
fstreamtest()
{
std::cout << "fstreamtest constructor" << std::endl;
}
~fstreamtest()
{
std::cout << "fstreamtest destructor" << std::endl;
}
};
void func(fstreamtest t)
{
(void)t;
}
int main(int argc, char** argv)
{
(void) argc;
(void) argv;
fstreamtest t;
func(t);
return 0;
}
好吧,前面的不用理会。抱歉误导了,因为我忘了讲 非常重要的 事实:
- 代码 100% 适用于 Win32。我在 Linux64 上编译它。
- FileLoader 的析构函数(是虚拟的)并触发。
我添加了以下内容
FileLoader::~FileLoader() {
std::cout << "destructor fired\n";
if(m_file){
fclose(m_file);
m_file = NULL;
std::cout << "destructor closed file\n";
}
delete[] m_buffer;
std::cout << "destructor deleted buffer\n";
for(int i = 0; i < CACHE_BLOCKS; i++){
if(m_cached_data[i].data)
delete m_cached_data[i].data;
}
std::cout << "destructor deleted cache\n";
}
输出为:
destructor fired
<valgrind crying>
destructor closed file
所以它与缓冲区没有任何关系。我发现了问题,版本实际上不匹配,关闭了文件,但没有 NULL 它。因此实际上是原始代码中的错误。
真正解决问题的方法是Win32 long = Linux64 int。这解决了部分问题。
我尝试 run/compile Linux64 上的 OpenTibia 服务器。小调整,编译,一切似乎都很好。然而,Valgrind 说:
==32360== Invalid free() / delete / delete[] / realloc()
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
==32360== by 0x41CF8D: FileLoader::~FileLoader() (fileloader.cpp:49)
==32360== by 0x45DB1B: Items::loadFromOtb(std::string) (itemloader.h:232)
==32360== by 0x4067D7: main (otserv.cpp:564)
==32360== Address 0x8126590 is 0 bytes inside a block of size 568 free'd
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
==32360== by 0x41D268: FileLoader::openFile(char const*, bool, bool) (fileloader.cpp:92)
==32360== by 0x45DB00: Items::loadFromOtb(std::string) (items.cpp:230)
==32360== by 0x4067D7: main (otserv.cpp:564)
现在代码开始了,对于 FileLoader(尤其是析构函数):
/*somewhere in the header*/
FILE* m_file;
FileLoader::FileLoader() {
m_file = NULL;
m_buffer = new unsigned char[1024];
//cache, some cache data
memset(m_cached_data, 0, sizeof(m_cached_data));
}
FileLoader::~FileLoader() {
if(m_file){
fclose(m_file);
m_file = NULL;
}
delete[] m_buffer;
for(int i = 0; i < CACHE_BLOCKS; i++){
if(m_cached_data[i].data)
delete m_cached_data[i].data;
}
}
bool FileLoader::openFile(const char* filename, bool write, bool caching /*= false*/){
if(write) {/*unimportant*/}
else {
unsigned long version;
m_file = fopen(filename, "rb");
if(m_file){
fread(&version, sizeof(unsigned long), 1, m_file);
if(version > 0){/*version is 0*/}
else{
if(caching){
m_use_cache = true;
fseek(m_file, 0, SEEK_END);
int file_size = ftell(m_file);
m_cache_size = min(32768, max(file_size/20, 8192)) & ~0x1FFF;
}
return true;
}
}
else{
m_lastError = ERROR_CAN_NOT_OPEN;
return false;
}
}
}
ItemLoader 只是 FileLoader 的扩展:
class ItemLoader : public FileLoader {/*Overrides nothing*/};
现在到项目中的功能:
int Items::loadFromOtb(std::string file) {
ItemLoader f;
if(!f.openFile(file.c_str(), false, true)){return f.getError();}
//...Loading, processing, reading from file and stuff...
//delete &f; //I tried this but didn't change anything
return ERROR_NONE;
}
问题是,Valgrind 是否指向 fclose 或其他问题? 另请注意,该应用程序使用 libboost(如果这有任何关系的话)。 我尽量做到具体
看起来 FileLoader
的存在是为了做 caching/buffering 的形式。使用 FILE*
或 IOStreams 时无需自己进行缓冲,它们会为您完成。 FileLoader
在其上添加另一个缓冲区。
您可能希望重构 FileLoader
以放弃所有缓冲,只为您的 类 提供序列化功能,同时将所有 I/O 和相关缓冲委托给 FILE*
或IOStreams.
Vagrind 直接向您展示问题——您在同一个 FILE
描述符上调用 fclose
两次:
==32360== Invalid free() / delete / delete[] / realloc()
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
second call ==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
--------->> ==32360== by 0x41CF8D: FileLoader::~FileLoader() (fileloader.cpp:49)
==32360== by 0x45DB1B: Items::loadFromOtb(std::string) (itemloader.h:232)
==32360== by 0x4067D7: main (otserv.cpp:564)
==32360== Address 0x8126590 is 0 bytes inside a block of size 568 free'd
==32360== at 0x4C2BDEC: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
first call ==32360== by 0x6074AE4: fclose@@GLIBC_2.2.5 (iofclose.c:85)
--------->> ==32360== by 0x41D268: FileLoader::openFile(char const*, bool, bool) (fileloader.cpp:92)
==32360== by 0x45DB00: Items::loadFromOtb(std::string) (items.cpp:230)
==32360== by 0x4067D7: main (otserv.cpp:564)
第二次调用是在第 49 行的析构函数中,第一次是在第 92 行的 openFile 中。
房间里的大象:Ahrrr。这是C++。为什么要使用文件? fstream 将为您处理这些废话(好吧......其中一些)而不会掉入 C 的剩余部分。
关于错误。
在我看来,Valgrind 说程序正在关闭 m_file 两次。不是个好主意。
为什么程序关闭 m_file 两次?最有可能的答案是 FileLoader 析构函数被调用了两次。
在这里我进入了未定义的领域,因为我所能做的就是从字里行间推断出问题中遗漏的信息。甚至无法关闭此问题并将其指向 What is The Rule of Three?,因为我们无法确定。它可以因为不清楚和无法回答而关闭,所以在它之前...
以下是我的假设:
- m_file 是 FileLoader 的 class 成员。如果不是并且它是全局的,那么这是一个不值得尝试修复的糟糕设计。
- 正在复制
//...Loading, processing, reading from file and stuff...
f
中的某处。请给f
一个真实的描述性名称。您节省的调试时间可能是您自己的。 - FileLoader 和 ItemLoader 违反了三原则。如果你不知道我在说什么,请阅读上面 link 关于三的规则。严重地。阅读。即使我错了,也请阅读。
这是怎么发生的:
假设您有函数 void doesStuff(ItemLoader loader)
,它在 //...Loading, processing, reading from file and stuff...
的黑洞中被调用。注意 loader 的传递值。这意味着它将被复制并成为一个临时变量,其生命周期受函数范围的限制。
因为 FileLoader 不符合三规则,loader
获取 f
的 m_file
以及 FileLoader 拥有的任何其他成员的副本。
doesStuff
做东西和 returns。 loader
超出范围并被销毁。 ~FileLoader() 被调用。 m_file
不为空,文件已关闭且 m_file
设置为空。顺便说一下,没有必要将它设置为空。它即将消失。
我们 return 调用函数,其中 f
现在有一个无效的 FILE 指针,因为副本刚刚关闭了文件。
如何检验理论:
将 std::cout << "~FileLoader() called. m_file = " << m_file << std::endl;
放在 FileLoader 的析构函数的顶部,以查看它现在被调用的次数与您认为打开它的次数相比。
修复:
使您的对象符合三规则或使它们不可复制并始终通过引用传递。
使 FileLoader 规则三兼容是一项非常重要的工作。 FILE
指针并且不能很好地复制,这让你玩弱指针游戏以确保吸盘在每个人都完成之前不会关闭。
这也是 deleting the copy constructor and assignment operator 的一个很好的例子,因此无法复制 FileLoader。这样编译器可以在你试图做一些愚蠢的事情时警告你,比如当你想通过引用传递时通过值传递。
fstream
根本不复制,防止你在第一时间陷入这种混乱。但如果您不知道删除的函数是什么,它确实会给出神秘的错误消息流。
这里有一段测试代码来展示我认为正在发生的事情:
#include <iostream>
#include <cstdio>
class FILETest
{
public:
FILE* filep;
FILETest(): filep(NULL)
{
std::cout << "FILETest constructor" << std::endl;
}
~FILETest()
{
std::cout << "FILETest destructor" << std::endl;
}
};
void func(FILETest t)
{
(void)t;
}
int main(int argc, char** argv)
{
(void) argc;
(void) argv;
FILETest t;
func(t);
return 0;
}
以及 fstream
如何防止这种情况发生:
#include <iostream>
#include <fstream>
class fstreamtest
{
public:
std::fstream file;
fstreamtest()
{
std::cout << "fstreamtest constructor" << std::endl;
}
~fstreamtest()
{
std::cout << "fstreamtest destructor" << std::endl;
}
};
void func(fstreamtest t)
{
(void)t;
}
int main(int argc, char** argv)
{
(void) argc;
(void) argv;
fstreamtest t;
func(t);
return 0;
}
好吧,前面的不用理会。抱歉误导了,因为我忘了讲 非常重要的 事实:
- 代码 100% 适用于 Win32。我在 Linux64 上编译它。
- FileLoader 的析构函数(是虚拟的)并触发。
我添加了以下内容
FileLoader::~FileLoader() { std::cout << "destructor fired\n"; if(m_file){ fclose(m_file); m_file = NULL; std::cout << "destructor closed file\n"; } delete[] m_buffer; std::cout << "destructor deleted buffer\n"; for(int i = 0; i < CACHE_BLOCKS; i++){ if(m_cached_data[i].data) delete m_cached_data[i].data; } std::cout << "destructor deleted cache\n"; }
输出为:
destructor fired <valgrind crying> destructor closed file
所以它与缓冲区没有任何关系。我发现了问题,版本实际上不匹配,关闭了文件,但没有 NULL 它。因此实际上是原始代码中的错误。
真正解决问题的方法是Win32 long = Linux64 int。这解决了部分问题。