เหตุใดโครงสร้างเหล่านี้จึงใช้พฤติกรรมที่ไม่ได้กำหนดไว้ก่อนและหลังการเพิ่มขึ้น

Jun 04 2009
#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?
}

คำตอบ

573 unwind Jun 04 2009 at 16:20

C มีแนวคิดเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดกล่าวคือโครงสร้างภาษาบางส่วนมีความถูกต้องทางวากยสัมพันธ์ แต่คุณไม่สามารถคาดเดาพฤติกรรมได้เมื่อรันโค้ด

เท่าที่ฉันทราบมาตรฐานไม่ได้บอกอย่างชัดเจนว่าเหตุใดจึงมีแนวคิดเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดไว้อย่างชัดเจน ในความคิดของฉันมันเป็นเพียงเพราะนักออกแบบภาษาต้องการให้มีความว่างในความหมายแทนที่จะต้องการให้การใช้งานทั้งหมดจัดการกับจำนวนเต็มล้นในลักษณะเดียวกันซึ่งอาจทำให้เสียค่าใช้จ่ายด้านประสิทธิภาพอย่างมากพวกเขาก็ออกจากพฤติกรรม ไม่ได้กำหนดไว้ดังนั้นหากคุณเขียนโค้ดที่ทำให้จำนวนเต็มล้นจะมีอะไรเกิดขึ้นได้

เหตุใดจึงมี "ประเด็น" เหล่านี้ ภาษาพูดอย่างชัดเจนว่าบางสิ่งบางอย่างนำไปสู่พฤติกรรมที่ไม่ได้กำหนด ไม่มีปัญหาไม่มี "ควร" เข้ามาเกี่ยวข้อง หากพฤติกรรมที่ไม่ได้กำหนดเปลี่ยนไปเมื่อมีการประกาศตัวแปรที่เกี่ยวข้องสิ่งvolatileนั้นจะไม่พิสูจน์หรือเปลี่ยนแปลงอะไร มันไม่ได้กำหนด ; คุณไม่สามารถให้เหตุผลเกี่ยวกับพฤติกรรมได้

ตัวอย่างที่น่าสนใจที่สุดของคุณตัวอย่างที่มี

u = (u++);

เป็นตัวอย่างหนังสือข้อความเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด (ดูรายการของ Wikipedia เกี่ยวกับจุดลำดับ )

76 badp May 24 2010 at 20:26

เพียงรวบรวมและแยกบรรทัดรหัสของคุณหากคุณมีแนวโน้มที่จะรู้ว่ามันเป็นอย่างไรคุณจะได้รับสิ่งที่คุณได้รับ

นี่คือสิ่งที่ฉันได้รับจากเครื่องของฉันพร้อมกับสิ่งที่ฉันคิดว่าเกิดขึ้น:

$ 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 เป็นการเพิ่มประสิทธิภาพคอมไพเลอร์บางประเภท?)

66 Christoph Jun 04 2009 at 16:35

ฉันคิดว่าส่วนที่เกี่ยวข้องของมาตรฐาน C99 คือ 6.5 นิพจน์§2

ระหว่างจุดลำดับก่อนหน้าและลำดับถัดไปอ็อบเจ็กต์จะต้องมีการแก้ไขค่าที่เก็บไว้มากที่สุดครั้งเดียวโดยการประเมินนิพจน์ นอกจากนี้ค่าก่อนหน้าจะถูกอ่านเพื่อกำหนดค่าที่จะจัดเก็บเท่านั้น

และ 6.5.16 ตัวดำเนินการมอบหมาย§4:

ไม่ได้ระบุลำดับของการประเมินตัวถูกดำเนินการ หากมีการพยายามแก้ไขผลลัพธ์ของตัวดำเนินการกำหนดหรือเพื่อเข้าถึงหลังจากจุดลำดับถัดไปพฤติกรรมนั้นจะไม่ได้กำหนดไว้

61 haccks Jun 27 2015 at 07:27

