Чому ці конструкції використовують невизначену поведінку до і після збільшення?


814
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

12
@Jarett, nope, просто потрібні були деякі покажчики на "точки послідовності". Під час роботи я знайшов фрагмент коду з i = i ++, я думаю: "Це не змінює значення i". Я тестував і цікавився, чому. Оскільки я видалив цю позицію та замінив її на i ++;
PiX

198
Я думаю, що цікаво, що ВСЕ ТАКОЖ припускають, що такі питання задаються тому, що запитуючий прагне використати цю конструкцію. Моє перше припущення було те, що PiX знає, що це погано, але цікаво, чому вони поводяться так, як це робив компілятор whataver, який він / він використовував ... І так, те, що розмовляєш ... це не визначено, це може зробити що завгодно. .. в тому числі JCF (Jump and Catch Fire)
Брайан Постув

32
Мені цікаво: чому компілятори, схоже, не попереджають про такі конструкції, як "u = u ++ + ++ u;" якщо результат не визначений?
Дізнайтеся OpenGL ES

5
(i++)як і раніше оцінює до 1, незалежно від дужок
Дрю Макгоуен

2
Що б не i = (i++);було задумано зробити, безумовно, є більш чіткий спосіб написати це. Це було б правдою, навіть якби це було чітко визначено. Навіть у Java, яка визначає поведінку i = (i++);, це все ще поганий код. Просто напишітьi++;
Кіт Томпсон

Відповіді:


566

C має поняття не визначеної поведінки, тобто деякі мовні конструкції є синтаксично допустимими, але ви не можете передбачити поведінку під час запуску коду.

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

Отже, маючи на увазі, чому саме ці "питання"? Мова чітко говорить про те, що певні речі призводять до невизначеної поведінки . Немає жодних проблем, не бере участь «слід». Якщо невизначена поведінка змінюється, коли оголошується одна із залучених змінних volatile, це нічого не підтверджує і не змінює. Не визначено ; Ви не можете міркувати про свою поведінку.

Ваш найцікавіший приклад, той, з яким

u = (u++);

- це приклад з підручників із невизначеною поведінкою (див. запис Вікіпедії про пункти послідовності ).


8
@PiX: речі не визначені з кількох можливих причин. До них належать: немає чіткого «правильного результату», різні архітектури машин сильно б сприяли різним результатам, існуюча практика не є послідовною або виходить за рамки стандарту (наприклад, які імена файлів є дійсними).
Річард

Просто для того, щоб заплутати всіх, деякі подібні приклади зараз чітко визначені у С11, наприклад i = ++i + 1;.
ММ

2
Читаючи Стандарт та опубліковані обґрунтування, зрозуміло, чому існує концепція UB. Стандарт ніколи не мав на меті повністю описати все, що має виконувати реалізація C, щоб бути придатним для будь-якої конкретної мети (див. Обговорення правила "Єдиної програми"), а натомість покладається на судження виконавців та бажання виробляти корисні якості. Якісна реалізація, придатна для програмування систем низького рівня, повинна визначати поведінку дій, які не потрібні були б у великих класах. Замість того, щоб спробувати ускладнити Стандарт ...
supercat

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

1
@jrh: Я написав цю відповідь ще до того, як зрозумів, як з рук вийшла філософія гіпермодерну. Що мене дратує - це прогрес від "Нам не потрібно офіційно визнавати таку поведінку, тому що платформи, де це потрібно, так чи інакше можуть її підтримувати" до "Ми можемо видалити таку поведінку, не надаючи зручну заміну, оскільки вона ніколи не була розпізнана, і, таким чином, будь-який код потребуючий був зламаний ". Багато поведінки мали бути давно застарілими на користь заміни, які були в будь-якому кращому вигляді , але це вимагало б визнання їх законності.
supercat

78

Просто складіть і розберіть свій рядок коду, якщо ви настільки схильні знати, як саме ви отримуєте те, що отримуєте.

Це те, що я отримую на своїй машині разом з тим, що, на мою думку, відбувається:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Я думаю, що інструкція 0x00000014 була якоюсь оптимізацією компілятора?)


як мені отримати машинний код? Я використовую програму Dev C ++, і в налаштуваннях компілятора я розігрувався з опцією 'Генерація коду', але не виходжу додаткового виводу файлів або будь-якого консольного виходу
bad_keypoints

