Где вы можете и не можете объявлять новые переменные в C?

Я слышал (вероятно, от учителя), что нужно объявлять все переменные поверх программы/функции, а объявление новых из этих операторов может вызвать проблемы.

Но затем я читал K & R, и я натолкнулся на это предложение: "Объявления переменных (включая инициализации) могут следовать за левой скобкой, которая вводит какой-либо составной оператор, а не только тот, который начинает функцию". Он следует примеру:

if (n > 0){
    int i;
    for (i=0;i<n;i++)
    ...
}

Я немного поработал с концепцией, и он работает даже с массивами. Например:

int main(){
    int x = 0 ;

    while (x<10){
        if (x>5){
            int y[x];
            y[0] = 10;
            printf("%d %d\n",y[0],y[4]);
        }
        x++;
    }
}

Итак, когда мне не разрешено объявлять переменные? Например, что, если мое объявление переменной не верно после открытия скобки? Как здесь:

int main(){
    int x = 10;

    x++;
    printf("%d\n",x);

    int z = 6;
    printf("%d\n",z);
}

Может ли это вызвать проблемы в зависимости от программы/машины?

Ответ 1

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

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

C99 или C++

Современные компиляторы C, такие как gcc и clang, поддерживают стандарты C99 и C11, которые позволяют объявлять переменную в любом месте, куда может идти оператор. Область видимости переменной начинается с точки объявления до конца блока (следующая закрывающая скобка).

if( x < 10 ){
   printf("%d", 17);  // z is not in scope in this line
   int z = 42;
   printf("%d", z);   // z is in scope in this line
}

Вы также можете объявить переменные внутри для инициализаторов цикла. Переменная будет существовать только внутри цикла.

for(int i=0; i<10; i++){
    printf("%d", i);
}

ANSI C (C90)

Если вы нацелены на более старый стандарт ANSI C, то вы можете объявить переменные сразу после открывающей скобки 1.

Это не означает, что вы должны объявлять все свои переменные в начале ваших функций. В C вы можете поместить блок с разделителями в фигурные скобки в любом месте, куда может идти оператор (не только после таких вещей, как if или for), и вы можете использовать это для введения новых областей видимости переменных. Ниже приведена версия ANSI C предыдущих примеров C99:

if( x < 10 ){
   printf("%d", 17);  // z is not in scope in this line

   {
       int z = 42;
       printf("%d", z);   // z is in scope in this line
   }
}

{int i; for(i=0; i<10; i++){
    printf("%d", i);
}}

1 Обратите внимание, что если вы используете gcc, вам нужно передать флаг --pedantic, чтобы он фактически соблюдает стандарт C90, и жаловаться, что переменные объявлены в неправильном месте. Если вы просто используете -std=c90, это заставляет gcc принять расширенный набор C90, который также позволяет более гибкие объявления переменных C99.

Ответ 2

missingno охватывает то, что разрешает ANSI C, но он не решает, почему ваши учителя сказали вам объявить ваши переменные в верхней части ваших функций. Объявление переменных в нечетных местах может сделать ваш код более трудным для чтения, и это может вызвать ошибки.

В качестве примера возьмем следующий код.

#include <stdio.h>

int main() {
    int i, j;
    i = 20;
    j = 30;

    printf("(1) i: %d, j: %d\n", i, j);

    {
        int i;
        i = 88;
        j = 99;
        printf("(2) i: %d, j: %d\n", i, j);
    }

    printf("(3) i: %d, j: %d\n", i, j);

    return 0;
}

Как вы можете видеть, я дважды объявил i. Ну, точнее, я объявил две переменные, как с именем i. Вы можете подумать, что это приведет к ошибке, но это не так, потому что две переменные i находятся в разных областях. Вы можете видеть это более четко, когда смотрите на выход этой функции.

(1) i: 20, j: 30
(2) i: 88, j: 99
(3) i: 20, j: 99

Сначала мы назначим 20 и 30 на i и j соответственно. Затем внутри фигурных скобок мы назначаем 88 и 99. Итак, почему же j сохраняет свое значение, но i снова возвращается к 20? Это связано с двумя разными переменными i.

