Параметры и возвращаемые значения
Параметры — значения, скопированные в новый кадр до начала выполнения тела функции. Возвращаемое значение передаётся вызывающей стороне при срабатывании RET. Оба механизма передают данные через границу кадра стека.
Ты уже умеешь трассировать стек вызовов по мере вызова и возврата функций. Следующий вопрос: как данные попадают в функцию и как результат возвращается обратно?
Функция, которая всегда работает с одними и теми же фиксированными значениями, почти бесполезна. Настоящие функции принимают параметры — значения, предоставленные вызывающей стороной в момент вызова — и производят возвращаемое значение, которое вызывающая сторона может использовать. Эти два механизма — интерфейс между функцией и кодом, который её вызывает, и оба работают через кадр стека. К концу урока ты сможешь трассировать точное состояние стека до, во время и после вызова — включая то, где живут входные значения и как результат возвращается обратно.
После этого урока ты сможешь объяснить, что такое параметр (значение, скопированное в новый кадр в момент вызова), что такое возвращаемое значение (значение, произведённое функцией и переданное вызывающей стороне), и трассировать вызов функции с параметрами и возвращаемым значением, показывая содержимое кадра на каждом шаге.
Параметры объявляются в сигнатуре функции. Когда вызывающая сторона выдаёт CALL,
значения, которые она передаёт (называемые аргументами), копируются в новый кадр до
начала выполнения тела функции. Внутри функции параметры выглядят и ведут себя точно как
локальные переменные — это именованные ячейки внутри кадра. Разница в том, что они
заранее заполнены значениями вызывающей стороны, а не устанавливаются присваиванием.
В синтаксисе TypeScript function add(x: number, y: number): number объявляет два
параметра x и y. В точке вызова add(3, 5) значения 3 и 5 — аргументы,
которые копируются в ячейки x и y нового кадра.
Возвращаемые значения работают в обратном направлении. Когда тело функции достигает
оператора return, оно вычисляет возвращаемое значение и делает его доступным для
вызывающей стороны. Аппаратно это обычно делается путём помещения возвращаемого значения
в регистр CPU (быстрый слот хранения внутри самого CPU) до срабатывания RET. Вызывающая
сторона затем читает этот регистр после того, как RET восстанавливает счётчик команд.
Ключевой момент: параметры передают данные в кадр; возвращаемое значение передаёт
данные из кадра. Оба пересечения происходят на границе кадра — в момент CALL (вход)
и RET (выход).
Функция для трассировки: add(x, y) принимает два числа и возвращает их сумму.
Трассировка показывает add(3, 5) с состоянием стека до вызова, во время выполнения
и после возврата.
function add(x: number, y: number): number { let result = x + y; // result — локальная переменная в кадре add return result; // передаёт result обратно вызывающей стороне } function main(): void { let total = add(3, 5); // вызывающая сторона: аргументы 3 и 5 переданы в add // total теперь хранит 8 } - L1 x и y — параметры: живут в кадре add, заранее заполнены аргументами вызывающей стороны
- L2 result — локальная переменная: кадр add теперь содержит x, y и result
- L3 return: помещает значение result в регистр, затем срабатывает RET
- L7 add(3,5): аргументы 3 и 5 копируются в новый кадр add как x и y
- L8 после RET: регистр с 8 прочитан; total присваивается 8 в кадре main
Трассировка main(), которая вызывает add(3, 5). Каждая ячейка показывает содержимое
одного кадра.
function add(x: number, y: number): number { let result = x + y; return result; } function main(): void { let total = add(3, 5); } ▸Почему это работает
Почему параметры копируются в кадр, а не используются совместно с вызывающей стороной?
Потому что каждый вызов функции нуждается в независимом рабочем пространстве. Если бы
add могла изменять 3 и 5 вызывающей стороны, вызвать add дважды с разными
аргументами было бы невозможно осмыслить. Копирование значений означает, что кадр
вызываемой функции полностью самодостаточен — она может свободно изменять ячейки x
и y, не затрагивая ничего в кадре вызывающей стороны. Это поведение «копировать при
входе» называется передачей по значению и является стандартом в большинстве языков
для примитивных значений, таких как числа.
function multiply(a: number, b: number): number { return a * b; } — Сколько параметров у multiply?
Вызывается multiply(4, 6). Какое значение возвращает multiply?
После возврата multiply(4, 6) сколько кадров стека на стеке (при условии, что она была вызвана из main и ничего больше не активно)?
function square(n: number): number { return n * n; } — Вызывается square(7). Какое значение помещается в регистр возврата?
function add(x: number, y: number): number { let r = x + y; return r; } — Кадр add содержит x, y и r. Сколько именованных ячеек содержит кадр add?
При вызове add(3, 5), где живут значения 3 и 5 во время выполнения add?
Параметры — именованные ячейки в кадре стека функции, заранее заполненные
аргументами (значениями), которые вызывающая сторона передала в момент вызова.
Внутри функции параметры ведут себя точно как локальные переменные. Значения
копируются в кадр (передача по значению для примитивов), поэтому у вызываемой
функции есть независимое рабочее пространство. Возвращаемое значение производится
оператором return: значение помещается в регистр CPU, срабатывает RET, кадр
снимается, и вызывающая сторона читает возвращаемое значение из регистра. Параметры
несут данные в кадр при CALL; возвращаемое значение несёт данные из кадра при
RET. Теперь, когда видишь сигнатуру вроде add(x: number, y: number): number, ты
знаешь точную историю в памяти: две ячейки входят в новый кадр при вызове, одно
значение выходит через регистр при возврате, и данные не утекают через границы кадров.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.