Де можна, а що не можна оголошувати нові змінні в C?


77

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

Але тоді я читав K&R, і я натрапив на це речення: "Оголошення змінних (включаючи ініціалізації) можуть слідувати за лівою дужкою, яка вводить будь-яке складене твердження, а не лише те, що починає функцію". Він подає приклад:

Я трохи погрався з концепцією, і вона працює навіть з масивами. Наприклад:

То коли саме мені не дозволяється оголошувати змінні? Наприклад, що робити, якщо моя декларація змінної не відповідає одразу після відкриття фігурної дужки? Як тут:

Чи може це спричинити проблеми залежно від програми / машини?


5
gccдосить розслаблений. Ви використовуєте масиви та декларації змінної довжини c99. Скомпілюйте, gcc -std=c89 -pedanticі на вас закричать. Відповідно до c99, все, що це кошерно.
Дейв

6
Проблема в тому, що ви читали K&R, який застарів.
Лундін,

1
@Lundin Чи є відповідна заміна K&R ?? Після видання ANSI C нічого немає, і читач цієї книги може чітко прочитати, до якого стандарту вона посилається
Брандін,

Відповіді:


126

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

Незважаючи на те, що всі версії C дозволяють використовувати лексичний обсяг блоків, де ви можете оголосити змінні залежно від версії стандарту C, на яку ви націлюєтеся:

C99 або C ++

Сучасні компілятори C, такі як gcc і clang, підтримують стандарти C99 і C11 , які дозволяють оголошувати змінну куди завгодно. Область дії змінної починається з точки оголошення до кінця блоку (наступна закриваюча дужка).

Ви також можете оголосити змінні всередині для ініціалізаторів циклу. Змінна буде існувати лише всередині циклу.

ANSI C (C90)

Якщо ви націлюєтесь на старий стандарт ANSI C , тоді ви обмежуєтесь декларуванням змінних відразу після відкриття фігурної дужки 1 .

Це не означає, що ви повинні оголосити всі свої змінні у верхній частині своїх функцій. У C ви можете поставити блок, розділений фігурними дужками, куди завгодно може піти висловлювання (не лише після речей, таких як ifor for), і ви можете використовувати це для введення нових змінних областей. Далі наведена версія ANSI C попередніх прикладів C99:


1 Зверніть увагу, що якщо ви використовуєте gcc, вам потрібно передати --pedanticпрапор, щоб він фактично застосував стандарт C90, і поскаржитися на те, що змінні оголошені не в тому місці. Якщо ви просто використовуєте -std=c90це, змушує gcc приймати надмножину C90, що також дозволяє більш гнучкі декларації змінних C99.


1
"Сфера дії змінної починається з точки оголошення до кінця блоку" - що, у випадку, якщо хтось задається питанням, не означає, що ручне створення вужчого блоку є корисним / необхідним для того, щоб компілятор ефективно використовував простір стека. Я бачив це пару разів, і це помилковий висновок з помилкового припеву, що C є "портативним асемблером". Оскільки (A) змінна може бути виділена в регістр, а не в стек, & (B), якщо змінна знаходиться в стеку, але компілятор бачить, що ви перестаєте використовувати її, наприклад, 10% шляху через блок, це можна легко переробити цей простір для чогось іншого.
underscore_d

3
@underscore_d Майте на увазі, що люди, які хочуть заощадити пам'ять, часто мають справу із вбудованими системами, де людина або змушена дотримуватися нижчих рівнів оптимізації та / або старих версій компілятора через аспекти сертифікації та / або інструментарію.
класний штабелер

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

2
Я не знаю, звідки у вас ідея, що оголошення змінних в середині області дії - це просто "хак, який ефективно переміщує оголошення вгору". Це не так, і якщо ви спробуєте використати змінну в одному рядку та оголосити її в наступному рядку, ви отримаєте помилку компіляції "змінна не оголошена".
hugomg

3

missingno охоплює те, що дозволяє ANSI C, але він не звертається до того, чому ваші вчителі сказали вам оголошувати свої змінні у верхній частині своїх функцій. Оголошення змінних у непарних місцях може ускладнити читання коду, а це може спричинити помилки.

Візьмемо наступний код як приклад.

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

По-перше, ми призначаємо 20 і 30 відповідно iі j. Потім, всередині фігурних дужок, ми призначаємо 88 і 99. Отже, чому ж тоді значення jзберігає значення, але iзнову повертається до значення 20? Це через дві різні iзмінні.

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

