在C++中使用静态库链接与使用对象文件链接时的不同行为

different behavior when linking with static library vs using object files in C++

本文关键字:链接 文件 C++ 静态 对象      更新时间:2023-10-16

我正在处理一些遗留的C++代码,这些代码的行为方式我不理解。我使用的是微软编译器,但我也用g++(在Linux上)尝试过——同样的行为。

下面列出了4个文件。本质上,它是一个跟踪成员列表的注册表。如果我编译所有文件并将对象文件链接到一个程序中,它将显示正确的行为:registry.memberRegistered为真:

>cl shell.cpp registry.cpp member.cpp
>shell.exe
1

所以member.cpp中的代码以某种方式被执行了(我真的不明白,但还好)。

然而,我想要的是从registry.cpp和member.cpp构建一个静态库,并将其与从shell.cpp构建的可执行文件链接。但当我这样做时,member.cp中的代码不会被执行,registry.memberRegistered为false:

>cl registry.cpp member.cpp  /c
>lib registry.obj member.obj -OUT:registry.lib
>cl shell.cpp registry.lib
>shell.exe
0

我的问题是:为什么它以第一种方式工作,而不是以第二种方式工作?有没有一种方法(例如编译器/链接器选项)可以使它以第二方式工作?


registry.h:

class Registry {
public:
    static Registry& get_registry();
    bool memberRegistered;
private:
    Registry() {
        memberRegistered = false; 
    }
};

registry.cpp:

#include "registry.h"
Registry& Registry::get_registry() {
    static Registry registry;
    return registry;
}

member.cpp:

#include "registry.h"
int dummy() {
    Registry::get_registry().memberRegistered = true;
    return 0;
}
int x = dummy();

外壳.cpp:

#include <iostream>
#include "registry.h"
class shell {
public:
    shell() {};
    void init() {
        std::cout << Registry::get_registry().memberRegistered;
    };
};
void main() {
    shell *cf = new shell;
    cf->init();
}

您受到了通常称为静态初始化顺序惨败的打击。基本原理是,未指定跨转换单元的静态对象的初始化顺序。查看此

"shell.cpp"中此处的Registry::get_registry().memberRegistered;调用可能发生在"member.cpp"中此处int x = dummy();调用之前

编辑:

嗯,x没有使用ODR。因此,允许编译器在输入main()之前或之后,甚至根本不评估int x = dummy();

只是引用CppReference(强调我的)中的一句话

是否动态初始化由实现定义发生在主函数的第一条语句之前(对于statics)或线程的初始函数(对于线程本地人),或延迟之后发生。

如果初始化被推迟到第一条语句之后进行对于main/thread函数,它发生在第一次odr使用任何变量中定义了静态/线程存储持续时间转换单元作为要初始化的变量如果给定的翻译单元中没有使用任何变量或函数,则该翻译单元中定义的非局部变量可能永远不会初始化(这为按需动态库的行为建模)。。。


让你的程序按你想要的方式工作的唯一方法是确保x是ODR使用的

shell.cpp

#include <iostream>
#include "registry.h"
class shell {
public:
    shell() {};
    void init() {
        std::cout << Registry::get_registry().memberRegistered;
    };
};
extern int x;   //or extern int dummy();
int main() {
    shell *cf = new shell;
    cf->init();
    int k = x;   //or dummy();
}

^现在,您的程序应该按预期工作。:-)

这是链接器处理库的方式的结果:它们挑选定义迄今为止处理的其他对象未定义的符号的对象。这有助于减小可执行文件的大小,但当静态初始化有副作用时,它会导致您发现的可疑行为:member.obj/member.o根本不会链接到程序,尽管它的存在会起到一定作用。

使用g++,您可以使用:

g++ shell.cpp -Wl,-whole-archive registry.a -Wl,-no-whole-archive -o shell

以强制链接器将您的所有库放入程序中。MSVC可能有类似的选择。

非常感谢您的回复。很有帮助。

因此,WhiZTiM(使用x ODR)和aschepler(强制链接器包含整个库)提出的解决方案都适用于我。后者是我的首选,因为它不需要对代码进行任何更改。然而,似乎并没有MSVC等效于--整个归档。在Visual Studio中,我设法解决了以下问题(我有一个注册表静态库的项目,还有一个shell可执行文件的项目):

  1. 在shell项目中添加对注册表项目的引用
  2. 在常规集合下的shell项目的链接器属性中"链接库依赖项";以及";使用库相关输入";到"是的"

如果将这些选项设置为registry.memberRegistered已正确初始化。然而,在研究了编译器/链接器命令后,我得出结论,设置这些选项会导致VS简单地将registry.obj和member.obj文件传递给链接器,即:

>cl /c member.cpp registry.cpp shell.cpp
>lib /OUT:registry.lib member.obj registry.obj
>link /OUT:shell.exe "registry.lib" shell.obj member.obj registry.obj
>shell.exe
1

在我看来,这基本上是我最初问题的第一个方法。如果在链接器命令中省略registry.lib,它也可以正常工作。不管怎样,就目前而言,这对我来说已经足够了。

我正在使用CMake,所以现在我需要弄清楚如何调整CMake设置,以确保对象文件被传递到链接器?有什么想法吗?