假设我有一个包含两个文件的库(共享或静态):
lib.h
和lib.cpp
。
lib.h
:
#pragma once
struct Trigger {
struct TriggerImpl{
TriggerImpl();
};
inline static TriggerImpl trigger;
};
lib.cpp
:
#include "lib.h"
#include <iostream>
struct LogConsole {
LogConsole() { std::cout << "LogConsole constructor" << std::endl; };
};
LogConsole log_console;
Trigger::TriggerImpl::TriggerImpl() {
std::cout << "Trigger constructor" << std::endl;
}
然后我将库链接到可执行文件。
main.cpp
:
#include <iostream>
// #include "lib.h" ///< uncomment this line and you will get different result!!!
int main() {
std::cout << "Done." << std::endl;
return 0;
}
使用静态链接(例如 CMake
target_link_library(... STATIC ...)
),您将得到:
Done.
通过动态链接(例如 CMake
target_link_library(... SHARED ...)
),您将得到:
Trigger constructor
LogConsole constructor
Done.
但是,如果取消注释
#include
行并使用静态链接,您将得到:
Trigger constructor
LogConsole constructor
Done.
我的问题是:
为什么动态链接和静态链接会(或不会)调用全局变量的构造函数?
我可以理解,包含header会初始化
trigger
,但是为什么log_console
也会被初始化呢?编译器/链接器的内部机制是什么?
可能的相关链接是:
但我认为这可能是某些特定工具链的“功能”(我正在使用的是:
gcc 14.1.1
在我的带有 Manjaro Linux 的 x64 桌面上)。
当且仅当
trigger
实际上链接到流程中时,
log_console
和 lib.cpp
都会被初始化。当您将
lib.cpp
构建到共享库中,并将主可执行文件直接与该共享库链接时,动态加载器会将共享库加载到您的进程中并对其进行初始化(在可执行文件的第一条指令运行之前)。当您将
lib.cpp
构建到存档库中时,会发生什么取决于静态链接器是否选择
lib.o
到链接中。链接器只会选择
lib.o
进入链接if 有某种原因这样做。请参阅这篇文章了解完整详细信息。 当您
#include "lib.h"
时,确实存在从
main.o
到
lib.o
的引用,并且链接器将
lib.o
拉入主可执行文件中。当您注释掉
// #include "lib.h"
时,
main.o
中没有 nothing,这表明链接器需要
lib.o
,因此它会跳过该对象。您可以观察实际情况:
#inlude
注释掉,
g++ main.cpp
将成功生成主二进制文件
#include
注释,相同的命令将会失败并出现未解决的
trigger
。
nm main.o
并观察到
#include
存在未解析的
trigger
符号,而注释掉后则没有。附注仅当
lib.o
位于存档库中时,链接器才会决定是否将
lib.o
链接到二进制文件中。如果
lib.o
或
lib.cpp
直接在命令行上列出,则链接器没有选择(这就是为什么 selbie 的实验未能重现观察到的行为)。