我最近将两个共享库(都是我自己制作的)链接在一起时遇到崩溃问题。我最终发现这是因为两个文件之间有一个源文件重复。在该源文件中,定义了一个全局std :: vector(实际上是类的静态成员),最终它被释放了两次-每个库都释放了一个。
然后我写了一些测试代码来验证我的想法。 在标头中,我声明一个类和该类的全局对象:
#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_
#include <iostream>
struct Data {
Data(void) {std::cout << "Constructor" << std::endl;}
~Data(void) {std::cout << "Destructor" << std::endl;}
int FuncDefinedByLib(void) const;
};
extern const Data data;
#endif
FuncDefinedByLib
函数未定义。
然后,我创建了两个库libA
和libB
,它们都包含此标头。
libA
看起来像这样
const Data data;
int Data::FuncDefinedByLib(void) const {return 1;}
void PrintA(void) {
std::cout << "LibB:" << &data << " "
<< (void*)&Data::FuncDefinedByLib << " "
<< data.FuncDefinedByLib() << std::endl;
}
它定义了全局data
对象,FuncDefinedByLib
函数和函数PrintA
,该函数打印data
对象的地址,即{{1}的地址},以及返回值FuncDefinedByLib
。
FuncDefinedByLib
与libB
几乎相同,除了名称libA
更改为PrintA
并且PrintB
返回2而不是1。
然后,我创建一个程序,该程序链接到两个库并调用FuncDefinedByLib
和PrintA
。在遇到崩溃问题之前,我认为两个库都将创建自己的PrintB
版本。但是,实际输出
class Data
表示这两个库都只使用Constructor
Constructor
LibB:0x7efceaac0079 0x7efcea8bed60 1
LibB:0x7efceaac0079 0x7efcea8bed60 1
Destructor
Destructor
的一个版本,并且class Data
的仅一个版本,即使类和对象的定义不同,也不同于const Data data
(我理解是因为libA
首先被链接)。两次毁灭清楚地说明了我的撞车问题。
这是我的问题
这是怎么发生的?我了解链接这两个库的主要代码可能仅链接到它看到的第一个符号。但是共享库在创建时应该已经在内部链接了(或者不是吗?我真的对共享库没有太多的了解),他们怎么能知道其他库中有一个孪生类,并在创建后链接到该类是自己创建的?
我知道在共享库之间重复代码通常是一种不好的做法。但是,是否存在满足条件的条件,即在库之间进行复制是安全的?还是有系统的方式使我的代码没有风险地重复使用?还是永远不安全,应该始终严格禁止它?我不想总是为了共享一小段代码而拆分另一个共享库。
这种行为看起来很神奇。有人利用这种行为做一些好神奇的事情吗?
答案 0 :(得分:1)
这是C和C ++中的一个已知问题,它是当前编译模型的结果。 如何的完整解释超出了此答案的范围,但是this talk by Matt Godbolt为初学者提供了对该过程的深入解释。另请参见this article on the linker。
2020年将有一个新版本的C ++发行,它将引入一个新的编译模型(称为模块),以避免此类问题。您将能够从模块导入和导出内容,类似于程序包在Java中的工作方式。
有几种不同的解决方案。
这很漂亮。如果将全局变量作为静态变量放置在函数中,则它只会始终构造一次,这是标准所保证的(即使在多线程环境中也是如此)。
#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_
#include <iostream>
struct Data {
Data(void) {std::cout << "Constructor" << std::endl;}
~Data(void) {std::cout << "Destructor" << std::endl;}
int FuncDefinedByLib(void) const;
};
Data& getMyDataExactlyOnce() {
// The compiler will ensure
// that data only gets constructed once
static Data data;
// Because data is static, it's fine to return a reference to it
return data;
}
// Here, the global variable is a reference
extern const Data& data = getMyDataExactlyOnce();
#endif
如果在C ++ 17中将全局变量标记为内联,则每个包含标头的翻译单元都会在内存中的位置获取自己的副本。请参阅:https://en.cppreference.com/w/cpp/language/inline
#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_
#include <iostream>
struct Data {
Data(void) {std::cout << "Constructor" << std::endl;}
~Data(void) {std::cout << "Destructor" << std::endl;}
int FuncDefinedByLib(void) const;
};
// Everyone gets their own copy of data
inline extern const Data data;
#endif
种类。如果您真的想使用全局变量来做Dark Magic,C ++ 14会引入 templated 全局变量:
template<class Key, class Value>
std::unordered_map<Key, Value> myGlobalMap;
void foo() {
myGlobalMap<int, int>[10] = 20;
myGlobalMap<std::string, std::string>["Hello"] = "World";
}
做到这一点。我对模板化的全局变量没有多大用处,尽管我想象如果您正在做一些事情,例如计算一个函数被调用的次数或创建一个类型的次数,这样做会很有用。