Ответ
Вызов функции в C++ — это передача управления от вызывающей функции (caller) к вызываемой (callee). Этот процесс включает несколько шагов на уровне абстрактной машины:
- Вычисление аргументов: Все аргументы вычисляются. Порядок вычисления не специфицирован стандартом.
- Передача аргументов: Каждый аргумент инициализирует соответствующий параметр функции. По умолчанию используется передача по значению (копирование), но может быть передача по ссылке (
&,const &) или по указателю. - Создание кадра стека (stack frame): Выделяется память в стеке для:
- Возвратного адреса (куда вернуться после выполнения).
- Локальных переменных функции (включая параметры).
- Временных объектов, созданных в теле функции.
- Передача управления: Инструкция
callперенаправляет поток выполнения на первую инструкцию тела функции. - Исполнение тела: Выполняется код внутри функции.
- Возврат значения (если есть): Значение из
return-выражения инициализирует объект в точке вызова (может involve вызов конструктора перемещения/копирования или оптимизацию RVO/NRVO). - Уничтожение локальных объектов: Деструкторы локальных объектов вызываются в обратном порядке создания.
- Восстановление кадра: Управление возвращается по сохраненному адресу, стековый указатель корректируется.
Пример с разными способами передачи:
#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), что может полностью устранить накладные расходы.
Ответ 18+ 🔞
Давай разберём эту тему, чтобы у тебя в голове всё встало на свои места, а не болталось как хуй с горы. Вызов функции в C++ — это не просто так, "ой, вызвал и забыл". Это целый ритуал на уровне абстрактной машины, и если не понимать, как он работает, потом будешь чесать репу, откуда берутся ошибки.
Слушай сценарий. Ты — вызывающая функция (caller). Ты хочешь делегировать работу другой функции (callee). Что происходит за кулисами?
- Аргументы готовятся. Сначала вычисляется всё, что ты передаёшь в функцию. Но запомни раз и навсегда: порядок вычисления этих аргументов — ёпта, не определён стандартом! Компилятор может делать это как ему вздумается. Так что не строй логику, которая от этого порядка зависит, а то будет тебе хиросима и нигерсраки в одном флаконе.
- Аргументы передаются. Каждый вычисленный аргумент инициализирует соответствующий параметр. Тут вариантов — овердохуища. По умолчанию — передача по значению, то есть создаётся честная копия. Можно по ссылке (
&) — тогда функция будет работать с оригиналом, как с соседской девушкой в бане. Можно по константной ссылке (const &) — это чтобы заглянуть, но не трогать. Можно по указателю — это уже как дать адрес, пусть сам разбирается. - Создаётся кадр стека. Это как личное пространство функции в памяти. Тут хранится:
- Возвратный адрес — чтобы знать, куда тебе пиздовать обратно после работы.
- Локальные переменные — всё, что объявишь внутри, живёт тут.
- Временные объекты — всякая промежуточная хуйня, которая возникает в вычислениях.
- Происходит передача управления. Процессорная команда
call— и всё, ты уже внутри тела новой функции. Предыдущая функция замерла в ожидании. - Исполняется тело. Тут всё понятно: выполняется код, который ты написал. Можешь считать ворон, можешь мир менять.
- Возврат значения. Если функция что-то возвращает, значение из
returnидёт туда, откуда её вызвали. Тут могут вызываться конструкторы копирования или перемещения, но умные компиляторы часто это вырезают через оптимизации (RVO/NRVO), чтобы не делать лишнюю работу. - Уборка за собой. Локальные объекты уничтожаются в порядке, обратном созданию. Вызываются деструкторы — всё честно.
- Возврат домой. Управление прыгает по сохранённому адресу, стек подчищается, и жизнь вызывающей функции продолжается.
Теперь смотри, как это выглядит в коде на практике. Главное — понять разницу в способах передачи, а то будешь потом охуевать, почему переменная не меняется.
#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'. a останется 5, нихуя не поменялось.
std::cout << a << "n"; // Выведет 5
byReference(a); // Передали саму переменную 'a' по ссылке. a станет 10.
std::cout << a << "n"; // Выведет 10
std::string str = "Hello, я длинная строка, копировать меня накладно!";
byConstRef(str); // Передаём ссылку, но защищённую от изменений. Никакого копирования — эффективно и безопасно.
return 0;
}
И последнее, золотое правило: не парься раньше времени. Современные компиляторы — хитрая жопа. Они часто делают подстановку (inlining), особенно для маленьких функций. Это значит, что весь этот пафосный ритуал с кадрами стека может быть полностью вырезан, и код функции будет вставлен прямо в место вызова, как будто его там всегда и было. Но чтобы они могли это сделать, ты должен понимать основы. Иначе будешь писать код, который компилятор не сможет оптимизировать, и потом удивляться, почему всё тормозит.