Змінне розміщення декларації в С


129

Я довго думав, що в 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?


54
Лише зауваження: змінні в ansi C не потрібно оголошувати на початку функції, а на початку блоку. Отже, char c = ... у верхній частині вашого циклу for цикл є повністю законним в ansi C. Однак, char * s не було б.
Джейсон Коко

Відповіді:


149

Він збирається успішно, оскільки GCC дозволяє декларувати sяк розширення GNU, хоча це не є частиною стандарту C89 або ANSI. Якщо ви хочете чітко дотримуватися цих стандартів, ви повинні передати -pedanticпрапор.

Декларація cна початку { }блоку є частиною стандарту C89; блок не повинен бути функцією.


41
Напевно, варто відзначити, що лише декларація sє розширенням (з точки зору C89). Декларація щодо cабсолютно законна у C89, не потрібно розширення.
ANT

7
@AndreyT: Так, в C декларації змінних повинні бути @ початком блоку, а не функцією; але люди плутають блок з функцією, оскільки це основний приклад блоку.
legends2k

1
Я перемістив коментар на +39 голосів у відповідь.
Марч

78

Для C89 ви повинні оголосити всі свої змінні на початку блоку області .

Отже, ваша char cдекларація є дійсною, як і у верхній частині блоку області циклу. Але, в char *sдекларації має бути помилка.


2
Цілком правильно. Ви можете оголосити змінні на початку будь-якого {...}.
Артелій

5
@Artelius Не зовсім коректно. Тільки якщо Curlies є частиною блоку (НЕ , якщо вони є частиною структури або накидний декларації або рамно ініціалізатор.)
Jens

Щоб бути педантичним, помилкову декларацію слід принаймні повідомити відповідно до стандарту C. Отже, це має бути помилка чи попередження в gcc. Тобто не варто вірити, що програма може бути складена, що означає її сумісність.
jinawee

35

Групування декларацій змінних у верхній частині блоку є спадщиною, ймовірно, через обмеження старих примітивних компіляторів 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": IMHO, це просто чистий спосіб зробити це. Інша мова "вирішує" цю проблему, завжди ініціалізуючи 0. Bzzt, що маскує лише логічні помилки, якщо ви запитаєте мене. І є досить багато випадків, коли ви потребуєте декларації без ініціалізації, оскільки існує кілька можливих місць для ініціалізації. І саме тому RAII C ++ - це справді величезна біль у задці - тепер вам потрібно включити "дійсне" неініціалізований стан у кожен об'єкт, щоб дозволити ці випадки.
Jo So

1
@JoSo: Мене бентежить, чому ви думаєте, що зчитування неініціалізованих змінних дасть довільні ефекти, це полегшить виявлення помилок програмування, ніж надання їм послідовного значення або детермінованої помилки? Зауважте, що немає жодної гарантії, що зчитування неінціалізованого сховища буде вести себе таким чином, що відповідає будь-якому бітовому шаблону, який змінна могла б мати, і навіть, що така програма буде вести себе так, як це відповідає звичайним законам часу та причинності. Дано щось на кшталт int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat

1
@JoSo: Для покажчиків, особливо на реалізаціях, на яких виконуються операції з лову null, all-bits-zero часто є корисним значенням пастки. Крім того, у мовах, які явно вказують, що змінні за замовчуванням є всіма бітами-нулями, посилання на це значення не є помилкою . Укладачам не все ж мають тенденцію ставати занадто дурні з їх «оптимізації», але компілятор автори продовжують намагатися отримати більше і більше розумних. Варіант компілятора для ініціалізації змінних за допомогою навмисних псевдовипадкових змінних може бути корисним для виявлення помилок, але лише залишення місця зберігання, яке містить останнє значення, іноді може замаскувати помилки.
supercat

22

З точки зору ремонту, а не синтаксичного, існує як мінімум три напрямки думки:

  1. Заявіть всі змінні на початку функції, щоб вони опинилися в одному місці, і ви зможете побачити вичерпний список з першого погляду.

  2. Оголосіть всі змінні якомога ближче до місця, в якому вони вперше використовуються, щоб ви знали, для чого потрібна кожна.

  3. Заявіть про всі змінні на початку найпотужнішого блоку області, щоб вони якнайшвидше вийшли з рамки та дозволяли компілятору оптимізувати пам'ять та повідомити, чи випадково ви їх використовуєте там, де ви цього не планували.

Як правило, я віддаю перевагу першому варіанту, оскільки я вважаю, що інші часто змушують мене шукати код за деклараціями. Визначення всіх змінних спереду також полегшує ініціалізацію та перегляд їх з налагоджувача.

Я іноді декларую змінні в меншому блоці області, але лише з доброї причини, яких у мене дуже мало. Один із прикладів може бути після fork(), щоб оголосити змінні, необхідні лише дочірньому процесу. Мені цей візуальний показник є корисним нагадуванням про їх призначення.


27
Я використовую варіант 2 або 3, щоб було легше знайти змінні - адже функції не повинні бути такими великими, щоб ви не могли бачити декларації змінних.
Джонатан Леффлер

