Перейти к содержимому
Topos Uranos

Краткое содержание

В этом уроке объясняется, что такое программа и что делает компилятор: машина выполняет лишь элементарные операции над числами в памяти, и исходный код нужно перевести в машинный код, прежде чем его выполнить. На программе из двух файлов урок проходит три этапа этого перевода в C++ (препроцессирование, компиляцию и компоновку) и отличает то, что происходит при компиляции, от того, что происходит при выполнении, на примере ошибки каждого вида. Он сравнивает компилируемые языки с интерпретируемыми, кратко излагает историю C++ вплоть до стандартов C++23 и C++26 и уточняет, какому C++ учит этот курс.

Цели обучения

  1. Объяснить по схеме путь исходного файла до исполняемого файла: препроцессирование, компиляцию и компоновку, с тем, что поступает на вход каждого этапа и что получается на выходе.
  2. Отнести событие к компиляции, к компоновке или к выполнению, например синтаксическую ошибку, отсутствие функции или деление на ноль.
  3. Сравнить компилируемый язык с интерпретируемым по тому, в какой момент обнаруживаются ошибки и что программист видит от машины.
  4. Расположить во времени стандарты языка и объяснить, почему курс использует C++23.

Что такое программа

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

Память компьютера представляет собой длинный ряд пронумерованных ячеек, в каждой из которых хранится число от 0 до 255 (байт). Всё, с чем работает машина, закодировано там в виде чисел: и данные, и сами инструкции. Если это так, то исполняемая программа есть последовательность чисел, которые процессор истолковывает как команды, машинный код, и она зависит от процессора: процессоры семейства x86-64, распространённые в настольных компьютерах, и процессоры ARM, распространённые в телефонах и в последних моделях Mac, понимают не одни и те же числа.

Сегодня никто не пишет на машинном коде. Пишут на языке высокого уровня, таком как C++, инструкции которого напоминают математическую запись, в текстовом файле, который называется исходным кодом (или просто исходником). Отсюда следует необходимость в переводчике, который по исходному коду строит равносильный ему машинный код: в C++ это компилятор.

ПримерПрограмма и то, что выполняет машина

Вот полная программа на C++. Понимать её пока не нужно: каждая её строка будет объяснена в уроке 1.5. Достаточно увидеть, что это текст, что его можно прочитать и что ниже показано то, что она вывела при выполнении.

Программа на C++
// Будет объяснено построчно в уроке 1.5.#include <print>​int main(){    std::println("Привет, мир");    std::println("2 + 3 = {}", 2 + 3);}
Вывод
Привет, мир2 + 3 = 5

Чтобы увидеть другой конец перевода, рассмотрим функцию, которая лишь складывает два целых числа (функции будут изучаться в разделе 5). Это не полная программа, потому что в ней нет main, точки, с которой начинается выполнение, но компилятор может перевести её и отдельно.

Код на C++
int add(int a, int b){    return a + b;}

Программа objdump показывает машинный код, который GCC 15 породил для неё на компьютере x86-64 с Linux: в каждой строке указаны позиция инструкции, её байты в шестнадцатеричной записи и её удобочитаемое имя.

Терминал
$ g++ -std=c++23 -O1 -c add.cpp$ objdump -d add.o0000000000000000 <_Z3addii>:   0:	f3 0f 1e fa          	endbr64   4:	8d 04 37             	lea    (%rdi,%rsi,1),%eax   7:	c3                   	ret

Целочисленное сложение свелось к восьми байтам. Первая инструкция, endbr64, есть защитная метка, которая ничего не вычисляет; вторая, lea, складывает два числа, полученные функцией в двух регистрах процессора (%rdi и %rsi), и помещает результат в третий (%eax); третья, ret, возвращает управление в точку, откуда функция была вызвана. Имя _Z3addii есть способ, которым компилятор кодирует «функцию add с двумя параметрами int».

Из примера следуют два наблюдения. Первое: машинный код не сохраняет ни имён a и b, ни отступов, ни комментариев; всё это существовало для того, кто читает исходный код, и компилятор воспользовался этим, чтобы решить, какие инструкции записать. Второе: то, что пишет программист на C++, переводится в немногие инструкции и без посредников; эта близость к машине есть причина эффективности C++ и, как будет видно в курсе, также некоторых его опасностей.