5
@ronnieaka gcc evil.c -c -o evil.binі gdb evil.bindisassemble evil, або що б там не було для Windows еквівалентів :)
badp

21
Ця відповідь насправді не стосується питання Why are these constructs undefined behavior?.
Шафік Ягмур

9
Убік, це буде простіше скласти до складання (з gcc -S evil.c), що тут все, що потрібно. Збирання та розбирання - це лише круговий спосіб зробити це.
Кет

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

64

Я думаю, що відповідні частини стандарту C99 - це 6.5 вирази, §2

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

та 6.5.16 Оператори призначення, §4:

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


2
Чи означає вище, що "i = i = 5;" буде не визначеною поведінкою?
supercat

1
@supercat, наскільки я знаю, i=i=5це також невизначена поведінка
dhein

2
@Zaibis: Обгрунтування, яке я люблю використовувати для більшості місць правила, застосовує те, що теоретично платформа mutli-процесорів може реалізувати щось на кшталт A=B=5;"Write-lock A; Write-Lock B; Store 5 to A; store 5 to B; Unlock B ; Зняти A; ", і такий вираз, як C=A+B;" Read-lock A; Read-lock B; Compute A + B; Unlock A and B; Block Write C; Результат зберігання; Unlock C; "). Це забезпечило б те, що якщо один потік зробив, A=B=5;а інший робив, C=A+B;то останній потік буде бачити, що обидва записи відбулися, або ні. Потенційно корисна гарантія. Якщо одна нитка все-таки зробила I=I=5;...
supercat

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

1
@supercat, але чи не було б правилом доступу до точки99 послідовності лише c99 достатньо, щоб оголосити це як невизначене поведінку? Тож не має значення, що технічно обладнання може реалізувати?
dhein

55

Більшість відповідей тут цитується зі стандарту С, підкреслюючи, що поведінка цих конструкцій не визначена. Щоб зрозуміти, чому поведінка цих конструкцій не визначена , давайте розберемося спочатку в цих термінах з урахуванням стандарту C11:

Послідовно: (5.1.2.3)

З урахуванням будь-яких двох оцінок Aі B, якщо Aїх послідовно попередньо B, то виконання Aповинно передувати виконанню B.

Без наслідків:

Якщо AНЕ секвенували до або після B, то Aі Bє unsequenced.

Оцінка може бути однією з двох речей:

  • обчислення значень , які опрацьовують результат вираження; і
  • побічні ефекти , що є модифікаціями об’єктів.

Точка послідовності:

Наявність точки послідовності між оцінкою виразів Aі Bозначає, що кожне обчислення значення та пов'язаний з ним побічний ефектA секвенується перед кожним обчисленням значення та побічним ефектом, пов'язаним з B.

Тепер підійдемо до питання, для виразів на кшталт

int i = 1;
i = i++;

Стандарт говорить, що:

6.5 Вирази:

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

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

Нехай перейменовується iліворуч від присвоєння be, ilа праворуч від призначення (у виразі i++) be ir, тоді вираз буде подібним

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Важливим моментом щодо Postfix- ++оператора є:

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

Це означає, що вираз il = ir++можна оцінити як

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

або

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

що призводить до двох різних результатів , 1і 2який залежить від послідовності побічних ефектів шляхом привласнення та ++й , отже , викликає UB.


52

Поведінку насправді не можна пояснити, оскільки вона посилається як на неуточнену поведінку, так і на невизначену поведінку , тому ми не можемо робити жодних загальних прогнозів щодо цього коду, хоча якщо ви прочитаєте твір Ольве Модаля, такий як Deep C та Unspecified and Undefined, іноді ви можете зробити добре здогадки у дуже конкретних випадках із конкретним компілятором та середовищем, але, будь ласка, не робіть цього ніде поблизу виробництва.

Отже, переходячи до не визначеної поведінки , у пункті 3 стандартного розділу c99 сказано ( моє наголос ):6.5

Групування операторів і операндів позначається синтаксисом.74) За винятком випадків, зазначених пізніше (для операторів функції-виклику (), &&, ||,?: І комами), порядку оцінки піддепресій і порядку в які побічні ефекти мають місце і не визначено.

Отже, коли у нас є такий рядок:

i = i++ + ++i;

ми не знаємо , є чи i++або ++iбуде оцінюватися першим. Це головним чином, щоб дати компілятору кращі варіанти оптимізації .