Іноді така поведінка - це добре, інколи, можливо, ні, але повинно бути зрозуміло, що якщо ви користуєтеся цією функцією C без розбору, ви дійсно можете зробити свій код заплутаним і важким для розуміння.


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

3
Як правило, я оголошую всі змінні, які використовуються кілька разів у блоці на початку блоку. Якусь змінну temp, яка призначена десь для локального обчислення, я схильний заявляти, де вона використовується, оскільки вона не представляє інтересу за межами цього фрагмента.
Лундін,

5
Оголошення змінної там, де це потрібно, не обов'язково у верхній частині блоку, часто дозволяє вам її ініціалізувати. Замість того, щоб { int n; /* computations ... */ n = some_value; }писати { /* computations ... */ const int n = some_value; }.
Кіт Томпсон,

@Havok "ви використовували ту саму назву для двох змінних", також відому як "тіньові змінні" ( man gccпотім шукайте -Wshadow). отже, я погоджуюсь, тут показані тіньові змінні.
Тревор Бойд Сміт

1

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


0

Допис містить такий код:

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

Однак кілька рядків:

працює. Показано тонкощі цього правила C99.

Особисто я пристрасно уникаю цієї функції C99.

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


1
Більшість інших людей, які готові взяти на себе відповідальність за відстеження свого кодексу, “заявляють де завгодно” з розпростертими обіймами через численні переваги, які він відкриває для читабельності. І forце нерелевантне порівняння
underscore_d

Це не так складно, як ви робите це звучати. Область дії змінної починається з її оголошення та закінчується наступним }. Це воно! У першому прикладі, якщо ви хочете додати більше рядків, які використовуються zпісля printf, ви робите це всередині блоку коду, а не поза ним. Вам точно не потрібно "сканувати весь блок", щоб перевірити, чи нормально визначати нову змінну. Я повинен визнати, що перший фрагмент є трохи штучним прикладом, і я, як правило, уникаю його через додаткові відступи, які він створює. Однак {int i; for(..){ ... }}шаблон - це те, чим я займаюся постійно.
hugomg

Ваша претензія неточна, оскільки у другому фрагменті коду (ANSI C) ви навіть не можете розмістити другу декларацію int z внизу блоку ANSI C, оскільки ANSI C дозволяє лише розміщувати декларації змінних вгорі. Отже, помилка інша, але результат однаковий. Ви не можете розмістити int z внизу жодного з цих фрагментів коду.
RTHarston

Крім того, в чому проблема наявності кількох рядків цього циклу for? Int i живе лише в блоці циклу for, тому немає витоків і не повторюється визначень int i.
RTHarston

0

Відповідно до мови програмування C від K&R -

У C всі змінні повинні бути оголошені перед їх використанням, як правило, на початку функції перед будь-якими виконуваними операторами.

Тут ви можете побачити слово, як правило, це не обов'язково ..


У наші дні не всі C є K&R - дуже мало поточного коду компілюється із старовинними компіляторами K&R, то чому навіщо використовувати це як посилання?
Тобі Спейт,

Ясність та її здатність пояснити надзвичайні. Я думаю, що добре вчитися у оригінальних розробників. Так, це стародавні, але це добре для початківців.
Gagandeep kaur

0

З clang і gcc я зіткнувся з основними проблемами з наступним. версія gcc 8.2.1 20181011 clang версія 6.0.1

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


0

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

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

Як правило, збереження змінних всередині їх власного {}-cope може прискорити компіляцію та покращити згенерований код за рахунок зменшення розміру графіка, який компілятор повинен пройти, щоб визначити, які змінні де і як використовуються. У деяких випадках (особливо коли задіяний goto) компілятор може пропустити той факт, що змінна більше не використовуватиметься, якщо ви явно не повідомляєте компілятору обсяг її використання. Компілятори можуть мати обмеження часу / глибини для пошуку графіку програми.

Компілятор може розмістити змінні, оголошені поруч один з одним, в одну і ту ж область стека, що означає, що завантаження однієї попередньо завантажить усі інші в кеш. Таким же чином, оголошення змінної register, може дати компілятору натяк про те, що ви хочете уникнути розлиття зазначеної змінної у стеку за будь-яку ціну.

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

І останнє, але не менш важливе: для іншої людини, яка читає ваш код, було б приголомшливо, коли він побачить верх функції, заповнену дев’ятьма оголошеннями змінних, замість того, щоб вони були локалізовані в місцях їх використання. Це також полегшує коментування їх використання.

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


"Строгий стандарт C99 вимагає чіткого {" невірно. Думаю, ви мали на увазі там C89. C99 дозволяє декларації після операторів.
Mecki
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.