Первая программа на C++ в Windows: от исходного текста до exe и из чего этот текст состоит
g++ с ключами курса и запускать полученный exeПроцессор выполняет машинные команды — числа в памяти. Программа на C++ — текст. Между ними стоит компилятор: он заранее, один раз, переводит текст в машинный код и записывает результат в исполняемый файл .exe. Запускается потом только exe; исходный текст и компилятор для запуска не нужны. Такие языки называют компилируемыми, в отличие от интерпретируемых (Python), где текст программы разбирается при каждом запуске.
Слово «компиляция» в обиходе означает всю сборку, но на деле это четыре этапа, и каждый выполняет своя программа:
hello.cpp ──► препроцессор ──► hello.ii ──► компилятор ──► hello.s
исходный #include, текст C++ → ассемблер
текст #define без # ассемблер
hello.s ──► ассемблер ──► hello.o ──► компоновщик ──► hello.exe
объектный + код библиотек исполняемый
файл (cout и др.) файл
| Этап | Что делает | Команда, чтобы остановиться на нём |
|---|---|---|
| Препроцессор | выполняет строки с #: вставляет заголовочные файлы, подставляет макросы | g++ -E hello.cpp -o hello.ii |
| Компилятор | проверяет, что текст — правильный C++, и переводит его в ассемблер | g++ -S hello.ii -o hello.s |
| Ассемблер | переводит ассемблер в машинный код — объектный файл | g++ -c hello.s -o hello.o |
| Компоновщик (линкер) | соединяет объектный файл с кодом библиотек и оформляет exe для Windows | g++ hello.o -o hello.exe |
Обычно все четыре этапа выполняет одна команда g++ hello.cpp -o hello.exe, а промежуточные файлы удаляются. Ключ -save-temps их сохраняет — полезно открыть hello.ii один раз и увидеть, что строка #include <iostream> превратилась в десятки тысяч строк чужого кода.
undefined reference и ld returned 1 exit status. Пример — в пункте 03.В курсе используется компилятор g++ из проекта MinGW-w64 — это GCC, собранный для Windows. Он создаёт обычные exe, которые работают без дополнительных сред, и умеет вызывать функции Windows API — они нужны с раздела 14. Установить можно двумя способами; компилятор в обоих один и тот же.
1. Скачать установщик с https://www.msys2.org и установить в C:\msys64.
2. Из меню «Пуск» открыть «MSYS2 UCRT64» и обновить систему пакетов
(если окно закроется само — открыть снова и повторить команду):
pacman -Syu
3. Установить компилятор:
pacman -S mingw-w64-ucrt-x86_64-gcc
4. Добавить в переменную Path папку C:\msys64\ucrt64\bin (ниже — как).
1. На https://winlibs.com скачать архив последней версии: UCRT runtime, Win64,
вариант без LLVM/Clang — это zip, установки нет.
2. Распаковать в папку, куда есть права на запись, например D:\mingw64.
3. Добавить в Path папку D:\mingw64\bin.
Path — переменная окружения со списком папок, в которых Windows ищет программы по имени. Пока папки с g++.exe в ней нет, команда g++ не найдётся. Добавить: «Пуск» → «Изменение переменных среды для вашей учётной записи» → Path → «Изменить» → «Создать» → путь к папке bin. Переменные своей учётной записи меняются без прав администратора. Если и это запрещено, путь добавляется только для одного окна командной строки:
set PATH=D:\mingw64\bin;%PATH%
Проверка — в новом окне командной строки (cmd) или PowerShell: старые окна изменений Path не видят.
> g++ --version
g++ (Rev1, Built by MSYS2 project) 15.2.0 <- номер версии может быть другим
> where g++
C:\msys64\ucrt64\bin\g++.exe
bin, внутри которой лежит g++.exe. Папку для своих программ создайте с путём без пробелов и русских букв, например C:\cpp. Исходный текст можно писать в любом редакторе, который сохраняет файлы в кодировке UTF-8: VS Code, Notepad++, «Блокнот» Windows 10/11.Первая программа — файл C:\cpp\hello.cpp:
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
return 0;
}
Сборка и запуск из папки с файлом:
> cd C:\cpp
> g++ -std=c++17 -Wall -Wextra hello.cpp -o hello.exe
> hello.exe
Hello, world!
В PowerShell программа из текущей папки запускается с указанием пути: .\hello.exe. Если g++ ничего не вывел — сборка прошла без ошибок и предупреждений. Это нормальный ответ.
| Ключ | Смысл |
|---|---|
-std=c++17 | стандарт языка, по которому проверяется текст (пункт 04) |
-Wall | включить основные предупреждения |
-Wextra | включить дополнительные предупреждения |
-o hello.exe | имя выходного файла; без ключа получится a.exe |
-static | включить код библиотек в сам exe (ниже — зачем) |
Ошибка (error) — текст не соответствует языку; exe не создаётся. Предупреждение (warning) — текст правильный, exe создаётся, но в нём, скорее всего, ошибка логики. Сообщение всегда имеет вид файл:строка:столбец: вид: текст. Уберём точку с запятой после строки с cout:
hello.cpp: In function 'int main()':
hello.cpp:5:35: error: expected ';' before 'return'
5 | std::cout << "Hello, world!\n"
| ^
| ;
6 | return 0;
Компилятор указывает на место, где заметил ошибку, — конец строки 5 — и подсказывает, чего не хватает. Уберём строку #include <iostream>:
hello.cpp:4:10: error: 'cout' is not a member of 'std'
hello.cpp:1:1: note: 'std::cout' is defined in header '<iostream>';
this is probably fixable by adding '#include <iostream>'
Опечатка в имени main — int mian() — проходит компилятор: функция с таким именем допустима. Ошибку находит компоновщик: exe для Windows должен начинаться с main, а её нет. Библиотека MinGW-w64 в этом случае ищет WinMain — точку входа оконных программ:
...ld.exe: ...: undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status
Предупреждение — объявленная и неиспользуемая переменная (переменные — раздел 3):
hello.cpp:5:9: warning: unused variable 'count' [-Wunused-variable]
Exe, собранный командой выше, на другом компьютере может не запуститься с сообщением «отсутствует libstdc++-6.dll». Код стандартной библиотеки C++ лежит не в exe, а в отдельных DLL в папке bin компилятора; у вас она в Path, у другого человека — нет. Ключ -static включает этот код в exe: файл становится больше, но переносится на любой компьютер с Windows. Что такое DLL и как Windows их ищет — раздел 18.
C++ описан международным стандартом ISO. Стандарт пересматривается раз в три года; компиляторы поддерживают несколько версий и выбирают её ключом -std.
| Стандарт | Что появилось из того, что встретится в курсе |
|---|---|
| C++98 / C++03 | основа языка: типы, ветвления, циклы, функции, классы, iostream, string |
| C++11 | nullptr, auto, enum class, constexpr |
| C++14 | двоичные литералы 0b101, разделитель разрядов 1'000'000 |
| C++17 | стандарт курса; всё, что нужно курсу, в нём есть |
| C++20, C++23 | модули, диапазоны, std::print — в курсе не используются |
Программа курса опирается на две разные библиотеки, и их важно не путать:
| Стандартная библиотека C++ | Windows API | |
|---|---|---|
| Что это | часть стандарта языка | функции операционной системы Windows |
| Где работает | на любой ОС, где есть компилятор C++ | только в Windows |
| Заголовки | <iostream>, <string>, <cmath>, <fstream> | <windows.h>, <winsock2.h> |
| Имена | в пространстве std: std::cout | без пространства имён: SetConsoleOutputCP, CreateProcessA |
| В курсе | ввод-вывод, строки, математика, файлы — разделы 2–12 | консоль, процессы, потоки, каналы, память, DLL, сокеты — разделы 14–19 |
Курс — о системном программировании, поэтому системные механизмы изучаются через Windows API напрямую, а не через обёртки стандартной библиотеки: потоки — через CreateThread, а не std::thread. Контейнеры, шаблоны и исключения C++ в обязательную часть курса не входят.
Разберём hello.cpp построчно.
| Строка | Смысл |
|---|---|
#include <iostream> | директива препроцессора: вставить сюда заголовочный файл iostream с объявлениями потоков ввода-вывода |
int main() | заголовок функции main — с неё начинается выполнение программы; int — функция возвращает целое число |
{ … } | тело функции — блок инструкций |
std::cout << …; | инструкция: вывести значение в стандартный поток вывода; std:: — имя из пространства имён стандартной библиотеки |
return 0; | завершить main и вернуть операционной системе код завершения 0 — «успешно» |
Код завершения видит тот, кто запустил программу. В cmd он хранится в переменной ERRORLEVEL: если заменить return 0; на return 3; и пересобрать —
> hello.exe
Hello, world!
> echo %ERRORLEVEL%
3
По коду завершения командные файлы и другие программы узнают, чем кончился запуск. В разделе 14 одна программа будет запускать другую и читать её код завершения функцией Windows API.
Инструкция — одно действие программы; простая инструкция заканчивается точкой с запятой. Объявление вводит новое имя: переменную, функцию. Блок — инструкции в фигурных скобках; блок может стоять везде, где допустима одна инструкция, и может быть вложенным. Инструкции выполняются строго сверху вниз, в порядке записи, — это следование из раздела 1. Изменить порядок могут только ветвления, циклы и вызовы функций (разделы 4–7).
#include <iostream>
int main()
{
std::cout << "1\n";
{ // вложенный блок
std::cout << "2\n";
}
; // пустая инструкция — допустима
std::cout << "3\n";
return 0;
}
1
2
3
Main, Return и Std::cout — другие имена, а не ошибки оформления, и программа с ними не соберётся. Вторая частая ошибка — using namespace std; в начале файла ради того, чтобы писать cout без std::. В курсе так не делают: в разделах о Windows API имена стандартной библиотеки и системы должны различаться с первого взгляда.Ввод и вывод в C++ — это потоки: std::cout связан с экраном консоли, std::cin — с клавиатурой. Операция << отправляет значение в поток вывода, >> забирает значение из потока ввода в переменную. Операции записываются цепочкой: std::cout << a << b; выводит a, затем b.
Если в строке есть русские буквы, консоль Windows по умолчанию покажет вместо них набор символов:
╨Я╤А╨╕╨▓╨╡╤В <- так выводится "Привет"
Причина — разные кодовые страницы, то есть таблицы «номер байта → символ». Исходный файл сохранён в UTF-8, где буква «П» — два байта D0 9F. Консоль русской Windows по умолчанию работает в кодовой странице 866, где байт D0 — символ ╨, а 9F — буква Я. Байты те же, таблица другая. Решение курса — одна кодовая страница, 1251, для программы и для консоли:
-fexec-charset=cp1251 — компилятор перекодирует строки программы из UTF-8 в 1251;main — функции Windows API SetConsoleOutputCP(1251) и SetConsoleCP(1251): консоль выводит и принимает ввод в той же странице.Программа «анкета» — ввод трёх значений и вывод их в одной строке:
#include <iostream>
#include <string>
#include <windows.h>
int main()
{
SetConsoleCP(1251); // кодовая страница ввода
SetConsoleOutputCP(1251); // кодовая страница вывода
std::string name; // строка
int age; // целое число
double height; // вещественное число
std::cout << "Как вас зовут? ";
std::cin >> name;
std::cout << "Сколько вам лет? ";
std::cin >> age;
std::cout << "Ваш рост в метрах? ";
std::cin >> height;
std::cout << "Имя: " << name << ", возраст: " << age
<< ", рост: " << height << " м\n";
return 0;
}
> g++ -std=c++17 -Wall -Wextra -fexec-charset=cp1251 anketa.cpp -o anketa.exe
> anketa.exe
Как вас зовут? Анна
Сколько вам лет? 17
Ваш рост в метрах? 1.68
Имя: Анна, возраст: 17, рост: 1.68 м
Переменные name, age, height — именованные места в памяти для значений; std::string, int и double — их типы. Здесь они нужны, чтобы было куда вводить; типы и переменные подробно — в разделе 3. SetConsoleCP и SetConsoleOutputCP — первые функции Windows API в курсе; как устроена консоль и её буфер — раздел 15.
1.68, а не 1,68. На вводе 1,68 в height попадёт 1, а запятая останется в потоке. >> читает до первого пробела: имя «Анна Мария» даст в name только «Анна». Как проверить, что ввод удался, — раздел 6. Программы с русским текстом запускайте из cmd, PowerShell или Windows Terminal; в окне «MSYS2 UCRT64» другая кодировка.'\n' и std::endl оба переводят строку. Разница: std::endl ещё и принудительно выталкивает накопленный вывод на экран, что медленнее. В курсе используется '\n'.Строки, начинающиеся с #, — директивы препроцессора. Они выполняются до компиляции и работают с текстом, а не с кодом: препроцессор не знает C++, он только вставляет, заменяет и вырезает строки.
| Директива | Что делает |
|---|---|
#include <файл> | вставить заголовок из папок компилятора — стандартные и системные |
#include "файл" | вставить файл сначала из папки исходника, потом из папок компилятора — свои заголовки |
#define ИМЯ текст | дальше по файлу заменять ИМЯ на текст — макрос |
#ifdef ИМЯ … #endif | оставить строки, только если макрос определён — условная компиляция |
#ifndef, #else, #if 0 | то же с отрицанием, с веткой «иначе»; #if 0 вырезает блок целиком |
#include <iostream>
#define GREETING "Hello"
int main()
{
std::cout << GREETING << ", preprocessor\n";
#ifdef DEBUG
std::cout << "[debug] built with -DDEBUG\n";
#endif
return 0;
}
Макрос можно определить и из командной строки ключом -D — одна программа собирается в двух вариантах без правки текста:
> g++ -std=c++17 -Wall -Wextra define.cpp -o define.exe
> define.exe
Hello, preprocessor
> g++ -std=c++17 -Wall -Wextra -DDEBUG define.cpp -o define.exe
> define.exe
Hello, preprocessor
[debug] built with -DDEBUG
Условной компиляцией отключают отладочный вывод и выбирают код под операционную систему: заголовок windows.h сам состоит из сотен таких проверок. Её же используют в своих заголовках против двойного включения — это будет в разделе 18, когда появятся DLL.
#define SQUARE(x) x * x выражение SQUARE(2 + 3) превращается в 2 + 3 * 2 + 3 и равно 11, а не 25. Поэтому в курсе #define используется только для условной компиляции; именованные константы — const из раздела 3, вычисления — функции из раздела 7.Алфавит языка C++ — латинские буквы A–Z a–z, цифры 0–9, знак подчёркивания _, пробельные символы (пробел, табуляция, перевод строки) и знаки { } [ ] ( ) < > + - * / % = ! & | ^ ~ ? : ; , . " ' # \. Русские буквы и другие символы Юникода в курсе встречаются только внутри строк и комментариев.
Компилятор читает текст не по символам, а по лексемам — минимальным единицам со смыслом, как слова в предложении. Пробелы разделяют лексемы, но сами лексемами не являются. Строка
int total = price * 3 + 10;
состоит из девяти лексем:
| Лексема | Класс |
|---|---|
int | ключевое слово |
total, price | идентификаторы |
=, *, + | знаки операций |
3, 10 | литералы |
; | разделитель |
Идентификатор — имя, которое даёт программист: переменной, функции, типа. Правила:
_; не начинается с цифры;price, Price и PRICE — три разных имени;_ и заглавной буквы и не содержит __ — такие имена зарезервированы за компилятором и библиотекой.| Имя | Допустимо? | Почему |
|---|---|---|
room_area | да | — |
roomArea2 | да | цифра не первая |
2nd_place | нет | начинается с цифры |
room-area | нет | это room минус area |
return | нет | ключевое слово |
_Total | формально да, но нельзя | зарезервировано |
Имена в курсе пишутся по-английски и по смыслу: area, layers, can_count, а не a1 или ploshad. Переменная из раздела 1 «число банок N» в программе — can_count, а не n: через сотню строк однобуквенное имя уже ничего не говорит. Исключение — счётчики циклов i, j (раздел 5).
Ключевые слова — зарезервированные слова языка, их смысл задан стандартом. Всего их в C++17 больше семидесяти; в курсе встретятся эти:
int double char bool void const unsigned long short enum
if else switch case default break continue goto return
for while do struct class public private protected virtual
true false nullptr sizeof static new delete
Знаки операций обозначают действия: + - * / %, = += -=, == != < <=, && || !, << >>. Знак операции может состоять из двух символов: <= — одна лексема, и пробел внутри неё, < =, — ошибка. Операции подробно — в разделе 3. Разделители отделяют части программы: ; заканчивает инструкцию, , разделяет элементы списка, скобки ( ) { } [ ] группируют.
a+++b читается как a++ + b, а не a + ++b. Пробелы между лексемами не обязательны, но ставятся ради читаемости: total = price * 3, а не total=price*3.Литерал — значение, записанное в тексте программы прямо: 42, 3.14, 'A', "Привет". По записи литерала компилятор определяет его тип.
| Вид | Примеры | Замечание |
|---|---|---|
| Целый десятичный | 42, -7, 1'000'000 | апостроф — разделитель разрядов, на значение не влияет |
| Целый восьмеричный | 052 | ведущий ноль — восьмеричная запись: 052 = 42 |
| Целый шестнадцатеричный | 0x2A, 0xFF | так записываются флаги и коды ошибок Windows API |
| Целый двоичный | 0b101010 | двоичная запись, побитовые операции — раздел 3 |
| Вещественный | 3.5, 1.5e3, 2.5e-2 | разделитель — точка; e3 — «умножить на 10³» |
| Символьный | 'A', '7', '\n' | один символ в одинарных кавычках |
| Строковый | "Hello", "" | последовательность символов в двойных кавычках |
| Логический | true, false | раздел 3 |
Некоторые символы в строку не напечатать: перевод строки, табуляцию, саму кавычку. Для них есть escape-последовательности — обратная косая черта и символ после неё, вместе обозначающие один символ:
| Запись | Символ |
|---|---|
\n | перевод строки |
\t | табуляция |
\\ | сама обратная косая черта |
\", \' | двойная и одинарная кавычка |
\0 | символ с кодом 0 — конец строки в массиве char (раздел 10) |
\a | звуковой сигнал |
#include <iostream>
int main()
{
std::cout << 42 << ' ' << 052 << ' ' << 0x2A << ' ' << 0b101010 << '\n';
std::cout << 1'000'000 << '\n';
std::cout << 3.5 << ' ' << 1.5e3 << ' ' << 2.5e-2 << '\n';
std::cout << "Quote: \"C++\"\n";
std::cout << "Path: C:\\new\\table\n";
std::cout << "Wrong: C:\new\table\n";
return 0;
}
42 42 42 42
1000000
3.5 1500 0.025
Quote: "C++"
Path: C:\new\table
Wrong: C:
ew able
Все четыре целых литерала первой строки — одно и то же число 42 в разных системах счисления. В последней строке \n и \t внутри пути стали переводом строки и табуляцией.
"C:\new\table" собирается без ошибок и предупреждений, но это не тот путь. Обратная косая черта в пути пишется двойной: "C:\\new\\table". С разделов о файлах (11) и процессах (14) это будет встречаться в каждой программе. Неизвестную последовательность, например "\d", компилятор отметит предупреждением unknown escape sequence.Комментарий — текст для человека; компилятор его пропускает. Видов два:
// однострочный: от // до конца строки
int layers = 2; // можно после кода
/* многострочный:
от /* до первого */
Многострочные комментарии не вкладываются: первое */ закрывает комментарий, а остаток становится кодом и даёт ошибку. Чтобы временно отключить кусок программы, в котором уже есть /* … */, используют #if 0 … #endif из пункта 07.
Комментарий объясняет то, чего не видно из кода: почему сделано так, откуда взялось число, какое допущение модели здесь реализовано. Пересказывать код не нужно.
// Плохо: повторяет код
paint = area * layers * rate * 1.1; // умножаем площадь на слои, расход и 1.1
// Хорошо: объясняет число из модели
paint = area * layers * rate * 1.1; // запас 10 % на потери (допущение модели)
Правила оформления кода в курсе — их проверяют критерии практических работ:
// Плохо: собирается и работает, но читать нельзя
#include<iostream>
int main(){int a;std::cin>>a;std::cout<<a*a;return 0;}
// Хорошо
// Квадрат целого числа. Автор: Иванов И., группа 21П
#include <iostream>
int main()
{
int number;
std::cin >> number;
std::cout << number * number << '\n';
return 0;
}
g++ выполняет их всеg++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe без единого предупрежденияundefined reference — ошибка компоновщика, а не текстаmain; return из main передаёт операционной системе код завершенияstd и работает везде; Windows API — только в Windows, и через него в курсе изучается система-fexec-charset=cp1251 и SetConsoleOutputCP(1251), SetConsoleCP(1251)#include вставляет файл, #define подставляет текст без учёта смысла052 — это 42, а \ в пути пишется как \\