Google在Android ndk指南网站上写道:
在一个库中分配的内存,并在另一个库中释放内存,从而导致内存泄漏或堆损坏。
- 为什么?
- 它总是正确的?
编辑
正如@Galik所写,这句话的上下文是:
在C++中,在单个程序中定义同一函数或对象的多个副本是不安全的。这是C++标准中存在的一个定义规则的一个方面。
使用静态运行时(以及一般的静态库(时,很容易意外违反此规则。例如,以下应用程序违反了此规则:
。
在这种情况下,STL(包括全局数据和静态构造函数(将存在于两个库中。此应用程序的运行时行为未定义,实际上崩溃非常常见。其他可能的问题包括:
- 在一个库中分配的内存,并在另一个库中释放内存,从而导致内存泄漏或堆损坏。
- libfoo.so 引发的异常未在 libbar.so 中捕获,从而导致应用崩溃。
- 标准::cout 的缓冲无法正常工作。
它被认为是错误的一个可能原因是,通常分配带有一定的初始化,而释放带有一些破坏逻辑。
理论:
主要危险是初始化/破坏逻辑不匹配。
让我们将两个不同的 STL 版本看作两个不同且独立的库。
考虑一下:每个库都允许您分配/取消分配某些内容。在获取资源时,每个图书馆都以自己的方式对那个东西进行一些内务管理,这些方式被封装起来(阅读:你不知道它,也不需要(。 如果每个人所做的家政工作明显不同,会发生什么?
例:
class Foo
{
private:
int x;
public:
Foo() : x(42) {}
};
namespace ModuleA
{
Foo* createAFoo()
{
return new Foo();
}
void deleteAFoo(Foo* foo)
{
if(foo != nullptr)
delete foo;
}
}
namespace ModuleB
{
std::vector<Foo*> all_foos;
Foo* createAFoo()
{
Foo* foo = new Foo();
all_foos.push_back(foo);
return foo;
}
void deleteAFoo(Foo* foo)
{
if(foo != nullptr)
{
std::vector<int>::iterator position = std::find(all_foos.begin(), all_foos.end(), foo);
if (position != myVector.end())
{
myVector.erase(position);
}
delete foo;
}
}
}
问题:如果我们执行以下操作会发生什么?
Foo* foo = ModuleB::createAFoo();
ModuleA::deleteAFoo(foo);
答:ModuleB
现在有一个悬空的指针。 这可能会导致各种可怕且难以调试的问题。 我们也没有all_foos
更小,这可能会被视为内存泄漏(每次指针的大小(。
问题:如果我们执行以下操作会发生什么?
Foo* foo = ModuleA::createAFoo();
ModuleB::deleteAFoo(foo);
答:看起来像...没有发生任何不好的事情! 但是,如果我删除了if (position != myVector.end())
支票怎么办?然后我们就会遇到问题。 STL 可能会以优化的名义做到这一点,所以......
我写了文档的那部分。我不得不调试一个问题,其中一个标准流对象(cout
或类似对象(被双重链接到库,导致该对象的两个不同实例。对象的构造函数运行了两次,但在对象的同一实例上运行了两次。一个对象被双重初始化,另一个对象未初始化。当使用未构造的对象时,它将尝试访问一些未初始化的内存并崩溃。
未定义行为的奇怪性真的没有限制。我记得的错误完全有可能是我们当时使用的编译器、链接器或加载器版本所独有的。
编辑:这是一个重现案例:
// foo.cpp
#include <stdio.h>
class Foo {
public:
Foo() { printf("this: %pn", this); }
};
Foo foo;
// main.cpp
int main() {
}
构建方式:
$ clang++ --version
clang version 7.0.0 (trunk 330210)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
$ clang++ foo.cpp -shared -o libfoo.so
$ clang++ foo.cpp -shared -o libbar.so
$ clang++ main.cpp -L. -lfoo -lbar -rpath '$ORIGIN'
libfoo 和 libbar 都将被加载,并且每个都有自己的对象副本。构造函数将运行两次,但如您所见,只有对象的一个实例运行了构造函数;它只是运行两次。
$ ./a.out
this: 0x7f9475d48031
this: 0x7f9475d48031