คำตอบส่วนใหญ่ยกมาจากมาตรฐาน C โดยเน้นว่าพฤติกรรมของโครงสร้างเหล่านี้ไม่ได้กำหนดไว้ เพื่อให้เข้าใจว่าเหตุใดจึงไม่ได้กำหนดพฤติกรรมของโครงสร้างเหล่านี้มาทำความเข้าใจกับคำเหล่านี้ก่อนตามมาตรฐาน C11:

ลำดับ: (5.1.2.3)

ได้รับการประเมินผลที่สองAและBถ้าAเป็นลำดับขั้นตอนก่อนBแล้วการดำเนินการจะนำหน้าการดำเนินการของAB

ไม่ได้ตามมา:

ถ้าAไม่ได้ติดใจก่อนหรือหลังBแล้วAและBมี unsequenced

การประเมินอาจเป็นหนึ่งในสองสิ่ง:

  • การคำนวณค่าซึ่งหาผลลัพธ์ของนิพจน์ และ
  • ผลข้างเคียงซึ่งเป็นการดัดแปลงวัตถุ

จุดลำดับ:

การปรากฏตัวของจุดลำดับระหว่างการประเมินผลของการแสดงออกAและBแสดงให้เห็นว่าทุกการคำนวณมูลค่าและผลข้างเคียงที่เกี่ยวข้องกับการAเป็นลำดับขั้นตอนทุกครั้งก่อนการคำนวณมูลค่าและผลข้างเคียงBที่เกี่ยวข้องกับ

ตอนนี้มาถึงคำถามสำหรับนิพจน์เช่น

int i = 1;
i = i++;

มาตรฐานบอกว่า:

6.5 นิพจน์:

หากเป็นผลข้างเคียงบนวัตถุเกลาเป็นญาติ unsequenced การอย่างใดอย่างหนึ่งเป็นผลข้างเคียงที่แตกต่างกันบนวัตถุเกลาเดียวกันหรือการคำนวณค่าใช้ค่าของวัตถุเกลาเดียวกันพฤติกรรมจะไม่ได้กำหนด [... ]

ดังนั้นนิพจน์ข้างต้นจึงเรียกใช้ UB เนื่องจากผลข้างเคียงสองอย่างในวัตถุเดียวกันไม่ได้รับผลกระทบที่iสัมพันธ์กัน นั่นหมายความว่ามันไม่ได้ติดใจว่าผลข้างเคียงโดยมอบหมายให้จะทำก่อนหรือหลังผลข้างเคียงโดยi ขึ้นอยู่กับว่าได้รับมอบหมายเกิดขึ้นก่อนหรือหลังเพิ่มขึ้นผลที่แตกต่างกันจะมีการผลิตและนั่นคือหนึ่งในกรณีที่ไม่ได้กำหนดพฤติกรรม++

ให้เปลี่ยนชื่อiที่ด้านซ้ายของการมอบหมายเป็นilและที่ด้านขวาของการมอบหมาย (ในนิพจน์i++) เป็น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

54 ShafikYaghmour Aug 16 2013 at 02:25

ไม่สามารถอธิบายพฤติกรรมได้จริง ๆ เพราะมันเรียกใช้ทั้งพฤติกรรมที่ไม่ระบุและพฤติกรรมที่ไม่ได้กำหนดดังนั้นเราจึงไม่สามารถคาดเดาทั่วไปเกี่ยวกับรหัสนี้ได้แม้ว่าคุณจะอ่านงานของ Olve Maudalเช่นDeep CและUnspecified และ Undefinedบางครั้งคุณก็สามารถทำให้ดีได้ เดาในกรณีที่เฉพาะเจาะจงมากกับคอมไพเลอร์และสภาพแวดล้อมที่เฉพาะเจาะจง แต่โปรดอย่าทำเช่นนั้นที่ใดก็ได้ใกล้กับการผลิต

ดังนั้นจะย้ายไปยังพฤติกรรมที่ไม่ระบุในร่าง c99 มาตรฐานส่วน6.5วรรค3กล่าวว่า ( เหมืองเน้น ):

การจัดกลุ่มตัวดำเนินการและตัวถูกดำเนินการถูกระบุโดยไวยากรณ์ 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:

