Ответ 1
По причинам, которые мне не совсем понятны, почти каждый раз, когда в обсуждении появляется тема C99 VLA, люди начинают говорить в основном о возможности объявления массивов времени выполнения как локальных объектов (т.е. их создания) в стеке. "). Это довольно удивительно и вводит в заблуждение, поскольку этот аспект функциональности VLA - поддержка объявлений локальных массивов - оказывается довольно вспомогательной, вторичной возможностью, предоставляемой VLA. Это на самом деле не играет существенной роли в том, что может сделать VLA. Большую часть времени вопрос о локальных декларациях VLA и связанных с ними потенциальных подводных камнях выдвигается критиками VLA, которые используют его в качестве "соломенного человека", намеревающегося сорвать дискуссию и увязнуть в ней между едва уместными деталями.
Суть поддержки VLA в C - это, прежде всего, революционное качественное расширение языковой концепции типа. Он предполагает введение таких принципиально новых типов типов, как изменяемые типы. Практически каждая важная деталь реализации, связанная с VLA, фактически связана с ее типом, а не с объектом VLA как таковым. Именно введение в язык изменяемых типов является основной частью общеизвестного торта VLA, в то время как способность объявлять объекты таких типов в локальной памяти является ничем иным, как незначительным и довольно несущественным обледенением этого торта.
Учтите это: каждый раз, когда кто-то объявляет что-то подобное в одном коде
/* Block scope */
int n = 10;
...
typedef int A[n];
...
n = 5; /* <- Does not affect 'A' */
Связанные с размером характеристики изменяемого типа A
(например, значение n
) завершаются в тот самый момент, когда управление передает вышеуказанное объявление typedef. Любые изменения в значении n
сделанные далее по линии (ниже этой декларации A
), не влияют на размер A
Остановись на секунду и подумай, что это значит. Это означает, что реализация должна ассоциировать с A
скрытую внутреннюю переменную, в которой будет храниться размер типа массива. Эта скрытая внутренняя переменная инициализируется из n
во время выполнения, когда управление передает объявление A
Это дает приведенному выше объявлению typedef довольно интересное и необычное свойство, чего мы раньше не видели: это объявление typedef генерирует исполняемый код (!). Более того, он генерирует не только исполняемый код, но и критически важный исполняемый код. Если мы как-то забудем инициализировать внутреннюю переменную, связанную с таким объявлением typedef, мы получим "сломанный"/неинициализированный псевдоним typedef. Важность этого внутреннего кода является причиной того, что язык накладывает некоторые необычные ограничения на такие изменяемые объявления: язык запрещает передавать управление в их область извне.
/* Block scope */
int n = 10;
goto skip; /* Error: invalid goto */
typedef int A[n];
skip:;
Еще раз обратите внимание, что приведенный выше код не определяет никаких массивов VLA. Он просто объявляет, казалось бы, невинный псевдоним для изменяемого типа. Тем не менее, это незаконно, чтобы перепрыгнуть через такое объявление typedef. (Мы уже знакомы с такими связанными с прыжком ограничениями в C++, хотя и в других контекстах).
Генерация кода typedef
, typedef
который требует инициализации во время выполнения, является существенным отклонением от того, что typedef
имеет в "классическом" языке. (Это также создает значительные препятствия на пути принятия VLA в C++.)
Когда объявляется фактический объект VLA, в дополнение к выделению фактической памяти массива компилятор также создает одну или несколько скрытых внутренних переменных, которые содержат размер рассматриваемого массива. Нужно понимать, что эти скрытые переменные связаны не с самим массивом, а с его изменяемым типом.
Одним из важных и замечательных следствий этого подхода является следующее: дополнительная информация о размере массива, связанная с VLA, не встроена непосредственно в объектное представление VLA. На самом деле он хранится помимо массива как данные "sidecar". Это означает, что объектное представление (возможно многомерного) VLA полностью совместимо с объектным представлением обычного классического массива размера во время компиляции с той же размерностью и теми же размерами. Например
void foo(unsigned n, unsigned m, unsigned k, int a[n][m][k]) {}
void bar(int a[5][5][5]) {}
int main(void)
{
unsigned n = 5;
int vla_a[n][n][n];
bar(a);
int classic_a[5][6][7];
foo(5, 6, 7, classic_a);
}
Оба вызова функций в вышеприведенном коде совершенно допустимы, и их поведение полностью определяется языком, несмотря на то, что мы передаем VLA, где ожидается "классический" массив, и наоборот. Конечно, компилятор не может контролировать совместимость типов в таких вызовах (так как по крайней мере один из задействованных типов имеет размер во время выполнения). Однако при желании компилятор (или пользователь) имеет все необходимое для выполнения проверки во время выполнения в отладочной версии кода.
(Примечание: Как обычно, параметры типа массива всегда неявно корректируются в параметры типа указателя. Это относится к объявлениям параметров VLA точно так же, как и к "классическим" объявлениям параметров массива. Это означает, что в приведенном выше примере параметр a
фактически имеет тип int (*)[m][k]
. На этот тип не влияет значение n
. Я намеренно добавил несколько дополнительных измерений в массив, чтобы сохранить его зависимость от значений времени выполнения.)
Совместимость между VLA и "классическими" массивами в качестве параметров функции также поддерживается тем фактом, что компилятору не нужно сопровождать изменяемый параметр с какой-либо дополнительной скрытой информацией о его размере. Вместо этого синтаксис языка заставляет пользователя передавать эту дополнительную информацию в открытую. В приведенном выше примере пользователь был вынужден сначала включить параметры n
, m
и k
в список параметров функции. Без предварительного указания n
, m
и k
пользователь не смог бы объявить a
(см. Также примечание о n
). Эти параметры, явно переданные в функцию пользователем, перенесут информацию о фактических размерах a
.
Для другого примера, воспользовавшись поддержкой VLA, мы можем написать следующий код
#include <stdio.h>
#include <stdlib.h>
void init(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
a[i][j] = rand() % 100;
}
void display(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
printf("%2d%s", a[i][j], j + 1 < m ? " " : "\n");
printf("\n");
}
int main(void)
{
int a1[5][5] = { 42 };
display(5, 5, a1);
init(5, 5, a1);
display(5, 5, a1);
unsigned n = rand() % 10 + 5, m = rand() % 10 + 5;
int (*a2)[n][m] = malloc(sizeof *a2);
init(n, m, *a2);
display(n, m, *a2);
free(a2);
}
Этот код предназначен для того, чтобы привлечь ваше внимание к следующему факту: этот код интенсивно использует ценные свойства изменяемых типов. Невозможно элегантно реализовать без VLA. Это основная причина, почему эти свойства крайне необходимы в C, чтобы заменить уродливые хаки, которые ранее использовались вместо них. Тем не менее, в приведенной выше программе в локальной памяти даже не создается ни одного VLA, что означает, что этот популярный вектор критики VLA вообще не применим к этому коду.
По сути, два последних примера, приведенные выше, являются краткой иллюстрацией смысла поддержки VLA.