• И лишь после этого вызывается функция [w]main или [w]WinMain. Коротко можно сказать, что функция xxx CRTStartup вызывает соответствующую функцию xxx .
• Программа работает.
• Выполняется последовательность действий по очистке, к которой мы еще вернемся.
• И, наконец, происходит завершение процесса.
Теперь, наконец, можно ответить на мой вопрос: он был задан некорректно :). В самом деле, результат сборки будет зависеть от набора опций компоновщика, установленных в проекте или по умолчанию.
Так, например, при вызове компилятора в командной строке таким образом:
cl test.cpp user32.lib
мы получим консольную программу и сообщение "Hello from main()" (вспомните, что говорилось об умолчаниях).
А вызвав компилятор вот так:
cl test.cpp user32.lib /link /entry:WinMainCRTStartup /subsystem:console
мы получим "чудо чудное": программу, у которой выполняется функция WinMain, но создается окно консоли.
Код инициализации глобальных переменных
Как в VC++ реализован вызов цепочки функций инициализации/завершения?
Наличие в программе хотя бы одной глобальной переменной – экземпляра класса – заставляет компилятор сделать следующее. Во-первых, он генерирует невидимую за пределами модуля функцию, в которой и выполняются необходимые действия – вычисляется значение инициализатора или вызывается конструктор. Далее создается специальная запись с указателем на эту функцию в сегменте с именем вида ".CRT$ xxx ". Детально разбирать формат именования сегмента мы не будем, сейчас важно только то, что все сегменты такого типа будут при сборке объединены в алфавитном порядке в один сегмент. Таким образом, в момент старта программы в памяти будет находиться массив указателей на функции, при вызове которых и произойдут необходимые действия. В стартовом коде CRT VC этим занимается функция _initterm.
А почему здесь используется термин "функции инициализации/завершения " вместо терминов "конструкторы/деструкторы"?
Напомню, что стандарт языка C++ разрешает инициализацию переменных с помощью неконстантных выражений. Если переменная (даже простого типа) описана в глобальной области, то ее инициализатор должен быть выполнен до вызова функции main/WinMain:
int len = strlen("Hello, world!");
Обработка в этом случае ничем не отличается от инициализации экземпляра класса имеющего конструктор.
Код завершения
Упомянув инициализацию CRT, нельзя умолчать о коде очистки, или завершения. В нем выполняются действия обратного характера (и, в том числе, деструкторы глобальных переменных). Что действительно заслуживает описания, так это то, что код очистки можно вызвать собственноручно. Да-да, он содержится в функции exit. Если же не вызвать ее явно, то она вызовется после возврата из main/WinMain. Наиболее выразительную реализацию вышесказанного я встретил однажды в исходных файлах CRT компилятора WATCOM C++:
exit(main(__argv, __argc, __envp));
То есть, можно сказать, что все выполнение программы имеет целью получение параметра для функции exit. :)
ПРИМЕЧАНИЕ
Вообще-то, exit (вернее, возможность ее прямого вызова) является, скорее, "пережитком" со времен программирования на C. При вызове этой функции из программы на C++ не выполнятся деструкторы для локальных переменных (что естественно, поскольку, в отличие от глобальных объектов, их деструкторы нигде не зарегистрированы). Кроме того, вызов exit из деструктора может привести к входу программы в бесконечный цикл, так что не злоупотребляйте этой функцией.
Со времен создания библиотеки языка C осталась и такая возможность, как регистрация цепочки обработчиков завершения с помощью функций atexit/_onexit. Функции, зарегистрированные вызовом atexit/_onexit, будут вызваны в ходе завершения программы в порядке, обратном порядку их регистрации. Для программы на C++ с этой целью лучше воспользоваться глобальными деструкторами.
На самом деле, в программе на VC регистрация деструкторов глобальных объектов также выполняется с помощью внутреннего вызова atexitпосле вызова конструктора. Это имеет довольно веские основания: если конструктор объекта вызван не был, то не будет вызван и его деструктор. Но, в любом случае, это – деталь реализации, на которую полагаться не стоит.
Внутри exit содержится вызов функции более низкого уровня – _exit. Ее вызов не приведет к вызову деструкторов и exit-обработчиков, а только выполнит самую необходимую очистку (не буду вдаваться в подробности, замечу только, что при этом вызываются C-терминаторы (функции из таблицы в сегментах "CRT$XT[A-Z]"), в частности, подчищается low-level i/o) и завершит программу вызовом функции Windows API ExitProcess.
Читать дальше