พฤติกรรมเมื่อใช้โครงสร้างโปรแกรมที่ไม่สามารถพกพาได้หรือผิดพลาดหรือข้อมูลที่ผิดพลาดซึ่งมาตรฐานสากลนี้ไม่ได้กำหนดข้อกำหนด

และบันทึกว่า:

พฤติกรรมที่ไม่ได้กำหนดที่เป็นไปได้มีตั้งแต่การเพิกเฉยต่อสถานการณ์โดยสิ้นเชิงด้วยผลลัพธ์ที่คาดเดาไม่ได้ไปจนถึงการทำงานระหว่างการแปลหรือการดำเนินการโปรแกรมในลักษณะที่เป็นเอกสารของสภาพแวดล้อม (มีหรือไม่มีการออกข้อความวินิจฉัย) ไปจนถึงการยุติการแปลหรือการดำเนินการ (ด้วยการออก ของข้อความวินิจฉัย)

38 SteveSummit Jun 18 2015 at 18:55

อีกวิธีหนึ่งในการตอบคำถามนี้แทนที่จะจมอยู่กับรายละเอียดที่ลึกลับของจุดลำดับและพฤติกรรมที่ไม่ได้กำหนดคือถามว่าพวกเขาควรจะหมายถึงอะไร? โปรแกรมเมอร์กำลังพยายามทำอะไรอยู่?

ส่วนแรกที่ถามเกี่ยวกับi = i++ + ++iหนังสือของฉันค่อนข้างบ้าอย่างเห็นได้ชัด ไม่มีใครเคยเขียนมันในโปรแกรมจริงไม่ชัดเจนว่ามันทำอะไรไม่มีอัลกอริทึมที่เป็นไปได้ที่ใครบางคนอาจพยายามเขียนโค้ดซึ่งจะส่งผลให้เกิดลำดับการดำเนินการที่สร้างขึ้นโดยเฉพาะนี้ และเนื่องจากมันไม่ชัดเจนสำหรับคุณและฉันว่ามันควรจะทำอะไรมันก็ดีในหนังสือของฉันถ้าคอมไพเลอร์ไม่สามารถคิดได้ว่ามันควรจะทำอะไรเช่นกัน

ส่วนที่สองi = i++เข้าใจง่ายกว่าเล็กน้อย เห็นได้ชัดว่ามีคนพยายามเพิ่ม i และกำหนดผลลัพธ์กลับไปที่ i แต่มีสองวิธีในการทำเช่นนี้ใน C วิธีพื้นฐานที่สุดในการเพิ่ม 1 เป็น i และกำหนดผลลัพธ์กลับเป็น i จะเหมือนกันในภาษาโปรแกรมเกือบทุกภาษา:

i = i + 1

แน่นอน C มีทางลัดที่สะดวก:

i++

ซึ่งหมายความว่า "เพิ่ม 1 ใน i และกำหนดผลลัพธ์กลับเป็น i" ดังนั้นถ้าเราสร้าง hodgepodge ของทั้งสองโดยการเขียน

i = i++

สิ่งที่เราพูดจริงๆคือ "เพิ่ม 1 ใน i และกำหนดผลลัพธ์กลับไปที่ i และกำหนดผลลัพธ์กลับเป็น i" เราสับสนดังนั้นมันจึงไม่รบกวนฉันมากเกินไปหากคอมไพเลอร์สับสนเช่นกัน

ตามความเป็นจริงครั้งเดียวที่สำนวนบ้าๆเหล่านี้ถูกเขียนขึ้นคือเมื่อผู้คนใช้มันเป็นตัวอย่างเทียมว่า ++ ควรจะทำงานอย่างไร และแน่นอนว่าสิ่งสำคัญคือต้องเข้าใจว่า ++ ทำงานอย่างไร แต่กฎที่ใช้ได้จริงข้อหนึ่งสำหรับการใช้ ++ คือ "หากไม่ชัดเจนว่านิพจน์ที่ใช้ ++ หมายถึงอะไรอย่าเขียน"