8
Варіант 3 не є проблемою, якщо ви не використовуєте компілятор з 70-х.
edgar.holleis

15
Якщо ви використовували гідну IDE, вам не потрібно було б шукати код, тому що для отримання декларації для вас повинна бути команда IDE. (F3 in Eclipse)
edgar.holleis

4
Я не розумію, як можна забезпечити ініціалізацію у варіанті 1, можливо, коли ви можете отримати лише початкове значення пізніше в блоці, зателефонувавши до іншої функції або виконуючи калькуляцію.
Плюменатор

4
@Plumenator: варіант 1 не забезпечує ініціалізацію; Я вирішив ініціалізувати їх після декларування, або до їх "правильних" значень, або до того, що гарантує, що наступний код порушиться, якщо вони не встановлені належним чином. Я кажу "вибрав", тому що мої уподобання змінилися на №2 з моменту написання цього, можливо, тому, що зараз я використовую Java більше, ніж на C, і тому, що в мене є кращі інструменти для розробників.
Адам Лісс

6

Як зазначають інші, GCC в цьому плані є дозвільним (і, можливо, іншими компіляторами, залежно від аргументів, з якими вони викликаються) навіть у режимі "C89", якщо ви не використовуєте "педантичну" перевірку. Якщо чесно, то не так багато вагомих причин не мати педантичності; якісний сучасний код завжди повинен складатись без попереджень (або дуже мало, де ви знаєте, що ви робите щось конкретне, що є підозрілим для компілятора як можлива помилка), тому, якщо ви не можете зробити компіляцію коду з педантичною установкою, він, ймовірно, потребує певної уваги.

C89 вимагає, щоб змінні були оголошені перед будь-якими іншими висловлюваннями в межах кожної області, пізніші стандарти дозволяють декларувати ближче до використання (що може бути як більш інтуїтивно зрозумілим, так і більш ефективним), особливо одночасне декларування та ініціалізація змінної керування циклом в циклі 'for'.


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. Ми можемо помітити, що компілятор зміг пройти код, який використовує бар, оскільки він не використовує 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", яка використовується для двох для циклів). Якщо використання не пов'язане між собою, створіть нові змінні для кожного з цих неспоріднених застосувань.


0

Вам слід оголосити всю змінну вгорі або "локально" у функції. Відповідь:

Це залежить від того, який тип системи ви використовуєте:

1 / Вбудована система (особливо пов'язана з життями, такими як літак або автомобіль): вона дозволяє використовувати динамічну пам'ять (наприклад: calloc, malloc, new ...). Уявіть, що ви працюєте у дуже великому проекті з 1000 інженерами. Що робити, якщо вони виділяють нову динамічну пам'ять і забули її видалити (коли вона більше не використовується)? Якщо вбудована система працює тривалий час, це призведе до переповнення стека, і програмне забезпечення пошкодиться. Переконатися в якості непросто (найкращий спосіб - заборона динамічної пам'яті).

Якщо літак працює за 30 днів і не повертається, що станеться, якщо програмне забезпечення пошкоджене (коли літак все ще знаходиться в повітрі)?

2 / Інші системи, такі як Інтернет, ПК (мають великий об'єм пам'яті):

Вам слід оголосити змінну "локально", щоб оптимізувати використання пам'яті. Якщо ці системи працюють тривалий час і переповнення стека трапляється (адже хтось забув видалити динамічну пам'ять). Просто виконайте просту річ, щоб скинути ПК: P Це не впливає на життя


Я не впевнений, що це правильно. Я думаю, ви говорите, що простіше перевірити витоки пам'яті, якщо ви оголосите всі свої локальні змінні в одному місці? Це може бути правдою, але я не дуже впевнений, що купую. Що стосується пункту (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". Але це не так. Щодо проблеми, яка оголошує всі змінні на початку краще чи гірше - це лише справа звички.


відповідність не означає неприйняття розширень: доки компілятор збирає дійсні програми та виробляє будь-яку необхідну діагностику для інших, вона відповідає.
Згадайте Моніку

@Marc Lehmann, так, ви маєте рацію, коли слово "відповідно" використовується для розмежування компіляторів. Але коли слово "відповідно" використовується для опису деяких звичаїв, ви можете сказати "Використання не відповідає стандарту". І всі початківці мають думку, що звичаї, які не відповідають стандарту, повинні спричинити помилку.
junwanghe

@Marc Lehmann, до речі, немає діагностики, коли gcc бачить використання, яке не відповідає стандарту C89.
junwanghe

Ваша відповідь все ще помилкова, оскільки стверджувати, що "gcc не відповідає" - це не те саме, що "якась програма користувача не відповідає". Використання конформу просто неправильне. Крім того, коли я був початківцем, я не був думкою, яку ви висловлюєте, так що це теж неправильно. Нарешті, не існує вимоги до відповідного компілятора діагностувати невідповідний код, і насправді це неможливо здійснити.
Згадайте Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.