У нас також є невизначене поведінку тут, а оскільки програма зміни змінних ( i, uі т.д ..) більше , ніж один раз між точками послідовності . З 6.5пункту 2 проекту стандартного розділу ( моє наголос ):

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

він наводить такі приклади коду як невизначені:

i = ++i + 1;
a[i++] = i; 

У всіх цих прикладах код намагається змінити об'єкт більше одного разу в одній і тій же точці послідовності, яка закінчуватиметься ;в кожному з цих випадків:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Невизначена поведінка визначена в проекті стандарту c99 у розділі 3.4.4як:

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

а невизначена поведінка визначається в розділі 3.4.3як:

поведінка при використанні неподатної або помилкової побудови програми або помилкових даних, до яких цей Міжнародний стандарт не пред'являє жодних вимог

і зазначає, що:

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


33

Ще один спосіб відповісти на це, а не занурюватися в таємні подробиці точок послідовності та невизначеної поведінки - просто запитати, що вони повинні означати? Що намагався зробити програміст?

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

Другий фрагмент, i = i++трохи легше зрозуміти. Хтось явно намагається збільшити i і присвоїти результат назад i. Але є кілька способів зробити це в C. Найбільш основний спосіб додати 1 до i та присвоїти результат назад i, той самий майже в будь-якій мові програмування:

i = i + 1

C, звичайно, має зручний ярлик:

i++

Це означає, "додай 1 до i і призначи результат назад до i". Так що, якщо ми побудуємо між собою два халяви, пишучи

i = i++

що ми говоримо насправді, це "додати 1 до i, і присвоїти результат назад i, і присвоїти результат назад i". Ми розгублені, тому це не дуже мене турбує, якщо компілятор теж заплутався.

Реально кажучи, єдиний раз, коли ці божевільні висловлювання пишуть, це коли люди використовують їх як штучні приклади того, як ++ має працювати. І звичайно важливо зрозуміти, як працює ++. Але одне практичне правило використання ++: "Якщо не очевидно, що означає вираз із використанням ++, не пишіть його".

Ми витрачали незліченну кількість годин на comp.lang.c, обговорюючи такі вирази, і чому вони не визначені. Два моїх більш довгих відповіді, які намагаються реально пояснити чому, архівуються в Інтернеті:

Див. Також питання 3.8 та решту питань у розділі 3 списку C FAQ .


1
Досить неприємною проблемою щодо невизначеної поведінки є те, що хоча 99,9% компіляторів раніше було безпечно, *p=(*q)++;це означає, if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;що це вже не так. Гіперсучасний C вимагає написання чогось подібного до останнього формулювання (хоча стандартний спосіб вказівки коду не байдуже, що є *p), щоб досягти рівня компіляторів ефективності, використовуваних для надання попереднього ( elseпункт необхідний для того, щоб компілятор оптимізує те, ifчого потребують деякі нові компілятори).
supercat

@supercat Зараз я вважаю, що будь-який компілятор, достатньо розумний для здійснення такої оптимізації, також повинен бути достатньо розумним, щоб зазирнути на assertзаяви, щоб програміст міг передувати питання, про який йде мова, просто assert(p != q). (Звичайно, для цього курсу також знадобиться переписання, <assert.h>щоб не видаляти твердження прямо в невідладних версіях, а, скоріше, перетворити їх на щось подібне, __builtin_assert_disabled()що може бачити власний компілятор, а потім не видавати код.)
Steve Summit

25

Часто це питання пов'язується як дублікат питань, пов'язаних з подібним кодом

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

або

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

або подібні варіанти.

Хоча це також не визначена поведінка, як уже було зазначено, є незначні відмінності, коли printf()це стосується при порівнянні з твердженням, наприклад:

x = i++ + i++;

У наступному твердженні:

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

порядок оцінки аргументів на printf()це не визначене . Це означає, що вирази i++і ++iможуть бути оцінені в будь-якому порядку. Стандарт C11 містить деякі відповідні описи щодо цього:

Додаток J, неуточнена поведінка

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

3.4.4, неуточнена поведінка

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

ПРИКЛАД Прикладом не визначеної поведінки є порядок, в якому оцінюються аргументи функції.

Сама не визначена поведінка НЕ є проблемою. Розглянемо цей приклад:

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

