我有一个主程序(main.cpp
)和一个共享库(test.h
和test.cpp
):
test.h:
#include <stdio.h>
struct A {
A() { printf("A ctor\n"); }
~A() { printf("A dtor\n"); }
};
A& getA();
test.cpp:
#include "test.h"
A& getA() {
static A a;
return a;
}
main.cpp:
#include "test.h"
struct B {
B() { printf("B ctor\n"); }
~B() { printf("B dtor\n"); }
};
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错误还是我缺少什么?
答案 0 :(得分:9)
DLL的整个概念不在C ++标准的范围之内。
使用Windows,可以在程序执行过程中动态卸载DLL。为了帮助支持这一点,每个DLL都将处理在加载时构造的静态变量的破坏。结果是静态变量将按照依赖于DLL的卸载顺序的顺序被销毁(当它们收到DLL_PROCESS_DETACH通知时)。 DLLs and Visual C++ run-time library behavior描述了此过程。
答案 1 :(得分:4)
我发现您的分析中缺少两件事。
程序:该标准对程序的执行方式提出了要求。您的程序包含命令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]第clause 12条)。事实证明,“在……之前发生”是指“在……之前发生顺序”或三种其他情况之一。因此,措词的更改是对标准部分的有意扩展,涵盖了比以前更多的案例。
答案 2 :(得分:2)
构造函数和析构函数的相对顺序仅在静态链接的可执行文件或(共享)库中定义。它由作用域的作用域规则和静态对象的顺序定义。后者也很模糊,因为有时很难保证链接的顺序。
共享库(dll)在执行开始时由操作系统加载,也可以由程序按需加载。因此,没有已知的顺序来加载这些库。结果,没有已知的卸载顺序。结果,库之间的构造函数和析构函数的顺序可能会有所不同。在单个库中只能保证它们的相对顺序。
通常,当构造函数或析构函数的顺序在库或不同文件中很重要时,可以使用简单的方法来实现。其中之一是使用指向对象的指针。例如,如果对象A要求在对象B之前构造对象B,则可以执行以下操作:
A *aPtr = nullptr;
class B {
public:
B() {
if (aPtr == nullptr)
aPtr = new A();
aPtr->doSomething();
}
};
...
B *b = new B();
以上内容将确保在使用A之前先对其进行构造。这样做时,您可以 保留分配对象的列表,或在其他对象中保留指针,shared_pointers等,以安排有序的销毁,例如在退出main之前。
因此,为说明上述内容,我以一种基本方式重新实现了您的示例。肯定有多种处理方式。在此示例中,销毁列表是根据上述技术构建的,分配的A和B放在列表中,并在最后按特定顺序销毁。
test.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 ctor\n");
}
~A() { printf("A dtor\n"); }
};
A& getA();
test.cpp
#include "test.h"
A& getA() {
static A *a = new A();;
return *a;
}
list<Destructor *> *dList = nullptr;
main.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 ctor\n");
}
~B() { printf("B dtor\n"); }
};
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;
}
答案 3 :(得分:1)
即使在Linux上,如果使用dlopen()和dlclose()手动打开和关闭DLL,也会遇到静态构造函数和析构函数调用的冲突:
testa.cpp:
#include <stdio.h>
struct A {
A() { printf("A ctor\n"); }
~A() { printf("A dtor\n"); }
};
A& getA() {
static A a;
return a;
}
(testb.cpp是模拟的,除了A
更改为B
,而a
更改为b
)
main.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 opened\n");
void* libb = dlopen("./libtestb.so", RTLD_NOW);
printf("dll libtestb.so opened\n");
getAtype* getA = reinterpret_cast<getAtype*>(dlsym(liba, "_Z4getAv"));
printf("gotten getA\n");
getBtype* getB = reinterpret_cast<getBtype*>(dlsym(libb, "_Z4getBv"));
printf("gotten getB\n");
A& a = (*getA)();
printf("gotten a\n");
B& b = (*getB)();
printf("gotten b\n");
dlclose(liba);
printf("dll libtesta.so closed\n");
dlclose(libb);
printf("dll libtestb.so closed\n");
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时已经自动调用了构造函数。
当然,如果在调用a
或b
之后在main()
函数中仍使用dlclose(liba)
或dlclose(libb)
,则应用程序将崩溃,分别。
如果您正常编译和链接应用程序,则对dlopen()
和dlclose()
的调用将由运行时环境中的代码执行。看来,您经过测试的Windows版本按顺序执行了这些调用,这是您无法预料的。 Microsoft选择这种方式的原因可能是,在程序退出时,与其他方式相比,主应用程序中的任何内容仍然倾向于依赖DLL中的任何内容的可能性更高。因此,通常应在销毁主应用程序之后销毁库中的静态对象。
以相同的理由,初始化顺序也应颠倒:DLL应该是第一位,主要应用程序是第二位。因此,Linux在初始化和清除上都会出错,而Windows至少在清除上会正确。