Что происходит в C++ при вызове функции?

«Что происходит в C++ при вызове функции?» — вопрос из категории C++ Core, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Вызов функции в C++ — это передача управления от вызывающей функции (caller) к вызываемой (callee). Этот процесс включает несколько шагов на уровне абстрактной машины:

  1. Вычисление аргументов: Все аргументы вычисляются. Порядок вычисления не специфицирован стандартом.
  2. Передача аргументов: Каждый аргумент инициализирует соответствующий параметр функции. По умолчанию используется передача по значению (копирование), но может быть передача по ссылке (&, const &) или по указателю.
  3. Создание кадра стека (stack frame): Выделяется память в стеке для:
    • Возвратного адреса (куда вернуться после выполнения).
    • Локальных переменных функции (включая параметры).
    • Временных объектов, созданных в теле функции.
  4. Передача управления: Инструкция call перенаправляет поток выполнения на первую инструкцию тела функции.
  5. Исполнение тела: Выполняется код внутри функции.
  6. Возврат значения (если есть): Значение из return-выражения инициализирует объект в точке вызова (может involve вызов конструктора перемещения/копирования или оптимизацию RVO/NRVO).
  7. Уничтожение локальных объектов: Деструкторы локальных объектов вызываются в обратном порядке создания.
  8. Восстановление кадра: Управление возвращается по сохраненному адресу, стековый указатель корректируется.

Пример с разными способами передачи:

#include <iostream>
#include <string>

void byValue(int x) { x = 10; } // Изменяет копию
void byReference(int& x) { x = 10; } // Изменяет оригинал
void byConstRef(const std::string& s) { std::cout << s.length(); } // Эффективно для тяжелых объектов

int main() {
    int a = 5;
    byValue(a);     // a останется 5
    byReference(a); // a станет 10

    std::string str = "Hello";
    byConstRef(str); // Избегаем копирования всей строки
    return 0;
}

Важно: Современные компиляторы активно оптимизируют вызовы функций (inlining), что может полностью устранить накладные расходы.