Программирование на Visual C++. Архив рассылки - Алекс Jenter
Шрифт:
Интервал:
Закладка:
Если это вас устраивает, проблема с упомянутым ATL-проектом решается достаточно просто. Необходимо зайти в настройки проекта ("Project" – "Settings"), выбрать нужную Release-конфигурацию и на закладке "C++" удалить опцию препроцессора _ATL_MIN_CRT. Вопрос будет снят. Дальше можно не читать.
Но встречаются случаи, когда считаешь буквально каждый байт исполняемого модуля. Это может быть ядро инсталлятора или самораспаковывающегося архива, элемент управления ActiveX, который скачивается через Интернет, или приложение для встраиваемой системы. Компиляторы C++ (и Visual C++, в том числе), на мой взгляд, наиболее подходят для такого рода разработок. Приложение может, в конце концов, состоять из большого количества модулей, и мало что значащие 30 Кб могут превратиться в несколько сотен килобайт, а то и мегабайт. Но для контроля над процессом сборки придется погрузиться в некоторые детали реализации поддержки CRT.
main или WinMain?Среди начинающих программистов можно услышать такое мнение: для консольной программы используется только функция main, а для оконной – WinMain. Это мнение, хотя и подтвержденное умолчаниями компилятора и линкера, в общем случае, является ошибочным.
Чтобы немного развлечься, проведем эксперимент. Создадим файл test.cpp:
#include <windows.h>
int main() {
MessageBox(0, "Hello from main()", "A test program", MB_OK);
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
MessageBox(0, "Hello from WinMain()", "A test program", MB_OK);
return 0;
}
Внимание, вопрос: что появится на экране после запуска такой программы? Постарайтесь ответить на этот вопрос, не заглядывая в дальнейшее описание.
ПРИМЕЧАНИЕ
Я не стал рассматривать еще два возможных варианта стартовой функции: wmain или wWinMain, предназначенных для проектов, компилируемых в Unicode. Кроме того, при создании DLL имеется еще один вариант стартовой функции – DllMain.
Точка входа в программуФункция [w]main или [w]WinMain, с которой начинается выполнение программы, вовсе не является точкой входа исполняемого модуля! На самом деле, программа на C++ начинает работу с выполнения специальной процедуры инициализации. Что касается Win32, то адрес этой процедуры и содержится в поле AddressOfEntryPoint заголовка portable executable (pe) выполняемого файла. Она представляет собой обычную функцию C, описанную с соглашением о вызовах __stdcall. В зависимости от настроек проекта, в Visual C++ эта функция может называться [w]mainCRTStartup, [w]WinMainCRTStartup или _DllMainCRTStartup (символ 'w' добавляется к имени для Unicode-проектов). Конкретно же для сборки приложения имя функции-точки входа можно задать опцией линкера /entry. Умолчанием для visual c++ является "maincrtstartup". Все сказанное справедливо и для некоторых других компиляторов C++ для Win32.
Что же происходит во время ее выполнения? Вот типичный сценарий работы такой функции (случай DLL здесь не рассматривается).
• Инициализируются переменные CRT (такие, как errno и osver). Многопоточная библиотека требует особой инициализации.
• Происходит инициализация динамической памяти (кучи).
• Инициализируется среда обработки ошибок в вычислениях с плавающей точкой. Это необходимо не только для библиотечных функций (таких, как sqrt), но и для преобразований между целочисленными и плавающими типами данных.
• Получаются значения аргументов командной строки программы и переменных среды.
• В случае необходимости, происходит инициализация консоли и привязка стандартного вывода к файловым дескрипторам C. При старте исполняемого файла, у которого в уже упомянутом заголовке PE значение поля Subsystem равно 3 (Windows character-mode executable), создается консоль. Это значение можно задать опцией линкера /subsystem. Выбор подсистемы выполнения также влияет на выбор стартовой функции (если ее имя не задано явно). Умолчанием является "console".
• Происходит вызов цепочки функций инициализации CRT и конструкторов глобальных переменных (подробнее об этом – в следующем разделе).
• И лишь после этого вызывается функция [w]main или [w]WinMain. Коротко можно сказать, что функция xxxCRTStartup вызывает соответствующую функцию 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.