Между внутренним набором фигурных скобок переменная i со значением 20 скрыта и недоступна, но поскольку мы не объявили новый j, мы по-прежнему используем j из внешней области. Когда мы оставляем внутренний набор фигурных скобок, i, удерживая значение 88, уходит, и мы снова имеем доступ к i со значением 20.

Иногда такое поведение - это хорошо, иногда, может быть, нет, но должно быть ясно, что если вы используете эту функцию C без разбора, вы действительно можете сделать ваш код запутанным и трудно понятным.

Ответ 3

Если ваш компилятор разрешает это, тогда его штраф объявлять в любом месте. На самом деле код более читабельен (IMHO), когда вы объявляете переменную, в которой вы используете, а не в верхней части функции, потому что она облегчает обнаружение ошибок, например. забыв инициализировать переменную или случайно скрыть переменную.

Ответ 4

Сообщение показывает следующий код:

//C99
printf("%d", 17);
int z=42;
printf("%d", z);

//ANSI C
printf("%d", 17);
{
    int z=42;
    printf("%d", z);
}

и я думаю, что подразумевается, что они эквивалентны. Они не. Если int z помещен в нижней части этого фрагмента кода, он вызывает ошибку переопределения в отношении первого определения z, но не против второго.

Однако несколько строк:

//C99
for(int i=0; i<10; i++){}

действительно работает. Показывает тонкость этого правила C99.

Лично я страстно избегаю этой функции C99.

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

Ответ 5

В соответствии с языком программирования C By K & R -

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

Здесь вы можете увидеть слово обычно, это не обязательно.

Ответ 6

С clang и gcc я столкнулся с серьезными проблемами со следующим. gcc версия 8.2.1 20181011 Clang версия 6.0.1

  {
    char f1[]="This_is_part1 This_is_part2";
    char f2[64]; char f3[64];
    sscanf(f1,"%s %s",f2,f3);      //split part1 to f2, part2 to f3 
  }

ни одному из компиляторов не понравилось, что f1, f2 или f3 находятся внутри блока. Мне пришлось переместить f1, f2, f3 в область определения функции. компилятор не возражал против определения целого числа с блоком.

Ответ 7

Внутри все переменные, локальные для функции, размещаются в стеке или внутри регистров ЦП, а затем сгенерированный машинный код переставляется между регистрами и стеком (так называемый разлив регистров), если компилятор неисправен или если ЦП не имеет достаточно регистров для хранения все шары жонглируют в воздухе.

Для распределения содержимого в стеке ЦП имеет два специальных регистра, один из которых называется указателем стека (SP), а другой - базовым указателем (BP) или указателем фрейма (то есть фреймом стека, локальным для текущей области действия функции). SP указывает внутри текущего местоположения в стеке, а BP указывает на рабочий набор данных (над ним) и аргументы функции (под ним). Когда функция вызывается, она помещает BP стека вызывающей/родительской функции в стек (указывает SP) и устанавливает текущий SP в качестве нового BP, затем увеличивает SP на количество байтов, вылитых из регистров в стек, выполняет вычисления и по возвращении восстанавливает родительский BP путем извлечения его из стека.

Как правило, хранение ваших переменных внутри их собственного {} -scope может ускорить компиляцию и улучшить сгенерированный код за счет уменьшения размера графика, который должен пройти компилятор, чтобы определить, какие переменные используются, где и как. В некоторых случаях (особенно когда задействован goto) компилятор может упустить тот факт, что переменная больше не будет использоваться, если вы явно не укажете компилятору область его использования. Компиляторы могут иметь ограничение по времени/глубине для поиска в графе программы.

Компилятор может помещать переменные, объявленные рядом друг с другом, в одну и ту же область стека, что означает, что загрузка одной из них будет предварительно загружать все остальные в кэш. Аналогично, объявление переменной register может дать компилятору подсказку о том, что вы хотите избежать разлития указанной переменной в стек любой ценой.

Строгий стандарт C99 требует явного { перед объявлениями, в то время как расширения, введенные C++ и GCC, позволяют объявлять переменные дальше в теле, что усложняет операторы goto и case. C++ также позволяет объявлять содержимое внутри для инициализации цикла, что ограничено областью действия цикла.

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

TLDR: использование {} для явного определения области видимости переменных может помочь как компилятору, так и читателю.