เราเคยใช้เวลานับไม่ถ้วนใน comp.lang.c เพื่อพูดคุยเกี่ยวกับนิพจน์เช่นนี้และทำไมจึงไม่ได้กำหนด คำตอบที่ยาวกว่าของฉันสองคำซึ่งพยายามอธิบายว่าทำไมจึงถูกเก็บถาวรบนเว็บ:

  • เหตุใด Standard จึงไม่กำหนดว่าสิ่งเหล่านี้ทำหน้าที่อะไร?
  • ลำดับความสำคัญของตัวดำเนินการไม่ได้กำหนดลำดับของการประเมินหรือไม่?

ดูเพิ่มเติมคำถาม 3.8และส่วนที่เหลือของคำถามในมาตรา 3ของC คำถามที่พบบ่อยรายการ

27 P.P Dec 31 2015 at 03:26

บ่อยครั้งที่คำถามนี้ถูกเชื่อมโยงเป็นคำถามที่ซ้ำกันเกี่ยวกับรหัสเช่น

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 ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดเพื่อทำความเข้าใจแนวคิดและพฤติกรรมที่ไม่ได้กำหนดอื่น ๆ อีกมากมายใน C

โพสต์นี้: พฤติกรรมที่ไม่ได้กำหนดไม่ระบุและการกำหนดการนำไปใช้ก็มีความเกี่ยวข้องเช่นกัน

23 supercat Dec 06 2012 at 01:30

แม้ว่าจะไม่น่าเป็นไปได้ที่คอมไพเลอร์และตัวประมวลผลใด ๆ จะทำเช่นนั้นได้ แต่ก็เป็นเรื่องถูกกฎหมายภายใต้มาตรฐาน 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สองครั้ง) คอมไพเลอร์จะไม่ต้องรับรู้หรือหลีกเลี่ยงการหยุดชะงักที่จะเกิดขึ้นหากอยู่วัตถุเดียวกันก็ผ่านไปทั้งสองและpq

18 AnttiHaapala Mar 26 2017 at 21:58

ในขณะที่ไวยากรณ์ของนิพจน์เหมือน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;
    

จุดลำดับต่างๆแสดงอยู่ในภาคผนวก C ของC11 (และC99 ):

  1. ต่อไปนี้เป็นจุดลำดับที่อธิบายไว้ใน 5.1.2.3:

    • ระหว่างการประเมินของตัวกำหนดฟังก์ชันและอาร์กิวเมนต์จริงในการเรียกใช้ฟังก์ชันและการเรียกใช้จริง (6.5.2.2)
    • ระหว่างการประเมินตัวถูกดำเนินการตัวแรกและตัวที่สองของตัวดำเนินการต่อไปนี้: ตรรกะ AND && (6.5.13); ตรรกะหรือ || (6.5.14); ลูกน้ำ, (6.5.17)
    • ระหว่างการประเมินตัวถูกดำเนินการแรกของเงื่อนไข? : ตัวดำเนินการและตัวถูกดำเนินการที่สองและสามจะได้รับการประเมิน (6.5.15)
    • The end of a full declarator: declarators (6.7.6);
    • Between the evaluation of a full expression and the next full expression to be evaluated. The following are full expressions: an initializer that is not part of a compound literal (6.7.9); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the (optional) expressions of a for statement (6.8.5.3); the (optional) expression in a return statement (6.8.6.4).
    • Immediately before a library function returns (7.1.4).
    • After the actions associated with each formatted input/output function conversion specifier (7.21.6, 7.29.2).
    • Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.22.5).

The wording of the same paragraph in C11 is:

  1. If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. If there are multiple allowable orderings of the subexpressions of an expression, the behavior is undefined if such an unsequenced side effect occurs in any of the orderings.84)

You can detect such errors in a program by for example using a recent version of GCC with -Wall and -Werror, and then GCC will outright refuse to compile your program. The following is the output of 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

The important part is to know what a sequence point is -- and what is a sequence point and what isn't. For example the comma operator is a sequence point, so

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

is well-defined, and will increment i by one, yielding the old value, discard that value; then at comma operator, settle the side effects; and then increment i by one, and the resulting value becomes the value of the expression - i.e. this is just a contrived way to write j = (i += 2) which is yet again a "clever" way to write

