Почему смешивание деклараций и кода запрещено до C99?

Недавно я стал ассистентом преподавателя для университетского курса, который в основном учит C. Курс стандартизован на C90, в основном из-за широко распространенной поддержки компилятора. Одной из очень запутывающих концепций для новичков C с предыдущим опытом Java является правило, что объявления переменных и код не могут быть перемешаны внутри блока (составной оператор).

Это ограничение было наконец снято с C99, но мне интересно: кто-нибудь знает, почему он был там в первую очередь? Упрощает ли процесс анализа переменных? Позволяет ли программисту указать, в каких точках выполнения программы стек должен расти для новых переменных?

Я предполагаю, что разработчики языка не добавили бы такого ограничения, если бы не имели абсолютно никакой цели.

Ответ 1

В самом начале C доступные ресурсы памяти и процессора были действительно скудными. Поэтому он должен был быстро скомпилироваться с минимальными требованиями к памяти.

Поэтому язык C был спроектирован так, чтобы требовать только очень простой компилятор, который быстро компилируется. Это, в свою очередь, приводит к " однопроходному компилятору": компилятор читает исходный файл и как можно скорее переводит все в код ассемблера - обычно при чтении исходного файла. Например: когда компилятор читает определение глобальной переменной, соответствующий код испускается немедленно.

Эта черта видна в C до сегодняшнего дня:

  • C требует "форвардных деклараций" всех и вся. Многопроходный компилятор может смотреть вперёд и выводить декларации переменных функций в одном файле сам по себе.
  • Это, в свою очередь, делает файлы *.h необходимыми.
  • При компиляции функции макет фрейма стека должен быть вычислен как можно скорее - иначе компилятор должен был выполнить несколько проходов над телом функции.

В настоящее время ни один серьезный компилятор C по-прежнему не "один проход", потому что многие важные оптимизации не могут быть выполнены за один проход. Немного больше можно найти в Wikipedia.

Стандартное тело задержалось на некоторое время, чтобы расслабиться, что точка "одного прохода" в отношении тела функции. Я предполагаю, что другие вещи были более важными.

Ответ 2

Это было так, потому что это всегда делалось именно так, это упростило составление компиляторов, и никто не думал об этом иначе. Со временем люди поняли, что было бы более важно облегчить жизнь пользователям языков, а не авторам компилятора.

Я предполагаю, что разработчики языка не добавили бы такого ограничения, если бы не имели абсолютно никакой цели.

Не предполагайте, что разработчики языка решили ограничить язык. Часто такие ограничения возникают случайно и обстоятельством.

Ответ 3

Я думаю, что для не оптимизирующего компилятора должно быть проще создать эффективный код следующим образом:

int a;
int b;
int c;
...

Хотя объявляются 3 отдельные переменные, указатель стека может быть увеличен сразу без оптимизации стратегий, таких как переупорядочение и т.д.

Сравните это с:

int a;
foo();
int b;
bar();
int c;

Чтобы увеличить указатель стека только один раз, для этого требуется своего рода оптимизация, хотя и не очень продвинутая.

Кроме того, в качестве стилистической проблемы, первый подход поощряет более дисциплинированный способ кодирования (неудивительно, что Паскаль тоже применяет это), имея возможность видеть все локальные переменные в одном месте и в конечном итоге осматривать их вместе в целом. Это обеспечивает более четкое разделение между кодом и данными.

Ответ 4

В дни молодежи C, когда Деннис Ритчи работал над этим, компьютеры (например, PDP-11) имели очень ограниченную память (например, 64K слов), а компилятор должен был быть небольшим, поэтому пришлось оптимизировать очень мало вещи и очень просто. И в то время (я закодирован в C на Sun4/110 в 1986-1989 годах), объявление переменных register было действительно полезно для компилятора.

Сегодня компиляторы намного сложнее. Например, недавняя версия GCC (4.6) содержит более 5 или 10 миллионов строк исходного кода (в зависимости от того, как вы ее измеряете), и делает большую часть оптимизаций, которых не было, когда появились первые компиляторы C.

И сегодня процессоры также очень разные (вы не можете предполагать, что сегодня машины похожи на машины с 1980-х годов, но на тысячи раз быстрее и с тысячами больше оперативной памяти и диска). Сегодня иерархия памяти очень важна: кэш-пропуски - это то, что процессор делает больше всего (ожидание данных из ОЗУ). Но в 1980-х годах доступ к памяти был почти таким же быстрым (или медленным, по текущим стандартам), чем выполнение одной машинной команды. Сегодня это совершенно неверно: для чтения вашего модуля RAM ваш процессор, возможно, придется ждать несколько сотен наносекунд, а для данных в кеше L1 он может выполнять больше одной инструкции за каждую наносекунду.

Поэтому не думайте о C как о языке, очень близком к аппаратным средствам: это было верно в 1980-х годах, но сегодня оно ложно.

Ответ 5

oh, но вы могли (в некотором роде) объявить смешение и код, но объявление новых переменных было ограничено началом блока. Например, допустимым является код C89:

void f()
{
  int a;
  do_something();
  {
    int b = do_something_else();
  }
}

Ответ 6

Требование о том, чтобы объявления переменных отображались в начале составного заявления, не ухудшали выразительность C89. Все, что можно было законным образом использовать с объявлением в середине блока, можно было бы сделать так же, добавив открытую скобку перед объявлением и удвоив закрывающую фигуру закрывающего блока. Хотя такое требование иногда может иметь загроможденный исходный код с дополнительными открывающими и закрывающимися фигурными скобками, такие скобки не были бы просто шумом - они бы обозначили области и end для переменных.

Рассмотрим следующие два примера кода:

{
  do_something_1();
  {
    int foo;
    foo = something1();
    if (foo) do_something_1(foo);
  }
  {
    int bar;
    bar = something2();
    if (bar) do_something_2(bar);
  }
  {
    int boz;
    boz = something3();
    if (boz) do_something_3(boz);
  }
}

и

{
  do_something_1();

  int foo;
  foo = something1();
  if (foo) do_something_1(foo);

  int bar;
  bar = something2();
  if (bar) do_something_2(bar);

  int boz;
  boz = something3();
  if (boz) do_something_3(boz);
}

С точки зрения времени выполнения большинство современных компиляторов, вероятно, не заботятся о том, синтаксично ли foo в области видимости во время выполнения do_something3(), так как он может определить, что любое значение, которое оно провело до этого утверждения, не было бы использован после. С другой стороны, поощрение программистов к написанию деклараций таким образом, чтобы генерировать субоптимальный код в отсутствие оптимизирующего компилятора, вряд ли является привлекательной концепцией.

Кроме того, при обработке более простых случаев, связанных с объявлениями смешанных переменных, было бы непросто (даже компилятор 1970 года мог бы это сделать, если бы авторы хотели разрешить такие конструкции), все становится более сложным, если блок, содержащий смешанные объявления, также содержит любые метки goto или case. Создатели C, вероятно, думали, что позволяют смешивать объявления переменных и другие заявления, слишком усложняли бы стандарты, чтобы быть полезными.