Размещение объявления переменной в C

130

Я долго думал, что в C все переменные нужно объявлять в начале функции. Я знаю, что в C99 правила такие же, как в C ++, но каковы правила размещения объявления переменных для C89 / ANSI C?

Следующий код успешно компилируется с помощью gcc -std=c89и gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

Разве объявления cи не должны sвызывать ошибку в режиме C89 / ANSI?

mcjabberz
источник
55
Просто примечание: переменные в ansi C должны объявляться не в начале функции, а в начале блока. Итак, char c = ... в верхней части цикла for полностью допустимо в ansi C. Однако char * s не будет.
Джейсон Коко

Ответы:

150

Он успешно компилируется, поскольку GCC позволяет sобъявлять расширение GNU, даже если оно не является частью стандартов C89 или ANSI. Если вы хотите строго придерживаться этих стандартов, вы должны передать этот -pedanticфлаг.

Объявление cв начале { }блока является частью стандарта C89; блок не обязательно должен быть функцией.

mipadi
источник
41
Вероятно, стоит отметить, что только объявление sявляется расширением (с точки зрения C89). Объявление cсовершенно законно в C89, никаких расширений не требуется.
AnT
7
@AndreyT: Да, в C объявление переменных должно быть @ началом блока, а не функцией как таковой; но люди путают блок с функцией, поскольку это основной пример блока.
legends2k
1
Я переместил в ответ комментарий с +39 голосами.
MarcH
78

Для C89 вы должны объявить все свои переменные в начале блока области видимости .

Итак, ваше char cобъявление действительно, так как оно находится в верхней части блока области видимости цикла. Но char *sобъявление должно быть ошибкой.

Кили Хикави
источник
2
Совершенно верно. Вы можете объявлять переменные в начале любого {...}.
Artelius
5
@Artelius Не совсем правильно. Только если фигурные скобки являются частью блока (не если они являются частью объявления структуры или объединения или инициализатора в фигурных скобках.)
Йенс
Чтобы быть педантичным, ошибочное объявление должно быть как минимум уведомлено в соответствии со стандартом C. Значит это должна быть ошибка или предупреждение в gcc. То есть не верьте, что программа может быть скомпилирована, что означает, что она соответствует требованиям.
jinawee
36

Группировка объявлений переменных в верхней части блока является устаревшей, вероятно, из-за ограничений старых, примитивных компиляторов C. Все современные языки рекомендуют, а иногда даже предписывают объявление локальных переменных в самый последний момент: там, где они впервые инициализируются. Потому что это избавляет от риска ошибочного использования случайного значения. Разделение объявления и инициализации также не позволяет вам использовать «const» (или «final»), когда вы могли бы.

C ++, к сожалению, продолжает принимать старый способ декларирования обратной совместимости с C (одна проблема совместимости с C выделяется из многих других ...), но C ++ пытается отойти от нее:

  • Дизайн ссылок C ++ не допускает даже такой верхней части группировки блоков.
  • Если вы разделяете объявление и инициализацию локального объекта C ++, вы платите за дополнительный конструктор бесплатно. Если конструктор no-arg не существует, вам снова не разрешено даже разделять оба!

C99 начинает перемещать C в том же направлении.

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

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions

Марш
источник
Также посмотрите, как принудительное объявление переменных в верхней части блока может создать бреши в безопасности: lwn.net/Articles/443037
MarcH
«C ++, к сожалению, продолжает принимать старый способ декларирования обратной совместимости с C»: ИМХО, это просто чистый способ сделать это. Другой язык "решает" эту проблему, всегда инициализируя 0. Bzzt, это только маскирует логические ошибки, если вы спросите меня. И есть довольно много случаев, когда вам НУЖНО объявление без инициализации, потому что существует несколько возможных мест для инициализации. И именно поэтому RAII C ++ действительно вызывает огромную боль - теперь вам нужно включить «действительное» неинициализированное состояние в каждый объект, чтобы учесть эти случаи.
Джо Со
1
@JoSo: Меня смущает, почему вы думаете, что чтение неинициализированных переменных приводит к произвольным эффектам, что упрощает обнаружение программных ошибок, чем когда они дают постоянное значение или детерминированную ошибку? Обратите внимание, что нет никакой гарантии, что чтение неинициализированного хранилища будет вести себя в соответствии с любым битовым шаблоном, который могла бы содержать переменная, или даже что такая программа будет вести себя в соответствии с обычными законами времени и причинно-следственной связи. Учитывая что-то вроде int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat
1
@JoSo: для указателей, особенно в реализациях, которые перехватывают операции null, часто полезно перехватить все биты-ноль. Кроме того, в языках, в которых явно указано, что переменные по умолчанию имеют нулевые биты, использование этого значения не является ошибкой . Составители не все же имеют тенденцию становиться слишком дурацкие с их «оптимизации», но компилятор авторы продолжают пытаться получить больше и больше умных. Параметр компилятора для инициализации переменных с помощью преднамеренных псевдослучайных переменных может быть полезен для выявления ошибок, но просто оставление хранилища с его последним значением иногда может маскировать ошибки.
supercat
22

С точки зрения ремонтопригодности, а не с синтаксической точки зрения, существует как минимум три направления мысли:

  1. Объявите все переменные в начале функции, чтобы они были в одном месте, и вы могли сразу увидеть полный список.

  2. Объявите все переменные как можно ближе к месту их первого использования, чтобы вы знали, зачем каждая из них нужна.

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

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

Иногда я объявляю переменные в меньшем блоке области видимости, но только по уважительной причине, которой у меня очень мало. Один из примеров может быть после a fork()для объявления переменных, необходимых только дочернему процессу. Для меня этот визуальный индикатор - полезное напоминание об их назначении.

Адам Лисс
источник
27
Я использую вариант 2 или 3, чтобы было легче находить переменные, потому что функции не должны быть настолько большими, чтобы вы не могли видеть объявления переменных.
Джонатан Леффлер,
8
Вариант 3 не представляет проблемы, если вы не используете компилятор 70-х годов.
edgar.holleis 05
15
Если вы использовали достойную IDE, вам не нужно было бы искать код, потому что должна быть IDE-команда, которая найдет для вас объявление. (F3 в Eclipse)
edgar.holleis
4
Я не понимаю, как вы можете обеспечить инициализацию в варианте 1, возможно, вы можете получить только начальное значение позже в блоке, вызвав другую функцию или выполнив caclulation.
Plumenator
4
@Plumenator: вариант 1 не обеспечивает инициализацию; Я решил инициализировать их при объявлении либо их «правильными» значениями, либо чем-то, что гарантирует, что последующий код сломается, если они не установлены должным образом. Я говорю «выбрал», потому что с тех пор, как я написал это, мое предпочтение изменилось на №2, возможно, потому, что сейчас я использую Java больше, чем C, и потому что у меня есть лучшие инструменты разработки.
Адам Лисс
6

Как отмечали другие, GCC в этом отношении разрешает (и, возможно, другие компиляторы, в зависимости от аргументов, с которыми они вызываются) даже в режиме «C89», если вы не используете «педантичную» проверку. Если честно, не так много веских причин не проявлять педантичность; качественный современный код всегда должен компилироваться без предупреждений (или очень мало, если вы знаете, что делаете что-то конкретное, что подозрительно для компилятора как возможная ошибка), поэтому, если вы не можете скомпилировать свой код с помощью педантичной настройки, это, вероятно, требует некоторого внимания.

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

Gaidheal
источник
0

Как уже отмечалось, на этот счет существуют две точки зрения.

1) Объявите все в верхней части функций, потому что год 1987.

2) Объявите как можно ближе к первому использованию и в минимально возможном объеме.

Мой ответ - ДЕЛАЙТЕ ОБОИХ! Позволь мне объяснить:

