Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Что происходит, когда программа запускается

Компилятор выдал исполняемый файл. Дальше за дело берётся операционная система, и полезно представлять, что именно она делает: почти все загадочные падения C++-программ объясняются тем, где лежат данные и как долго они там живут.

Одна память на всё

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

Из этого следует главное свойство C++: указатель – это число. Не «ссылка на объект», не дескриптор, а адрес ячейки. Язык знает, какого типа объект должен лежать по этому адресу, но проверять это во время выполнения никто не будет.

Отсюда же растут и переполнения буфера: если запись вышла за границы массива на стеке, она может затереть адрес возврата, и процессор после return уйдёт исполнять то, что ему подсунули. Ни компилятор, ни процессор здесь ничего не нарушают – они делают ровно то, что написано.

Как программа лежит в памяти

Запущенный процесс видит своё адресное пространство разбитым на области с разным назначением и разными правами.

сегмент кода статические данные куча свободное пространство стек машинные инструкции, только чтение глобальные и static, всё время жизни программы new / delete, растёт вверх запас, за счёт которого растут обе стороны кадры вызовов, растёт вниз 0x00… 0xFF…

Сегмент кода – машинные инструкции, прочитанные из исполняемого файла. Помечен как исполняемый и доступный только для чтения. Сюда же обычно кладут строковые литералы и другие константы.

Статические данные – глобальные переменные и всё, помеченное static. Место под них известно на этапе компиляции, инициализируются они до входа в main, живут до выхода из программы.

Стек – область для локальных переменных и служебных данных вызовов. Размер ограничен (типично 1–8 МБ) и задаётся при запуске потока.

Куча – всё, что выделяется через new. Растёт по запросу, ограничена доступной памятью машины.

int g_total = 0;                     // статические данные

void Handle() {
    int local = 0;                   // стек
    static int calls = 0;            // статические данные, хотя объявлена внутри функции
    int* buffer = new int[1000];     // указатель на стеке, тысяча int – в куче
    delete[] buffer;
}

Обратите внимание на buffer: сама переменная-указатель лежит на стеке и занимает 8 байт, а массив, на который она смотрит, – в куче. Это разные вещи с разным временем жизни, и путаница между ними – источник большинства ошибок с памятью.

Стек против кучи

Стек Куча
Кто освобождает компилятор, на выходе из блока вы, вызовом delete
Стоимость выделения сдвиг регистра, наносекунды поиск подходящего блока
Размер фиксированный лимит потока ограничена памятью машины
Размер объекта должен быть известен на компиляции любой, вычисленный во время работы
Типичная ошибка переполнение стека утечка и висячий указатель

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

Что делает загрузчик

Запуск программы – это не «процессор начал читать файл с начала». Загрузчик операционной системы:

  1. Отображает секции исполняемого файла в адресное пространство, расставляя права (код – исполнять, данные – читать и писать, константы – только читать).
  2. Подгружает динамические библиотеки, которые нужны программе, и досвязывает вызовы к ним – ту самую работу компоновщика, только уже во время запуска.
  3. Инициализирует статические данные и вызывает конструкторы глобальных объектов.
  4. Передаёт управление на main.

Пункт 3 – источник неприятной проблемы, известной как «фиаско порядка статической инициализации»: порядок инициализации глобальных объектов из разных единиц трансляции стандартом не определён. Если один глобальный объект в конструкторе трогает другой, программа работает или падает в зависимости от порядка файлов в командной строке компоновщика. Лечится тем, что глобальных объектов со сложными конструкторами просто не заводят.

Зачем это знать

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

  • Segmentation fault – обращение по адресу, который процессу не принадлежит или недоступен на запись. Почти всегда это разыменование нулевого, неинициализированного или уже освобождённого указателя.
  • Stack overflow – кадры вызовов заполнили отведённую стеку область. Обычно бесконечная рекурсия, реже – огромный массив на стеке.
  • Программа съедает память – кучу выделяют и не освобождают.
  • Программа падает не там, где ошибка – запись за границы массива испортила соседние данные, а обнаружилось это позже. Самый неприятный класс, и именно поэтому в C++ так ценят инструменты вроде санитайзеров и valgrind.