Це також має не визначене поведінку, оскільки порядок оцінки ++xта y++не визначений. Але це абсолютно законна та дійсна заява. У цій заяві немає невизначеної поведінки. Тому що модифікації ( ++xі y++) робляться для розрізнення об'єктів.

Що дає наступне твердження

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

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


Ще одна деталь полягає в тому, що кома, що бере участь у виклику printf (), - це роздільник , а не оператор коми .

Це важлива відмінність, оскільки оператор комами вводить точку послідовності між оцінкою своїх операндів, що робить таке законним:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Оператор комами оцінює свої операнди зліва направо і отримує лише значення останнього операнда. Отже j = (++i, i++);, ++iприріст iдо 6та i++дає старе значення i( 6), яке присвоюється j. Потім iстає 7за рахунок інкрементації.

Отже, якщо кома у виклику функції повинна була бути оператором комами

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

не буде проблемою. Але це викликає невизначене поведінку, оскільки кома тут є роздільником .


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

Цей пост: Не визначена, не визначена та визначена реалізацією поведінка також є актуальною.


int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));Здається, що ця послідовність дає стабільну поведінку (оцінка аргументів справа наліво в gcc v7.3.0; результат "a = 110 b = 40 c = 60"). Це тому, що призначення розглядаються як "повні заяви" і таким чином вводять точку послідовності? Чи не може це спричинити оцінку аргументу / твердження зліва направо? Або це просто прояв невизначеної поведінки?
кавадіас

@kavadias Цей твердження printf передбачає невизначене поведінку, з тієї ж причини, поясненої вище. Ви пишете bі cв третьому, і в четвертому аргументах відповідно і читаєте в другому аргументі. Але між цими виразами немає послідовностей (2-й, 3-й та 4-й аргументи). gcc / clang має варіант, -Wsequence-pointякий також може допомогти знайти їх.
ПП

23

Хоча навряд чи будь-які компілятори та процесори насправді роблять це, було б законно, згідно стандарту C, компілятор реалізувати "i ++" з послідовністю:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

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

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


16

Хоча синтаксис виразів подобається a = a++або a++ + a++є законним, то поведінка цих конструкцій є невизначеним , оскільки повинен в стандарті C не виконується. C99 6.5p2 :

  1. Між попередньою і наступною точкою послідовності об'єкт повинен змінити його збережене значення щонайбільше одночасно оцінкою виразу. [72] Крім того, попереднє значення зчитується лише для визначення значення, яке потрібно зберігати [73]

З виноска 73 додатково уточнити , що

  1. Цей абзац надає невизначені вирази висловлювань, такі як

    i = ++i + 1;
    a[i++] = i;

    дозволяючи

    i = i + 1;
    a[i] = i;

У Додатку С C11 (та C99 ) наведено різні точки послідовності :

  1. Нижче наведено точки послідовності, описані в 5.1.2.3:

    • Між оцінками позначення функції та фактичними аргументами у виклику функції та фактичним викликом. (6.5.2.2).
    • Між оцінками першого та другого операндів наступних операторів: логічний AND && (6.5.13); логічний АБО || (6.5.14); кома, (6.5.17).
    • Між оцінками першого операнда умовного? : оцінюється оператор та залежно від другого та третього операндів (6.5.15).
    • Кінець повного декларатора: декларатори (6.7.6);
    • Між оцінкою повного вираження та наступним повним виразом, що підлягає оцінці. Наведені нижче повні вирази: ініціалізатор, який не є частиною складеного літералу (6.7.9); вираз у виразі виразів (6.8.3); керуючий вираз оператора вибору (якщо або перемикається) (6.8.4); керуючий вираз оператора time або do (6.8.5); кожен із (необов'язкових) виразів для оператора (6.8.5.3); (необов'язковий) вираз у зворотному операторі (6.8.6.4).
    • Безпосередньо до повернення функції бібліотеки (7.1.4).
    • Після дій, пов'язаних з кожним відформатованим специфікатором перетворення функції введення / виводу (7.21.6, 7.29.2).
    • Безпосередньо перед і безпосередньо після кожного виклику функції порівняння, а також між будь-яким викликом до функції порівняння та будь-яким рухом об'єктів, переданим як аргументи до цього виклику (7.22.5).

Формулювання того ж абзацу в C11 :

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

Ви можете виявити такі помилки в програмі, наприклад, скориставшись останньою версією GCC з -Wallі -Werror, і тоді GCC відмовиться від компіляції вашої програми. Далі йде вихід gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