i += 2;
j = i;

However, the , in function argument lists is not a comma operator, and there is no sequence point between evaluations of distinct arguments; instead their evaluations are unsequenced with regard to each other; so the function call

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

has undefined behaviour because there is no sequence point between the evaluations of i++ and ++i in function arguments, and the value of i is therefore modified twice, by both i++ and ++i, between the previous and the next sequence point.

14 NikhilVidhani Sep 11 2014 at 19:36

The C standard says that a variable should only be assigned at most once between two sequence points. A semi-colon for instance is a sequence point.
So every statement of the form:

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

and so on violate that rule. The standard also says that behavior is undefined and not unspecified. Some compilers do detect these and produce some result but this is not per standard.

However, two different variables can be incremented between two sequence points.

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

The above is a common coding practice while copying/analysing strings.

11 TomOnTime Apr 08 2015 at 10:20

In https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c someone asked about a statement like:

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);

which prints 7... the OP expected it to print 6.

The ++i increments aren't guaranteed to all complete before the rest of the calculations. In fact, different compilers will get different results here. In the example you provided, the first 2 ++i executed, then the values of k[] were read, then the last ++i then k[].

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

Modern compilers will optimize this very well. In fact, possibly better than the code you originally wrote (assuming it had worked the way you had hoped).

6 SteveSummit Aug 16 2018 at 18:54

Your question was probably not, "Why are these constructs undefined behavior in C?". Your question was probably, "Why did this code (using ++) not give me the value I expected?", and someone marked your question as a duplicate, and sent you here.

This answer tries to answer that question: why did your code not give you the answer you expected, and how can you learn to recognize (and avoid) expressions that will not work as expected.

I assume you've heard the basic definition of C's ++ and -- operators by now, and how the prefix form ++x differs from the postfix form x++. But these operators are hard to think about, so to make sure you understood, perhaps you wrote a tiny little test program involving something like

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

But, to your surprise, this program did not help you understand -- it printed some strange, unexpected, inexplicable output, suggesting that maybe ++ does something completely different, not at all what you thought it did.

Or, perhaps you're looking at a hard-to-understand expression like

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

Perhaps someone gave you that code as a puzzle. This code also makes no sense, especially if you run it -- and if you compile and run it under two different compilers, you're likely to get two different answers! What's up with that? Which answer is correct? (And the answer is that both of them are, or neither of them are.)

As you've heard by now, all of these expressions are undefined, which means that the C language makes no guarantee about what they'll do. This is a strange and surprising result, because you probably thought that any program you could write, as long as it compiled and ran, would generate a unique, well-defined output. But in the case of undefined behavior, that's not so.

What makes an expression undefined? Are expressions involving ++ and -- always undefined? Of course not: these are useful operators, and if you use them properly, they're perfectly well-defined.

For the expressions we're talking about what makes them undefined is when there's too much going on at once, when we're not sure what order things will happen in, but when the order matters to the result we get.

Let's go back to the two examples I've used in this answer. When I wrote

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

the question is, before calling printf, does the compiler compute the value of x first, or x++, or maybe ++x? But it turns out we don't know. There's no rule in C which says that the arguments to a function get evaluated left-to-right, or right-to-left, or in some other order. So we can't say whether the compiler will do x first, then ++x, then x++, or x++ then ++x then x, or some other order. But the order clearly matters, because depending on which order the compiler uses, we'll clearly get different results printed by printf.

What about this crazy expression?

x = x++ + ++x;

The problem with this expression is that it contains three different attempts to modify the value of x: (1) the x++ part tries to add 1 to x, store the new value in x, and return the old value of x; (2) the ++x part tries to add 1 to x, store the new value in x, and return the new value of x; and (3) the x = part tries to assign the sum of the other two back to x. Which of those three attempted assignments will "win"? Which of the three values will actually get assigned to x? Again, and perhaps surprisingly, there's no rule in C to tell us.

