Предположим, у меня есть библиотека (общая или статическая) с двумя файлами: 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.
Мои вопросы:
Почему динамическое и статическое связывание будет (или не будет) вызывать конструкторы глобальных переменных?
Я могу это понять, включая заголовок, который будет инициализировать trigger, но почему log_console также будет инициализирован? Каков внутренний механизм компилятора/компоновщика?
Возможные относительные ссылки:
Но я думаю, что это может быть «ФУНКЦИЯ» какой-то конкретной цепочки инструментов (я использую: gcc 14.1.1 на своем рабочем столе x64 с Manjaro Linux).
@user12002570 user12002570 Кажется, нет. Конструкторы вообще не вызываются, не в том порядке.
Зачем вам вообще нужны глобальные переменные? Узнайте о внедрении зависимостей или хотя бы о синглтоне Мейера. Я избегаю глобальных переменных, как чумы, потому что они затрудняют модульное тестирование кода, а во время обслуживания (десятилетий!) Могут появиться зависимости, которые все равно вызовут фиаско статической инициализации.
Компоновщик, вероятно, просто отбрасывает весь lib.cpp, потому что он может определить, что он не используется в программе.
Я пробовал использовать Window Visual C++ и Linux g++. Я не могу получить копию, просто напечатав «Готово». только. Как ни странно, если я изменю порядок исходных файлов в командной строке: g++ -std=c++17 lib.cpp main.cpp вместо g++ -std=c++17 main.cpp lib.cpp, я получу ошибку сегментации при запуске результирующего ./a.out.... О боже, я использую древнюю версию gcc...
Я не думаю, что компоновщику следует отбрасывать весь lib.cpp, потому что я создаю библиотеку. Как статическая библиотека, она должна экспортировать эти глобальные переменные и позволить исполняемому файлу инициализировать их перед основной.
динамическое связывание не стандартизировано и фактически отличается в Windows и Linux.





И trigger, и log_console инициализируются тогда и только тогда, когда lib.cpp действительно связан с процессом.
Когда вы встраиваете lib.cpp в общую библиотеку и связываете основной исполняемый файл напрямую с этой общей библиотекой, динамический загрузчик загружает общую библиотеку в ваш процесс и инициализирует ее (до запуска первой инструкции исполняемого файла).
Когда вы встраиваете lib.cpp в архивную библиотеку, то, что происходит, зависит от того, выберет ли статический компоновщик lib.o в ссылку или нет.
Компоновщик выберет lib.o в ссылке только в том случае, если для этого есть какая-то причина. Подробности смотрите в этом посте.
Когда вы #include "lib.h", действительно существует такая ссылка от main.o до lib.o, и компоновщик втягивает lib.o в основной исполняемый файл.
Когда вы закомментируете // #include "lib.h", в main.o нет ничего, что указывало бы компоновщику на необходимость lib.o, поэтому он пропускает этот объект.
Вы можете наблюдать это в действии:
#includeg++ main.cpp удастся создать основной двоичный файл#include та же команда завершится неудачей с неразрешенным trigger.Вы также можете проверить nm main.o в обоих случаях и заметить, что с #include есть неразрешенный символ trigger, а с закомментированным - нет.
P.S. Компоновщик может решить, связывать lib.o с двоичным файлом или нет, только если lib.o находится в архивной библиотеке. Если lib.o или lib.cpp указаны непосредственно в командной строке, то у компоновщика нет выбора, и он должен включить lib.o (именно поэтому эксперимент Селби не смог воспроизвести наблюдаемое поведение).