C является передачей по значению, что означает, что все параметры копируются в кадр стека каждый раз, когда вы вызываете функцию. C также не поддерживает внутренние функции, которые могут получать доступ к локальным переменным или изменять их в лексически объемлющей функции, хотя GNU C делает это как расширение. Единственный способ «передать по ссылке» в C — это передать указатель. Хотя в C++ есть ссылочные типы, это просто указатели за кулисами.
Я подумал о следующем сценарии на гипотетическом низкоуровневом C-подобном языке. Внутри другой функции может быть объявлена функция, видимая только внешней функции и вызываемая только внешней функцией. Если я правильно понимаю, указатель стека увеличивается на фиксированную величину, когда вызывается внутренняя функция, не предполагающая никаких забавных действий, таких как объекты переменного размера. Следовательно, по ходу мыслительного процесса любые локальные переменные, объявленные во внешней функции, будут иметь фиксированные смещения относительно указателя стека даже внутри внутренней функции. Теоретически к ним можно получить доступ так же, как и к другим локальным переменным, без необходимости передачи указателя. И, конечно же, попытка доступа и вызова внутренней функции извне внешней функции приведет к попытке доступа к локальным переменным вне времени жизни во внешней функции и приведет к неопределенному поведению.
Вот иллюстрация: (опять же это не C, а воображаемый C-подобный язык)
void (*address_to_inner)(void);
void outer(void) {
int a = 10;
void inner(void) {
printf("%d\n", a);
}
inner(); // Prints 10
address_to_inner = inner; // Not undefined behavior yet but not good
}
void another(void) {
outer(); // Prints 10
address_to_inner(); // Undefined behavior because inner() tries to access `a` after it was deallocated
}
Применим ли мой мыслительный процесс к типичным архитектурам, таким как x86 или ARM? Некоторые функции, такие как автоматическое управление памятью, не поддерживаются в низкоуровневых языках, таких как C, потому что они изначально не поддерживаются архитектурой и требуют слишком много сложной внутренней работы, чтобы быть осуществимыми для низкоуровневого языка. Но является ли мой сценарий «выполнимым» для языка низкого уровня или существуют ли какие-либо архитектурные или технические ограничения, которые я не учел, которые сделали бы реализацию этого нетривиальной в зависимости от того, как работают стековые/локальные переменные и вызовы функций? Если нет, то почему?
@GeneralGrievance Интересно. Языки, с которыми я знаком, такие как Objective-C или Swift, похоже, обрабатывают замыкания так, как я описал в комментарии. Что произойдет в JS, если я верну замыкание, которое обращается к локальной переменной, и вызываю ее? Обновлено: кажется, что в JS он работает так, как задумано.
@GeneralGrievance Я имею в виду, что это что-то вроде закрытия, но не совсем.
Вложенные функции GNU C становятся интересными, когда вы берете адрес функции и передаете этот указатель на функцию чему-то другому. Затем он должен сделать трамплин из машинного кода (в стеке) и передать указатель на него, который установит статическую цепочку указателей на вложенную функцию, которая может найти кадр стека своего родителя, даже если он не является его дочерним элементом. См. Реализация вложенных функций. Когда он вызывается из своего родителя, IIRC просто встраивается, даже с отключенной оптимизацией.
@PeterCordes Итак, GNU C доходит до того, чтобы заставить его работать, а не вести себя неопределенно.
Локальные переменные родительской функции должны оставаться активными во время вызова вложенной функции через указатель. Указатель функции перестает быть действительным, когда родительская функция возвращается. Таким образом, вызывающий объект по-прежнему должен быть дочерним элементом родителя в дереве вызовов, просто он не обязательно должен быть прямым дочерним элементом. Ваш аргумент о времени жизни var верен, и GCC ничего не делает, чтобы обойти это.
@PeterCordes Так что этот аспект по-прежнему является неопределенным поведением. Но он работает с ограничениями передачи его в качестве указателя на другую функцию, которая будет шифровать смещения относительно локальных переменных во внешней функции.
Следовательно, по ходу мыслительного процесса любые локальные переменные, объявленные во внешней функции, будут иметь фиксированные смещения относительно указателя стека даже внутри внутренней функции.
Это будет работать, хотя вам придется исключить вызов функции среди вложенных функций, что также исключает рекурсию между ними. Заметим, что вложенные функции Паскаля могут вызывать друг друга, причем рекурсивно.
Механизм статической ссылки Pascal на основе стека был подходящим для того времени, когда у машин было мало регистров ЦП, поэтому большинство любых переменных программы Pascal отображались в памяти.
Другая возможность, подходящая для машины с большим количеством регистров ЦП (x64, RISC V, ARM), может состоять в том, чтобы выполнить распределение регистров локальных переменных как внутри, так и между функциями и их вложенными функциями. Таким образом, как локальные переменные могут находиться в регистрах, так и нелокальные переменные тоже могут находиться в регистрах. Это также предотвратило бы рекурсию, но изменило бы производительность.
Наконец, давайте упомянем встраивание, поскольку оно обеспечивает большую эффективность, которую мы ожидаем от машин с большим количеством регистров ЦП, избегая использования памяти для локальных переменных — когда две функции объединяются путем встраивания, они совместно используют одно и то же пространство для хранения локальных переменных; встраивание может даже удалить пару адресов, взятых вызывающей стороной, а затем разыменовать ее внутри вызываемой, что позволяет сопоставить такую локальную переменную с регистром, а не с памятью, как это было бы необходимо, если бы адрес действительно должен был материализоваться. Поскольку встраивание выполняется путем анализа функций компилятором, оно гибко позволяет одной функции вызывать другую, включая рекурсию, хотя некоторые конструкции могут отключать некоторую оптимизацию.
Хм... по крайней мере, в случае закрытия JS внутренние переменные больше не доступны при возврате. Вы случайно не видели этот вопрос?