Для длинных функций 1) очень затрудняет рефакторинг. Если вы работаете в кодовой базе, где разработчики выступают против идеи подпрограмм, тогда у вас будет 50 объявлений переменных в начале функции, и некоторые из них могут быть просто «i» для цикла for, который находится в самом начале внизу функции.

Поэтому из этого я развил посттравматическое стрессовое расстройство и попытался применить вариант 2) неукоснительно.

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

Кроме того, анти-шаблон «объявить и установить в NULL», когда вы хотите объявить вверху, но вы не выполнили некоторые вычисления, необходимые для инициализации, разрешается, потому что вещи, которые вам нужно инициализировать, скорее всего, будут получены в качестве аргументов.

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

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

Мой рецепт такой. Для всех локальных переменных возьмите переменную и переместите ее объявление в конец, скомпилируйте, а затем переместите объявление непосредственно перед ошибкой компиляции. Это первое использование. Сделайте это для всех локальных переменных.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Теперь определите блок области видимости, который начинается перед объявлением, и переместите конец, пока программа не скомпилируется.

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Это не компилируется, потому что есть еще код, использующий foo. Мы можем заметить, что компилятор смог пройти через код, который использует bar, потому что он не использует foo. На данный момент есть два варианта. Механический - просто переместить "}" вниз до тех пор, пока он не скомпилируется, а другой вариант - проверить код и определить, можно ли изменить порядок на:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Если порядок можно изменить, вероятно, это то, что вам нужно, потому что это сокращает срок службы временных значений.

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

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

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

Но первый шаг - найти первое применение. Вы можете сделать это визуально, но иногда проще удалить объявление, попытаться скомпилировать и просто вернуть его выше первого использования. Если это первое использование находится внутри оператора if, поместите его туда и проверьте, компилируется ли он. Затем компилятор определит другие варианты использования. Попробуйте создать блок области видимости, охватывающий оба использования.

После того, как эта механическая часть сделана, становится легче анализировать, где находятся данные. Если переменная используется в большом блоке области видимости, проанализируйте ситуацию и посмотрите, используете ли вы одну и ту же переменную для двух разных вещей (например, «i», который используется для двух циклов for). Если использования не связаны, создайте новые переменные для каждого из этих несвязанных видов использования.

Филипп Карфин
источник
0

Вы должны объявить все переменные вверху или «локально» в функции. Ответ:

Это зависит от того, какую систему вы используете:

1 / Встроенная система (особенно связанная с такими жизнями, как самолет или автомобиль): она позволяет использовать динамическую память (например: calloc, malloc, new ...). Представьте, что вы работаете над очень большим проектом с 1000 инженерами. Что делать, если они выделяют новую динамическую память и забыли ее удалить (когда она больше не используется)? Если встроенная система будет работать долгое время, это приведет к переполнению стека и повреждению программного обеспечения. Убедиться в качестве не просто (лучше всего запретить динамическую память).

Если самолет работает в течение 30 дней и не выключается, что произойдет, если программное обеспечение будет повреждено (когда самолет все еще находится в воздухе)?

2 / Другие системы, такие как Интернет, ПК (имеют большой объем памяти):

Вы должны объявить переменную «локально», чтобы оптимизировать использование памяти. Если эти системы работают долгое время и происходит переполнение стека (из-за того, что кто-то забыл удалить динамическую память). Просто сделайте простую вещь, чтобы перезагрузить компьютер: P Это не влияет на жизнь