Важлива частина - знати, що таке точка послідовності - а що таке послідовність, а що ні . Наприклад, оператор комами є точкою послідовності, так

j = (i ++, ++ i);

є чітко визначеним і збільшиться iна одиницю, даючи старе значення, відкиньте це значення; потім у оператора кома вирішіть побічні ефекти; а потім приріст iна одиницю, і отримане значення стає значенням виразу - тобто це лише надуманий спосіб написання, j = (i += 2)який знову є "розумним" способом написання

i += 2;
j = i;

Однак ,списки аргументів функцій не є оператором комами, і між оцінками окремих аргументів немає точки послідовності; натомість їх оцінки не впливають один на одного; тому виклик функції

int i = 0;
printf("%d %d\n", i++, ++i, i);

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


14

Стандарт C говорить, що змінна повинна бути призначена не більше одного разу між двома точками послідовності. Наприклад, крапка з комою є точкою послідовності.
Отже, кожен вислів форми:

i = i++;
i = i++ + ++i;

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

Однак дві різні змінні можуть бути збільшені між двома точками послідовності.

while(*src++ = *dst++);

Вищенаведене є загальною практикою кодування під час копіювання / аналізу рядків.


Звичайно, це не стосується різних змінних в одному виразі. Це було б цілковитою невдачею дизайну, якби це було! Все, що вам потрібно у другому прикладі, - це як збільшувати обидва значення між закінченням оператора та наступним початком, і це гарантовано саме через концепцію точок послідовності в центрі всього цього.
підкреслюй_d

11

У /programming/29505280/incrementing-array-index-in-c хтось запитав про таке твердження, як:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

яка друкує 7 ... ОП очікувала, що вона надрукує 6.

Ці ++iзбільшення не гарантується всіх завершена до інших розрахунків. Насправді різні компілятори тут отримають різні результати. У прикладі , який ви вказали, перший 2 ++iвиконується, то значення k[]були прочитані, то останній ++iтоді k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Сучасні компілятори дуже добре оптимізують це. Насправді, можливо, краще, ніж код, який ви написали спочатку (якщо припустити, що він працював так, як ви сподівалися).


5

Хороше пояснення того, що відбувається в цьому виді обчислень, наведено в документі n1188 з сайту ISO W14 .

Я пояснюю ідеї.

Основне правило від стандарту ISO 9899, ​​який застосовується в цій ситуації, - це 6.5p2.

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

Точки послідовності в виразі, подібні i=i++до i=і після, і післяi++ .

У роботі, яку я цитував вище, пояснюється, що ви можете визначити, що програма формується невеликими полями, кожне поле яких містить інструкції між двома послідовними пунктами послідовності. Точки послідовності визначені в додатку C стандарту, у випадку, i=i++якщо є 2 точки послідовності, що обмежують повний вираз. Такий вираз синтаксично еквівалентний запису expression-statementграматики у формі Бекуса-Наура (граматика надана у додатку А до стандарту).

Тож порядок інструкцій всередині коробки не має чіткого порядку.

i=i++

можна інтерпретувати як

tmp = i
i=i+1
i = tmp

або як

tmp = i
i = tmp
i=i+1

оскільки обидві всі ці форми для інтерпретації коду i=i++є дійсними і тому, що обидва генерують різні відповіді, поведінка не визначена.

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

Редагувати:

Іншим хорошим джерелом для пояснення таких неясностей є записи із сайту c-faq (також опубліковані як книга ), а саме тут і тут і тут .


Як ця відповідь додала нових до існуючих відповідей? Також пояснення щодо i=i++дуже схожі на цю відповідь .
хакі

@haccks Я не читав інших відповідей. Я хотів пояснити рідною мовою, що я дізнався із згаданого документа на офіційному сайті ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

Напевно, ваше запитання не було: "Чому ці конструкції не визначені в C поведінці?". Можливо, ваше запитання: "Чому цей код (використовуючи ++) не дав мені очікуваного значення?", І хтось позначив ваше запитання як дублікат, і надіслав вас сюди.

Ця відповідь намагається відповісти на це запитання: чому ваш код не дав вам відповіді, яку ви очікували, і як можна навчитися розпізнавати (і уникати) вирази, які не працюватимуть, як очікувалося.

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

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

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

Або, можливо, ви дивитесь на важко зрозумілий вираз, як

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

