Windows для профессионалов


Ваше первое Windows-приложение


Windows поддерживает два типа приложений: основанные на графическом интерфейсе (graphical user interface, GUI) и консольные (console user interface, CUI). У приложений первого типа внешний интерфейс чисто графический GUI-приложения создают окна, имеют меню, взаимодействуют с пользователем через диалоговые окна и вообще пользуются всей стандартной "Windows'oвской" начинкой. Почти все стандартные программы Windows — Notepad, Calculator, Wordpad и др — являются GUI-приложениями. Приложения консольного типа работают в текстовом режиме: они не формируют окна, не обрабатывают сообщения и не требуют GUI. И хотя консольные приложения на экране тоже размещаются в окне, в нем содержится только текст. Командные процессоры вроде Cmd.exe (в Windows 2000) или Command.com (в Windows 98) — типичные образцы подобных приложений.

Вместе с тем граница между двумя типами приложений весьма условна. Можно, например, создать консольное приложение, способное отображать диалоговые окна. Скажем, в командном процессоре вполне может быть специальная команда, открывающая графическое диалоговое окно со списком команд, вроде мелочь — а избавляет от запоминания лишней информации. В то же время можно создать и GUI-приложение, выводящее текстовые строки в консольное окно. Я сам часто писал такие пpoграммы: создав консольное окно, я пересылал в него отладочную информацию, связанную с исполняемым приложением. Но, конечно, графический интерфейс предпочтительнее, чем старомодный текстовый. Как показывает опыт, приложения на основе GUI "дружественнее" к пользователю, а значит и более популярны.

Когда Вы создаете проект приложения, Microsoft Visual C++ устанавливает такие ключи для компоновщика, чтобы в исполняемом файле был указан соответствующий тип подсистемы. Для CUI-программ используется ключ /SUBSYSTEM:CONSOLE, а для GUI-приложений — /SUBSYSTEM:WINDOWS Когда пользователь запускает приложение, загрузчик операционной системы проверяет помер подсистемы, хранящийся в заголовке образа исполняемого файла, и определяет, что это за программа — GUI или СUI.
Если номер указывает на приложение последнего типа, загрузчик автоматически создает текстовое консольное окно, а если номер свидетельствует о противоположном — просто загружает программу в память. После того как приложение начинает работать, операционная система больше не интересуется, к какому типу оно относится.

Во всех Windows-приложениях должна быть входная функция за реализацию которой отвечаете Вы. Существует четыре такие функции:

int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow);

int WINAPT wWinMain( HINSTANCE hinstExe, HINSTANCE, PWSTR pszCmdLine, int nCmdShow);

int __cdecl main( int argc, char *argv[], char *envp[]);

int _cdecl wmain( int argc, wchar_t *argv[], wchar_t *envp[]);

На самом деле входная функция операционной системой не вызывается. Вместо этого происходит обращение к стартовой функции из библиотеки С/С++. Она инициализирует библиотеку С/С++, чтобы можно было вызывать такие функции, как malloc и free, а также обеспечивает корректное создание любых объявленных Вами глобальных и статических С++-объектов до того, как начнется выполнение Вашего кода. В следующей таблице показано, в каких случаях реализуются те или иные входные функции.



Тип приложения Входная функция Стартовая функция, встраиваемая в Ваш исполняемый файл
GUI-приложение, работающее с ANSI-символами и строками WinMain WinMainCRTStartup
GUI-приложение, работающее с Unicode-символами и строками wWinMain wWinMainCRTStartup
GUI-приложение, работающее

с ANSI-символами и строками
main mainCRTStartup
GUI-приложение, работающее с Unicode-символами и строками wmain wmainCRTStartup
Нужную стартовую функцию в библиотеке С/С++ выбирает компоновщик при сборке исполняемого файла. Если указан ключ /SUBSYSTEM:WINDOWS, компоновщик ищет в Вашем коде функцию WinMain или wWinMain. Если ни одной из них нет, он сообщает об ошибке "unresolved external symbol" ("неразрешенный внешний символ"); в ином случае — выбирает WtnMainCRTStartup или wWinMainCRTStartup соответственно.



Аналогичным образом, если ладан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке — "unresolved external symbol".

Но не многие знают, что в проекте можно вообще не указывать ключ /SUBSYSTEM компоновщика. Если Вы так и сделаете, компоновщик будет сам определять подсистему для Вашего приложения. При компоновке он проверит, какая из четырех функций (WinMain, wWinMain, main или wmain) присутствует в Вашем коде, и на основании этого выберет подсистему и стартовую функцию из библиотеки С/С++.

