Раздел 2 — Язык C++: сборка, структура программы, лексика

Первая программа на C++ в Windows: от исходного текста до exe и из чего этот текст состоит

Прогресс курса Раздел 2 из 20

Что вы освоите в этом разделе

12 академических часов теории, практической работы в разделе нет. С этого раздела нужен компьютер с Windows 10 или 11. Все программы страницы — полные: их можно скопировать, собрать и запустить. Раздел заканчивается тестом на портале; следующий раздел открывается после теста.
01

От исходного текста до 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 для Windowsg++ 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.
02

Установка MinGW-w64

В курсе используется компилятор g++ из проекта MinGW-w64 — это GCC, собранный для Windows. Он создаёт обычные exe, которые работают без дополнительных сред, и умеет вызывать функции Windows API — они нужны с раздела 14. Установить можно двумя способами; компилятор в обоих один и тот же.

Способ 1 — MSYS2 (домашний компьютер)

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 (ниже — как).

Способ 2 — WinLibs (компьютер без прав администратора)

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
«g++ не является внутренней или внешней командой» — папки нет в Path, или окно открыто до её добавления. Проверьте путь в Path: в нём должна быть именно папка bin, внутри которой лежит g++.exe. Папку для своих программ создайте с путём без пробелов и русских букв, например C:\cpp. Исходный текст можно писать в любом редакторе, который сохраняет файлы в кодировке UTF-8: VS Code, Notepad++, «Блокнот» Windows 10/11.
03

Первая сборка, ошибки и предупреждения

Первая программа — файл 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>'

Опечатка в имени mainint 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.

04

Стандарты C++, стандартная библиотека и Windows API

C++ описан международным стандартом ISO. Стандарт пересматривается раз в три года; компиляторы поддерживают несколько версий и выбирают её ключом -std.

СтандартЧто появилось из того, что встретится в курсе
C++98 / C++03основа языка: типы, ветвления, циклы, функции, классы, iostream, string
C++11nullptr, 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++ в обязательную часть курса не входят.

05

Структура программы

Разберём 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 имена стандартной библиотеки и системы должны различаться с первого взгляда.
06

Ввод-вывод и русский язык в консоли

Ввод и вывод в C++ — это потоки: std::cout связан с экраном консоли, std::cin — с клавиатурой. Операция << отправляет значение в поток вывода, >> забирает значение из потока ввода в переменную. Операции записываются цепочкой: std::cout << a << b; выводит a, затем b.

Если в строке есть русские буквы, консоль Windows по умолчанию покажет вместо них набор символов:

╨Я╤А╨╕╨▓╨╡╤В                <- так выводится "Привет"

Причина — разные кодовые страницы, то есть таблицы «номер байта → символ». Исходный файл сохранён в UTF-8, где буква «П» — два байта D0 9F. Консоль русской Windows по умолчанию работает в кодовой странице 866, где байт D0 — символ , а 9F — буква Я. Байты те же, таблица другая. Решение курса — одна кодовая страница, 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'.
07

Препроцессор

Строки, начинающиеся с #, — директивы препроцессора. Они выполняются до компиляции и работают с текстом, а не с кодом: препроцессор не знает 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.
08

Алфавит и лексемы

Алфавит языка C++ — латинские буквы A–Z a–z, цифры 0–9, знак подчёркивания _, пробельные символы (пробел, табуляция, перевод строки) и знаки { } [ ] ( ) < > + - * / % = ! & | ^ ~ ? : ; , . " ' # \. Русские буквы и другие символы Юникода в курсе встречаются только внутри строк и комментариев.

Компилятор читает текст не по символам, а по лексемам — минимальным единицам со смыслом, как слова в предложении. Пробелы разделяют лексемы, но сами лексемами не являются. Строка

int total = price * 3 + 10;

состоит из девяти лексем:

ЛексемаКласс
intключевое слово
total, priceидентификаторы
=, *, +знаки операций
3, 10литералы
;разделитель

Идентификатор — имя, которое даёт программист: переменной, функции, типа. Правила:

ИмяДопустимо?Почему
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.
09

Литералы и escape-последовательности

Литерал — значение, записанное в тексте программы прямо: 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 внутри пути стали переводом строки и табуляцией.

Пути Windows в строках — главная ловушка escape-последовательностей. Путь "C:\new\table" собирается без ошибок и предупреждений, но это не тот путь. Обратная косая черта в пути пишется двойной: "C:\\new\\table". С разделов о файлах (11) и процессах (14) это будет встречаться в каждой программе. Неизвестную последовательность, например "\d", компилятор отметит предупреждением unknown escape sequence.
10

Комментарии и оформление кода

Комментарий — текст для человека; компилятор его пропускает. Видов два:

// однострочный: от // до конца строки
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 передаёт операционной системе код завершения
  • Стандартная библиотека C++ — в пространстве std и работает везде; Windows API — только в Windows, и через него в курсе изучается система
  • Русский текст в консоли — -fexec-charset=cp1251 и SetConsoleOutputCP(1251), SetConsoleCP(1251)
  • Препроцессор работает с текстом: #include вставляет файл, #define подставляет текст без учёта смысла
  • Текст программы состоит из лексем: ключевых слов, идентификаторов, литералов, знаков операций и разделителей; 052 — это 42, а \ в пути пишется как \\
Связи раздела. Опирается на раздел 1: программа записывает алгоритм, инструкции выполняются в порядке следования. Нужен для: раздела 3 — типы и переменные, которые здесь появились в анкете; всех разделов с кодом — сборка, чтение ошибок и оформление; раздела 14 — код завершения процесса; раздела 15 — кодовые страницы консоли; раздела 18 — DLL, из-за которых exe не запускается на чужом компьютере.
Тест по разделу — на учебном портале. Тест, попытки и оценки — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 1: Задача, модель, алгоритм Тест на портале