Можливо, хтось подарував вам цей код як головоломку. Цей код також не має сенсу, особливо якщо ви його запускаєте - і якщо ви компілюєте та запускаєте його під двома різними компіляторами, ви, ймовірно, отримаєте дві різні відповіді! Що з цим? Яка відповідь правильна? (І відповідь полягає в тому, що вони обоє є, або жодна з них не є.)

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

Що робить вираз невизначеним? Чи є виразами, що стосуються ++та-- завжди не визначені? Звичайно, ні: це корисні оператори, і якщо ви правильно їх використовуєте, вони чудово визначені.

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

Повернемось до двох прикладів, які я використав у цій відповіді. Коли я писав

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

перед тим, як викликати printf, компілятор обчислює значення xпершого, або x++, чи, може ++x,? Але виявляється, ми не знаємо . Не існує жодного правила, яке говорить про те, що аргументи функції оцінюються зліва направо, або справа наліво, або в іншому порядку. Тому ми не можемо сказати , чи буде компілятор зробити xперший, потім ++x, потім x++, або x++потім ++xпотім x, або який -небудь інший порядок. Але порядок чітко має значення, оскільки залежно від того, який порядок використовує компілятор, ми будемо чітко отримувати різні результати, надруковані printf.

Що з цим шаленим виразом?

x = x++ + ++x;

Проблема з цим виразом полягає в тому, що він містить три різні спроби змінити значення x: (1) x++частина намагається додати 1 до x, зберегти нове значення в xі повернути старе значення x; (2) ++xчастина намагається додати 1 до x, зберегти нове значення у xта повернути нове значення x; і (3) x =частина намагається призначити суму двох інших назад х. Яке із цих трьох спроб "виграти"? Якому з трьох значень насправді буде призначено x? Знову, і, мабуть, дивно, в C немає правила сказати нам.

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


Тож з усім цим фоном і вступом не виходить, якщо ви хочете переконатися, що всі ваші програми чітко визначені, які вирази ви можете написати, а які ви не можете написати?

Ці вирази все добре:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Усі ці вирази не визначені:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

І останнє питання - як ви можете сказати, які вирази чітко визначені, а які вирази не визначені?

Як я вже говорив раніше, невизначені вирази - це ті, де відбувається занадто багато одразу, де ви не можете бути впевнені в тому, в якому порядку відбуваються речі, і де має значення порядок:

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

Як приклад №1 у виразі

x = x++ + ++x;

є три спроби змінити `x.

Як приклад №2, у виразі

y = x + x++;

ми обидва використовуємо значення xта змінюємо його.

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


3

Причина в тому, що в програмі працює невизначена поведінка. Проблема полягає в порядку оцінювання, оскільки немає необхідних точок послідовності відповідно до стандарту C ++ 98 (жодна операція не секвенсована до або після іншої відповідно до термінології C ++ 11).

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

  • Тож спочатку GCC: Використовуючи Nuwen MinGW 15 GCC 7.1, ви отримаєте:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Як працює GCC? він оцінює допоміжні вирази в порядку зліва направо для правої частини (RHS), а потім присвоює значення лівій частині (LHS). Саме так поводяться Java та C # і визначають свої стандарти. (Так, еквівалентне програмне забезпечення в Java та C # визначає поведінку). Він оцінює кожен підрядний вираз по одному в Заяві RHS в порядку зліва направо; для кожного підвиразу: спочатку оцінюється ++ c (попередній приріст), потім для операції використовується значення c, потім збільшення кроку c ++).

відповідно до GCC C ++: Оператори

У GCC C ++ пріоритет операторів контролює порядок оцінки окремих операторів

еквівалентний код у визначеній поведінці C ++, як розуміє GCC:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Потім переходимо до Visual Studio . Visual Studio 2015 ви отримуєте:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

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

Отже, еквівалент у визначеній поведінці C ++, як розуміє Visual C ++:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

як зазначено в документації Visual Studio на пріоритеті та порядку оцінювання :

Якщо кілька операторів з'являються разом, вони мають однаковий пріоритет і оцінюються відповідно до їх асоціативності. Оператори в таблиці описані в розділах, що починаються з Операторів Postfix.


1
Я редагував питання, щоб додати UB в оцінці аргументів функції, оскільки це питання часто використовується як дублікат для цього. (Останній приклад)
Антті Хаапала

1
Також питання про c зараз, а не C ++
Antti Haapala
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.