Dang_Ho
источник
Я не уверен, что это правильно. Полагаю, вы говорите, что легче проводить аудит утечек памяти, если вы объявляете все свои локальные переменные в одном месте? Это может быть правдой, но я не уверен, что куплю это. Что касается пункта (2), вы говорите, что объявление переменной локально «оптимизирует использование памяти»? Теоретически это возможно. Компилятор может выбрать изменение размера кадра стека в процессе выполнения функции, чтобы минимизировать использование памяти, но я не знаю никого, кто бы это делал. На самом деле компилятор просто преобразует все «локальные» объявления в «скрытый запуск функции».
QuinnFreedman
1 / Встроенная система иногда не позволяет использовать динамическую память, поэтому, если вы объявите все переменные в верхней части функции. Когда исходный код собран, он может вычислить количество байтов, которое им нужно в стеке для запуска программы. Но с динамической памятью компилятор не может сделать то же самое.
Dang_Ho
2 / Если вы объявляете переменную локально, эта переменная существует только внутри открывающей / закрывающей скобки "{}". Таким образом, компилятор может освободить пространство переменной, если эта переменная «вне области видимости». Это может быть лучше, чем объявить все в начале функции.
Dang_Ho
Я думаю, что вас смущает статическая и динамическая память. Статическая память выделяется в стеке. Все переменные, объявленные в функции, независимо от того, где они объявлены, выделяются статически. Динамическая память выделяется в куче с чем-то вроде malloc(). Хотя я никогда не видел устройства, которое не способно на это, лучше избегать динамического распределения во встроенных системах ( см. Здесь ). Но это не имеет ничего общего с тем, где вы объявляете свои переменные в функции.
QuinnFreedman
1
Хотя я согласен с тем, что это был бы разумный способ работы, на практике это не так. Вот фактическая сборка чего-то очень похожего на ваш пример: godbolt.org/z/mLhE9a . Как видите, в строке 11 sub rsp, 1008выделяется место для всего массива за пределами оператора if. Это верно для clangи gccв каждой версии и оптимизации уровня я попробовал.
QuinnFreedman
-1

Я процитирую некоторые утверждения из руководства для gcc версии 4.7.0 для ясного объяснения.

«Компилятор может принимать несколько базовых стандартов, таких как« c90 »или« c ++ 98 », и диалекты GNU этих стандартов, такие как« gnu90 »или« gnu ++ 98 ». Указав базовый стандарт, компилятор будет принимать все программы, следующие этому стандарту, а также те, которые используют расширения GNU, которые не противоречат ему. Например, '-std = c90' отключает некоторые функции GCC, которые несовместимы с ISO C90, такие как ключевые слова asm и typeof, но не другие расширения GNU, которые не имеют значения в ISO C90, например, опускание среднего члена в выражении?: ".

Я думаю, что ключевой момент вашего вопроса заключается в том, почему gcc не соответствует C89, даже если используется опция "-std = c89". Я не знаю версию вашего gcc, но думаю, что большой разницы не будет. Разработчик gcc сообщил нам, что опция «-std = c89» означает, что расширения, противоречащие C89, отключены. Таким образом, это не имеет ничего общего с некоторыми расширениями, которые не имеют значения в C89. А расширение, не ограничивающее размещение объявления переменных, принадлежит к расширениям, не противоречащим C89.

Честно говоря, каждый подумает, что он должен полностью соответствовать C89 при первом взгляде на параметр "-std = c89". Но это не так. Что касается проблемы, что объявлять все переменные вначале лучше или хуже, это просто вопрос привычки.

junwanghe
источник
соответствие не означает отказ от расширений: пока компилятор компилирует допустимые программы и производит любую необходимую диагностику для других, он соответствует.
Помните Монику
@Marc Lehmann, да, вы правы, когда слово «согласование» используется для различения компиляторов. Но когда слово «соответствует» используется для описания некоторых видов использования, вы можете сказать: «Использование не соответствует стандарту». И все новички считают, что использование, не соответствующее стандарту, должно вызывать ошибку.
junwanghe
@Marc Lehmann, кстати, нет диагностики, когда gcc видит использование, не соответствующее стандарту C89.
junwanghe
Ваш ответ по-прежнему неверен, потому что утверждение «gcc не соответствует» - это не то же самое, что «некоторая пользовательская программа не соответствует». Ваше использование конформ просто неверно. Кроме того, когда я был новичком, я не придерживался того мнения, о котором вы говорите, так что это тоже неправильно. Наконец, от совместимого компилятора не требуется диагностировать несоответствующий код, и, фактически, это невозможно реализовать.
Помните Монику