我有一个主程序(main.cpp
)和一个共享库(test.h
和test.cpp
):
测试.h:
#include <stdio.h>
struct A {
A() { printf("A ctorn"); }
~A() { printf("A dtorn"); }
};
A& getA();
测试.cpp:
#include "test.h"
A& getA() {
static A a;
return a;
}
主.cpp:
#include "test.h"
struct B {
B() { printf("B ctorn"); }
~B() { printf("B dtorn"); }
};
B& getB() {
static B b;
return b;
}
int main() {
B& b = getB();
A& a = getA();
return 0;
}
这就是我在 Linux 上编译这些源代码的方式:
g++ -shared -fPIC test.cpp -o libtest.so
g++ main.cpp -ltest
Linux上的输出:
B ctor
A ctor
A dtor
B dtor
当我在 Windows 上运行此示例时(经过一些调整,例如添加dllexport
),我得到了 MSVS 2015/2017:
B ctor
A ctor
B dtor
A dtor
对我来说,第一个输出似乎符合标准。例如,请参阅: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
从第3.6.3.1段开始:
如果用静态存储完成构造函数或对象的动态初始化 持续时间在另一个持续时间之前排序,第二个析构函数的完成被排序 在启动第一个析构函数之前。
也就是说B
如果首先构造对象,则应最后销毁 - 这是我们在Linux上看到的。但是Windows输出是不同的。是 MSVC 错误还是我错过了什么?
DLL 的整个概念超出了C++标准的范围。
使用 Windows,可以在程序执行期间动态卸载 DLL。 为了帮助支持这一点,每个 DLL 将处理加载时构造的静态变量的销毁。 结果是,静态变量将按照取决于 DLL 的卸载顺序(当它们收到DLL_PROCESS_DETACH通知时)销毁。 DLL 和可视C++运行时库行为描述了此过程。
我看到您的分析中缺少两件事。
程序:该标准对程序的执行方式提出了要求。你的程序由命令生成的(可执行)文件组成g++ main.cpp -ltest
,大概是a.out
或a.exe
。特别是,您的程序不包含它所链接的任何共享库。因此,共享库所做的任何事情都超出了标准的范围。
嗯,差不多。由于您是用C++编写共享库的,因此您的libtest.so
或test.dll
文件确实属于标准的范围,但它本身会这样做,与调用它的可执行文件无关。也就是说,忽略共享库的可观察行为a.exe
的可观察行为必须符合标准,而忽略可执行文件的可观察行为test.dll
的可观察行为必须符合标准。
您有两个相关但技术上独立的程序。该标准分别适用于它们中的每一个。C++标准不包括独立程序如何相互交互。
如果你想参考一下,我会看看"翻译阶段"的第9条([lex.phases] - 你引用的标准版本中的第2.2节)。链接的结果a.out
是程序图像,而test.dll
是执行环境的一部分。
之前排序:您似乎错过了">之前排序"的定义。是的,输出在"A ctor"之前有"B ctor"。但是,这本身并不意味着b
的构造函数是在a
的构造函数之前排序的。C++标准在[intro.execution](您引用的标准版本中的第1.9节第13条)中为"之前排序"提供了精确的含义。使用精确的含义,可以得出结论,如果b
的构造函数在a
的构造函数之前排序,那么输出应该在"A ctor"之前有"B ctor"。但是,相反的情况(您假设的)并不成立。
在评论中,您建议当"之前排序"被替换为"之前强烈发生"时,这是一个微小的变化。事实并非如此,因为"在较新版本的标准中也有确切的含义(第6.8.2.1节[intro.races]的第12条)。事实证明,"强烈发生在之前"意味着"在之前排序"或另外三种情况之一。因此,措辞上的改变是有意扩大了标准的这一部分,涵盖了比以前更多的案例。
构造函数和析构函数的相对顺序仅在静态链接的可执行文件或(共享)库中定义。它由喜欢时静态对象的作用域规则和顺序定义。后者也很模糊,因为有时很难保证链接的顺序。
共享库 (DLL) 在执行开始时由操作系统加载,也可以由程序按需加载。因此,没有已知的加载顺序来加载这些库。因此,没有已知的卸载顺序。因此,库之间的构造函数和析构函数的顺序可能会有所不同。在单个库中仅保证它们的相对顺序。
通常,当构造函数或析构函数的顺序跨库或跨不同文件很重要时,有一些简单的技术可以让你做到这一点。其中之一是使用指向对象的指针。例如,如果对象 A 要求在其之前构造对象 B,则可以这样做:
A *aPtr = nullptr;
class B {
public:
B() {
if (aPtr == nullptr)
aPtr = new A();
aPtr->doSomething();
}
};
...
B *b = new B();
以上将保证 A 在使用之前被构造。这样做时,您可以 保留已分配对象的列表,或保留指针、shared_pointers、...在其他物体中协调有序的破坏,比如在退出主线之前。
因此,为了说明上述内容,我以基本的方式重新实现了您的示例。肯定有多种处理方法。在这个例子中,销毁列表是按照上述技术构建的,分配的A和B被放在列表中,并以特定的顺序在最后销毁。
测试.h
#include <stdio.h>
#include <list>
using namespace std;
// to create a simple list for destructios.
struct Destructor {
virtual ~Destructor(){}
};
extern list<Destructor*> *dList;
struct A : public Destructor{
A() {
// check existencd of the destruction list.
if (dList == nullptr)
dList = new list<Destructor*>();
dList->push_front(this);
printf("A ctorn");
}
~A() { printf("A dtorn"); }
};
A& getA();
测试.cpp
#include "test.h"
A& getA() {
static A *a = new A();;
return *a;
}
list<Destructor *> *dList = nullptr;
主.cpp
#include "test.h"
struct B : public Destructor {
B() {
// check existence of the destruciton list
if (dList == nullptr)
dList = new list<Destructor*>();
dList->push_front(this);
printf("B ctorn");
}
~B() { printf("B dtorn"); }
};
B& getB() {
static B *b = new B();;
return *b;
}
int main() {
B& b = getB();
A& a = getA();
// run destructors
if (dList != nullptr) {
while (!dList->empty()) {
Destructor *d = dList->front();
dList->pop_front();
delete d;
}
delete dList;
}
return 0;
}
即使在 Linux 上,如果您使用 dlopen() 和 dlclose() 手动打开和关闭 DLL,也会遇到静态构造函数和析构函数调用的交叉:
Testa.cpp:
#include <stdio.h>
struct A {
A() { printf("A ctorn"); }
~A() { printf("A dtorn"); }
};
A& getA() {
static A a;
return a;
}
(testb.cpp是模拟的,除了A
改为B
,a
改为b
)
主.cpp:
#include <stdio.h>
#include <dlfcn.h>
class A;
class B;
typedef A& getAtype();
typedef B& getBtype();
int main(int argc, char *argv[])
{
void* liba = dlopen("./libtesta.so", RTLD_NOW);
printf("dll libtesta.so openedn");
void* libb = dlopen("./libtestb.so", RTLD_NOW);
printf("dll libtestb.so openedn");
getAtype* getA = reinterpret_cast<getAtype*>(dlsym(liba, "_Z4getAv"));
printf("gotten getAn");
getBtype* getB = reinterpret_cast<getBtype*>(dlsym(libb, "_Z4getBv"));
printf("gotten getBn");
A& a = (*getA)();
printf("gotten an");
B& b = (*getB)();
printf("gotten bn");
dlclose(liba);
printf("dll libtesta.so closedn");
dlclose(libb);
printf("dll libtestb.so closedn");
return 0;
}
输出为:
dll libtesta.so opened
dll libtestb.so opened
gotten getA
gotten getB
A ctor
gotten a
B ctor
gotten b
A dtor
dll libtesta.so closed
B dtor
dll libtestb.so closed
有趣的是,a
构造函数的执行被推迟到实际调用getA()
的时间。b
也是如此.如果a
和b
的静态声明从其 getter-Functions 内部移动到模块级别,则在加载 DLL 时已经自动调用构造函数。
当然,如果在分别调用dlclose(liba)
或dlclose(libb)
后,main()
函数中仍使用a
或b
,则应用程序会崩溃。
如果正常编译和链接应用程序,则对dlopen()
和dlclose()
的调用将由运行时环境中的代码执行。看起来,您测试的Windows版本按顺序执行这些调用,这是您出乎意料的。Microsoft选择这样做的原因可能是,在程序退出时,主应用程序中的任何内容仍然依赖于DLL中的任何内容的趋势高于相反的趋势。因此,库中的静态对象通常应在主应用程序被销毁后销毁。
出于同样的原因,初始顺序也应该颠倒过来:DLL 应该是第一个,主应用程序应该排在第二位。所以Linux在初始化和清理上都出错了,而Windows至少在清理上做对了。