You might imagine that precedence or associativity or left-to-right evaluation tells you what order things happen in, but they do not. You may not believe me, but please take my word for it, and I'll say it again: precedence and associativity do not determine every aspect of the evaluation order of an expression in C. In particular, if within one expression there are multiple different spots where we try to assign a new value to something like x, precedence and associativity do not tell us which of those attempts happens first, or last, or anything.


So with all that background and introduction out of the way, if you want to make sure that all your programs are well-defined, which expressions can you write, and which ones can you not write?

These expressions are all fine:

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++;

These expressions are all undefined:

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

And the last question is, how can you tell which expressions are well-defined, and which expressions are undefined?

As I said earlier, the undefined expressions are the ones where there's too much going at once, where you can't be sure what order things happen in, and where the order matters:

  1. If there's one variable that's getting modified (assigned to) in two or more different places, how do you know which modification happens first?
  2. If there's a variable that's getting modified in one place, and having its value used in another place, how do you know whether it uses the old value or the new value?

As an example of #1, in the expression

x = x++ + ++x;

there are three attempts to modify `x.

As an example of #2, in the expression

y = x + x++;

we both use the value of x, and modify it.

So that's the answer: make sure that in any expression you write, each variable is modified at most once, and if a variable is modified, you don't also attempt to use the value of that variable somewhere else.

5 alinsoar Oct 13 2017 at 20:58

A good explanation about what happens in this kind of computation is provided in the document n1188 from the ISO W14 site.

I explain the ideas.

The main rule from the standard ISO 9899 that applies in this situation is 6.5p2.

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.

The sequence points in an expression like i=i++ are before i= and after i++.

In the paper that I quoted above it is explained that you can figure out the program as being formed by small boxes, each box containing the instructions between 2 consecutive sequence points. The sequence points are defined in annex C of the standard, in the case of i=i++ there are 2 sequence points that delimit a full-expression. Such an expression is syntactically equivalent with an entry of expression-statement in the Backus-Naur form of the grammar (a grammar is provided in annex A of the Standard).

So the order of instructions inside a box has no clear order.

i=i++

can be interpreted as

tmp = i
i=i+1
i = tmp

or as

tmp = i
i = tmp
i=i+1

because both all these forms to interpret the code i=i++ are valid and because both generate different answers, the behavior is undefined.

So a sequence point can be seen by the beginning and the end of each box that composes the program [the boxes are atomic units in C] and inside a box the order of instructions is not defined in all cases. Changing that order one can change the result sometimes.

EDIT:

Other good source for explaining such ambiguities are the entries from c-faq site (also published as a book) , namely here and here and here .

3 MohamedEl-Nakib Jun 11 2017 at 05:56

The reason is that the program is running undefined behavior. The problem lies in the evaluation order, because there is no sequence points required according to C++98 standard ( no operations is sequenced before or after another according to C++11 terminology).

However if you stick to one compiler, you will find the behavior persistent, as long as you don't add function calls or pointers, which would make the behavior more messy.

  • So first the GCC: Using Nuwen MinGW 15 GCC 7.1 you will get:

    #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
    

    }

How does GCC work? it evaluates sub expressions at a left to right order for the right hand side (RHS) , then assigns the value to the left hand side (LHS) . This is exactly how Java and C# behave and define their standards. (Yes, the equivalent software in Java and C# has defined behaviors). It evaluate each sub expression one by one in the RHS Statement in a left to right order; for each sub expression: the ++c (pre-increment) is evaluated first then the value c is used for the operation, then the post increment c++).

according to GCC C++: Operators

In GCC C++, the precedence of the operators controls the order in which the individual operators are evaluated

the equivalent code in defined behavior C++ as GCC understands:

#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
}

Then we go to Visual Studio. Visual Studio 2015, you get:

#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 
}

How does visual studio work, it takes another approach, it evaluates all pre-increments expressions in first pass, then uses variables values in the operations in second pass, assign from RHS to LHS in third pass, then at last pass it evaluates all the post-increment expressions in one pass.

So the equivalent in defined behavior C++ as Visual C++ understands:

#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 
}

as Visual Studio documentation states at Precedence and Order of Evaluation:

Where several operators appear together, they have equal precedence and are evaluated according to their associativity. The operators in the table are described in the sections beginning with Postfix Operators.