Одна из частых ошибок, допускаемых теми, кто лишь начинает работать с Visual С++, — выбор неверного типа проекта. Например, разработчик хочет создать проект Win32 Application, а сам включает в код функцию main. При его сборке он получает сообщение об ошибке, так как для проекта Win32 Application в командной строке компоновщика автоматически указывается ключ /SUBSYSTEM:WlNDOWS, который требует присутствия в коде функции WinMain или wWinMatn. В этот момент раз работчик может выбрать один из четырех вариантов дальнейших действий:

  • заменить main на WinMain. Как правило, это не лучший вариант, поскольку разработчик скорее всего и хотел создать консольное приложение;
  • открыть новый проект, на этот раз — Win32 Console Application, и перенести в него все модули кода. Этот вариант весьма утомителен, и возникает ощущение, будто начинаешь все заново;
  • открыть вкладку Link в диалоговом окне Project Settings и заменить ключ /SUBSYSTEM:WINDOWS на /SUBSYSTEM:CONSOLE. Некоторые думают, что это единственный вариант;
  • открыть вкладку Link в диалоговом окне Project Settings и вообще убрать ключ /SUBSYSTEM:WINDOWS. Я предпочитаю именно этот способ, потому что он самый гибкий. Компоновщик сам сделает все, что надо, в зависимости от входной функции, которую Вы реализуете в своем коде. Никак не пойму, почему это не предлагается по умолчанию при создании нового проекта Win32 Application или Win32 Console Application.




  • Все стартовые функции из библиотеки С/С++ делают практически одно и то же. Разница лишь в том, какие строки они обрабатывают (в ANSI или Unicode) и какую входную функцию вызывают после инициализации библиотеки. Кстати, с Visual C++ поставляется исходный код этой библиотеки, и стартовые функции находятся в файле CRt0.c. А теперь рассмотрим, какие операции они выполняют:

  • считывают указатель на полную командную строку нового процесса;
  • считывают указатель на переменные окружения нового процесса;
  • инициализируют глобальные переменные из библиотеки С/С++, доступ к которым из Вашего кода обеспечивается включением файла StdLib.h. Список этих переменных приведен в таблице 4-1;
  • инициализируют кучу (динамически распределяемую область памяти), используемую С-функциями выделения памяти (т.е. malloc и calloc) и другими процедурами низкоуровневого ввода-вывода;
  • вызывают конструкторы всех глобальных и статических объектов С++-классов.


  • Закончив эти операции, стартовая функция обращается к входной функции в Вашей программе. Если Вы написали ее в виде wWinMain, то она вызывается так:

    GetStartupInfo(&StartupInfo);

    int nMainRetVal = wWinMain(GetMjduleHandle(NULL), NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow , SW_SHOWDEFAULT);

    А если Вы предпочли WinMain, то:

    GetStartupInfo(&StartupInfo);

    int nMainReLVal = WinMain(GetModuleHandle(NULL), NULL, pszCommandLineANSI, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? Startupinfo.wShowWindow , SW_SHOWDEFAULT);

    И, наконец, то же самое для функций wmain и main.

    int nMainRetVal = wmain(__argc, __wargv, _wenviron}; int nMainRetVal = main(_argc, __argv, _environ);

    Когда Ваша входняя функция возвращает управление, стартовая обращается к функции exit библиотеки С/С++ и передает ей значение nMainRetVal. Функция exit выполняет следующие операции:

  • вызывает все функции, зарегистрированные вызовами функции _onexit;
  • вызывает деструкторы всех глобальных и статических объектов С++-классов;
  • вызывает Windows-функцию ExifProcess, передавая ей значение nMainRetVal.


    Это заставляет операционную систему уничтожить Ваш процесс и установить код его завершения.


  • Имя переменной Тип Описание
    _osver unsigned int Версия сборки операционной системы. Например, у Windows 2000 Beta 3 этот номер был 2031, соответственно _osver равна 2031.
    _winmajor unsigned int Основной номер версии Windows в шестнадцатерич ной форме. Для Windows 2000 это значение равно 5.
    Таблица 4-1. Глобальные переменные из библиотеки С/С++, доступные Вашим программам

    Имя переменной Тип Описание
    _winminor unsigned int Дополнительный номер версии Windows в шестнадца теричной форме Для Windows 2000 это значение равно 0
    _winver unsigned int Вычисляется как ( winmajor << 8) + _winminor.
    __argc unsigned int Количество аргументов, переданных в командной строке
    __argv _ _wargv char ** wchar_t ** Массив размером __argc с указателями на ANSI- или Unicode-строки. Каждый элемент массива указывает на один из аргументов командной строки.
    _environ _wenviron char ** wchar_t ** Массив указателей на ANSI- или Unicode-строки. Каждый элемент массива указывает на строку — переменную окружения.
    _pgmptr _wpgmptr char ** wchar_t** Полный путь и имя (в ANSI или Unicode) запускаемой программы.

    Содержание раздела