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


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 ไม่ต้องการเพียงแค่พอยน์เตอร์เพื่อ "ชี้คะแนน" ขณะทำงานฉันพบรหัสด้วย i = i ++ ฉันคิดว่า "นี่ไม่ได้แก้ไขค่าของ i" ฉันทดสอบและฉันสงสัยว่าทำไม ตั้งแต่ฉันลบสถานะนี้และแทนที่ด้วย i ++;
PiX

198
ฉันคิดว่ามันน่าสนใจที่ทุกคนสันนิษฐานว่าคำถามเช่นนี้ถูกถามเสมอเพราะผู้ถามต้องการใช้โครงสร้างที่เป็นปัญหา ข้อสันนิษฐานแรกของฉันคือ PiX รู้ว่าสิ่งเหล่านี้ไม่ดี แต่สงสัยว่าทำไมพฤติกรรมของพวกเขาในการใช้คอมไพเลอร์ whataver / เขาใช้ ... และใช่สิ่งที่ไม่ได้พูดไว้ ... มันไม่ได้กำหนด .. รวมถึง JCF (Jump and Catch Fire)
Brian Postow

32
ฉันอยากรู้: ทำไมคอมไพเลอร์ดูเหมือนจะไม่เตือนในโครงสร้างเช่น "u = u ++ + ++ u;" ถ้าผลลัพธ์ไม่ได้กำหนด?
เรียนรู้ OpenGL ES

5
(i++)ยังคงประเมินเป็น 1 โดยไม่คำนึงถึงวงเล็บ
Drew McGowen

2
สิ่งที่i = (i++);ตั้งใจจะทำมีวิธีชัดเจนในการเขียน นั่นจะเป็นจริงแม้ว่ามันจะถูกนิยามไว้อย่างดีก็ตาม แม้ใน Java ซึ่งกำหนดพฤติกรรมของi = (i++);มันก็ยังคงเป็นรหัสที่ไม่ดี เพียงแค่เขียนi++;
Keith Thompson

คำตอบ:


566

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

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

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

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

u = (u++);

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


8
@PiX: สิ่งต่าง ๆ ไม่ได้ถูกกำหนดด้วยเหตุผลหลายประการ สิ่งเหล่านี้รวมถึง: ไม่มี "ผลลัพธ์ที่ถูกต้อง" อย่างชัดเจนสถาปัตยกรรมเครื่องที่แตกต่างกันจะให้ผลลัพธ์ที่แตกต่างกันอย่างมากการปฏิบัติที่มีอยู่ไม่สอดคล้องกันหรือเกินขอบเขตมาตรฐาน (เช่นชื่อไฟล์ที่ถูกต้อง)
Richard

เพียงเพื่อให้ทุกคนเกิดความสับสนตัวอย่างเช่นตอนนี้ที่ดีที่กำหนดใน C11 i = ++i + 1;เช่น
MM

2
การอ่านมาตรฐานและเหตุผลที่ตีพิมพ์มันชัดเจนว่าทำไมแนวคิดของ UB จึงมีอยู่ มาตรฐานไม่เคยมีความตั้งใจที่จะอธิบายทุกอย่างอย่างสมบูรณ์ในการนำ C ไปใช้งานเพื่อให้เหมาะสมกับวัตถุประสงค์เฉพาะใด ๆ (ดูการอภิปรายของกฎ "หนึ่งโปรแกรม") แต่ให้อาศัยการตัดสินของผู้พัฒนาและความปรารถนาที่จะสร้างคุณภาพที่เป็นประโยชน์ การใช้งานคุณภาพที่เหมาะสมสำหรับการเขียนโปรแกรมระบบระดับต่ำจะต้องกำหนดพฤติกรรมของการกระทำที่ไม่จำเป็นในการตรวจสอบตัวเลขระดับสูง แทนที่จะพยายามที่จะทำให้มาตรฐานซับซ้อนขึ้น ...
supercat

3
... โดยการลงรายละเอียดอย่างละเอียดเกี่ยวกับกรณีมุมหรือไม่ได้กำหนดไว้ผู้เขียนมาตรฐานได้รับการยอมรับว่าผู้ดำเนินการควรจะดีกว่าที่จะตัดสินว่าควรใช้พฤติกรรมประเภทใดในโปรแกรมที่พวกเขาคาดว่าจะสนับสนุน . คอมไพเลอร์ Hyper-modernist แสร้งทำเป็นว่าการกระทำบางอย่าง 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?คำตอบนี้ไม่อยู่จริงๆคำถามของ
Shafik Yaghmour

9
นอกจากนี้มันจะง่ายกว่าในการรวบรวมการชุมนุม (ด้วยgcc -S evil.c) ซึ่งเป็นสิ่งที่จำเป็นที่นี่ การประกอบแล้วการแยกส่วนมันเป็นเพียงวิธีการทำวงเวียน
Kat

50
สำหรับบันทึกถ้าด้วยเหตุผลใดก็ตามที่คุณสงสัยว่าโครงสร้างที่กำหนดทำอะไร - และโดยเฉพาะอย่างยิ่งหากมีข้อสงสัยว่ามันอาจเป็นพฤติกรรมที่ไม่ได้กำหนด - คำแนะนำอายุเก่าของ "แค่ลองดูด้วยคอมไพเลอร์แล้วดู" อาจเป็นอันตรายมาก คุณจะได้เรียนรู้ที่ดีที่สุดสิ่งที่มันไม่อยู่ภายใต้รุ่นของคอมไพเลอร์ของคุณภายใต้สถานการณ์เหล่านี้ในวันนี้ คุณจะไม่เรียนรู้มากนักหากมีอะไรเกี่ยวกับสิ่งที่รับประกันว่าจะทำ โดยทั่วไปแล้ว "ลองกับคอมไพเลอร์ของคุณ" นำไปสู่โปรแกรมที่ไม่สามารถทำงานได้ซึ่งทำงานกับคอมไพเลอร์ของคุณเท่านั้น
Steve Summit

64

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

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

และผู้ประกอบการที่ได้รับมอบหมาย 6.5.16, §4:

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


2
ข้างต้นจะบอกเป็นนัย ๆ ว่า 'i = i = 5; "จะเป็นพฤติกรรมที่ไม่ได้กำหนดหรือไม่
supercat

1
@supercat เท่าที่ฉันรู้i=i=5ก็เป็นพฤติกรรมที่ไม่ได้กำหนด
dhein

2
@Zaibis: เหตุผลที่ฉันต้องการใช้สำหรับกฎส่วนใหญ่สถานที่นำไปใช้ในทางทฤษฎีแล้วแพลตฟอร์ม mutli-processor สามารถใช้บางอย่างเช่นA=B=5;"ล็อคการเขียน A; ล็อคการเขียน B; ร้าน 5 ถึง A; ร้าน 5 ถึง B; ปลดล็อค B ; Unock A; "และคำสั่งเช่นC=A+B;" Read-lock A; Read-lock B; คำนวณ A + B; ปลดล็อค A และ B; Write-lock C; Store store; Unlock C; " นั่นจะช่วยให้มั่นใจได้ว่าหากเธรดหนึ่งเธรดทำA=B=5;ในขณะที่อีกเธรดหนึ่งทำC=A+B;ภายหลังจะเห็นทั้งการเขียนว่าเกิดขึ้นหรือไม่ อาจเป็นการรับประกันที่มีประโยชน์ อย่างไรก็ตามถ้ามีเธรดหนึ่งตัวI=I=5;...
supercat

1
... และคอมไพเลอร์ไม่ได้สังเกตว่าการเขียนทั้งสองนั้นอยู่ในตำแหน่งเดียวกัน (หากหนึ่งหรือทั้งสองค่าเกี่ยวข้องกับพอยน์เตอร์ซึ่งอาจยากต่อการพิจารณา) รหัสที่สร้างขึ้นอาจหยุดชะงักได้ ฉันไม่คิดว่าการใช้งานในโลกแห่งความเป็นจริงจะใช้การล็อกดังกล่าวเป็นส่วนหนึ่งของพฤติกรรมปกติของพวกเขา แต่มันจะได้รับอนุญาตภายใต้มาตรฐานและหากฮาร์ดแวร์สามารถใช้พฤติกรรมดังกล่าวได้ในราคาถูก ในฮาร์ดแวร์ของวันนี้พฤติกรรมดังกล่าวจะแพงเกินไปที่จะใช้เป็นค่าเริ่มต้น แต่นั่นไม่ได้หมายความว่ามันจะเป็นเช่นนั้นเสมอไป
supercat

1
@supercat แต่กฎการเข้าถึงจุดลำดับของ c99 เพียงอย่างเดียวจะไม่เพียงพอที่จะประกาศว่าเป็นพฤติกรรมที่ไม่ได้กำหนดหรือไม่ ดังนั้นมันไม่สำคัญว่าในทางเทคนิคฮาร์ดแวร์สามารถใช้?
dhein

55

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

ลำดับ: (5.1.2.3)

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

Unsequenced:

ถ้า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


52

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

ดังนั้นไปที่พฤติกรรมที่ไม่ได้ระบุไว้ในร่างมาตรามาตรฐาน c996.5วรรค3พูดว่า ( เหมืองที่เน้น ):

การจัดกลุ่มของโอเปอเรเตอร์และโอเปอแรนด์ถูกระบุโดยไวยากรณ์ 724) ยกเว้นตามที่ระบุในภายหลัง (สำหรับการเรียกใช้ฟังก์ชัน (), &&, ||,?:, และเครื่องหมายคอมมา) ลำดับการประเมิน subexpressions และลำดับใน ผลข้างเคียงใดบ้างที่ไม่ได้ระบุ

ดังนั้นเมื่อเรามีสายเช่นนี้:

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 คำถามที่พบบ่อยรายการ


1
Gotcha ที่น่ารังเกียจเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดคือในขณะที่มันเคยปลอดภัยกับคอมไพเลอร์ 99.9% เพื่อใช้*p=(*q)++;ในการแปลif (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;ว่าไม่เป็นเช่นนั้นอีกต่อไป Hyper-modern 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 ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด เพื่อทำความเข้าใจแนวคิดและตัวแปรอื่น ๆ ของพฤติกรรมที่ไม่ได้กำหนดใน 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

@kavadias คำสั่ง printf เกี่ยวข้องกับพฤติกรรมที่ไม่ได้กำหนดด้วยเหตุผลเดียวกันกับที่อธิบายไว้ข้างต้น คุณกำลังเขียนbและcในข้อโต้แย้งที่ 3 และ 4 ตามลำดับและอ่านในข้อโต้แย้งที่ 2 แต่ไม่มีการเรียงลำดับระหว่างนิพจน์เหล่านี้ (args 2nd, 3 และ 4) gcc / clang มีตัวเลือก-Wsequence-pointที่สามารถช่วยค้นหาสิ่งเหล่านี้ได้เช่นกัน
PP

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


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;

จุดลำดับต่าง ๆ แสดงอยู่ในภาคผนวก 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)
    • จุดสิ้นสุดของผู้ประกาศที่สมบูรณ์: ผู้ประกาศ (6.7.6);
    • ระหว่างการประเมินผลของการแสดงออกเต็มรูปแบบและการแสดงออกเต็มรูปแบบต่อไปที่จะได้รับการประเมิน ต่อไปนี้เป็นนิพจน์แบบเต็ม: initializer ที่ไม่ได้เป็นส่วนหนึ่งของตัวอักษรผสม (6.7.9); การแสดงออกในคำสั่งการแสดงออก (6.8.3); การควบคุมการแสดงออกของคำสั่งการเลือก (ถ้าหรือเปลี่ยน) (6.8.4); การแสดงออกในการควบคุมของคำสั่งในขณะหรือทำ (6.8.5); แต่ละนิพจน์ (ทางเลือก) ของคำสั่ง for (6.8.5.3); นิพจน์ (เป็นทางเลือก) ในคำสั่ง return (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++);

ข้างต้นเป็นวิธีการเขียนโปรแกรมทั่วไปในขณะที่การคัดลอก / วิเคราะห์สตริง


แน่นอนว่ามันใช้ไม่ได้กับตัวแปรต่าง ๆ ภายในนิพจน์เดียว มันจะเป็นความล้มเหลวในการออกแบบโดยรวมหากทำ! สิ่งที่คุณต้องการในตัวอย่างที่ 2 คือการเพิ่มขึ้นระหว่างการสิ้นสุดของคำสั่งและการเริ่มต้นถัดไปและรับประกันได้อย่างแม่นยำเพราะแนวคิดของจุดลำดับที่ศูนย์กลางของทั้งหมดนี้
underscore_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 ... OP คาดว่าจะพิมพ์ 6

ส่วน++iเพิ่มนั้นไม่ได้รับประกันว่าจะเสร็จสมบูรณ์ทั้งหมดก่อนการคำนวณที่เหลือ อันที่จริงคอมไพเลอร์ต่าง ๆ จะได้ผลลัพธ์ที่แตกต่างกันที่นี่ ในตัวอย่างที่คุณให้ 2 ครั้งแรก++iดำเนินการแล้วค่าของk[]ถูกอ่านแล้วสุดท้ายแล้ว++ik[]

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

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


5

คำอธิบายที่ดีเกี่ยวกับสิ่งที่เกิดขึ้นในลักษณะของการคำนวณนี้จะอยู่ในเอกสารn1188จากเว็บไซต์ ISO W14

ฉันอธิบายความคิด

กฎหลักจาก ISO 9899 มาตรฐานที่ใช้ในสถานการณ์นี้คือ 6.5p2

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

จุดลำดับในการแสดงออกเหมือนi=i++ก่อนและหลัง i=i++

ในกระดาษที่ฉันยกมาข้างต้นอธิบายไว้ว่าคุณสามารถเข้าใจโปรแกรมที่เกิดขึ้นจากกล่องเล็ก ๆ แต่ละกล่องมีคำแนะนำระหว่างจุดลำดับที่ต่อเนื่องกัน 2 จุด จุดลำดับที่กำหนดไว้ในภาคผนวก C ของมาตรฐานในกรณีที่i=i++มี 2 ​​จุดลำดับที่คั่นนิพจน์แบบเต็ม การแสดงออกดังกล่าวเทียบเท่ากับ syntactically กับรายการของexpression-statementในรูปแบบ Backus-Naur ของไวยากรณ์ (ไวยากรณ์ให้ไว้ในภาคผนวก A ของมาตรฐาน)

ลำดับของคำแนะนำภายในกล่องจึงไม่มีคำสั่งที่ชัดเจน

i=i++

สามารถตีความได้ว่า

tmp = i
i=i+1
i = tmp

หรือเป็น

tmp = i
i = tmp
i=i+1

เพราะทั้งสองรูปแบบทั้งหมดเหล่านี้เพื่อตีความรหัสi=i++นั้นถูกต้องและเนื่องจากทั้งสองสร้างคำตอบที่แตกต่างกันพฤติกรรมจึงไม่ได้กำหนด

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

แก้ไข:

แหล่งที่ดีอื่น ๆ สำหรับการอธิบายความคลุมเครือดังกล่าวเป็นรายการจากC-faqเว็บไซต์ (ตีพิมพ์เป็นหนังสือ ) คือที่นี่และที่นี่และที่นี่


คำตอบนี้เพิ่มใหม่สำหรับคำตอบที่มีอยู่อย่างไร คำอธิบายสำหรับi=i++คล้ายกับคำตอบนี้มาก
haccks

@haccks ฉันไม่ได้อ่านคำตอบอื่น ๆ ฉันต้องการอธิบายด้วยภาษาของฉันเองสิ่งที่ฉันเรียนรู้จากเอกสารที่กล่าวถึงจากเว็บไซต์อย่างเป็นทางการของ ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

คำถามของคุณอาจไม่ "ทำไมสิ่งเหล่านี้สร้างพฤติกรรมที่ไม่ได้กำหนดใน C?" อาจเป็นคำถามของคุณ "ทำไมรหัสนี้ (โดยใช้++) ไม่ให้คุณค่าที่ฉันคาดหวัง" และมีคนทำเครื่องหมายคำถามของคุณว่าซ้ำซ้อนและส่งมาให้คุณที่นี่

คำตอบนี้พยายามตอบคำถามนั้น: เหตุใดรหัสของคุณจึงไม่ให้คำตอบที่คุณคาดหวังและคุณจะเรียนรู้วิธีการรับรู้ (และหลีกเลี่ยง) การแสดงออกที่ไม่ได้ผลตามที่คาดไว้

ฉันคิดว่าคุณเคยได้ยินคำจำกัดความพื้นฐานของ C ++และ--ตัวดำเนินการแล้วและแบบฟอร์ม prefix นั้น++xแตกต่างจากแบบฟอร์ม postfix x++อย่างไร แต่โอเปอเรเตอร์เหล่านี้ยากที่จะคิดดังนั้นเพื่อให้แน่ใจว่าคุณเข้าใจบางทีคุณอาจเขียนโปรแกรมทดสอบเล็ก ๆ ที่เกี่ยวข้องกับบางสิ่งเช่น

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

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

หรือบางทีคุณอาจมองว่าการแสดงออกที่เข้าใจยากเป็นเช่นนั้น

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

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

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

สิ่งที่ทำให้การแสดงออกไม่ได้กำหนด? มีการแสดงออกที่เกี่ยวข้อง++และ--ไม่ได้กำหนดเสมอ? ไม่แน่นอน: นี่เป็นตัวดำเนินการที่มีประโยชน์และหากคุณใช้อย่างถูกต้อง

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

กลับไปที่สองตัวอย่างที่ฉันใช้ในคำตอบนี้ เมื่อฉันเขียน

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

คำถามคือก่อนที่จะโทรprintfรวบรวมคอมไพเลอร์คำนวณค่าxแรกหรือx++หรืออาจจะ++x? แต่มันกลับกลายเป็นเราไม่ทราบว่า ไม่มีกฎใน C ที่บอกว่าข้อโต้แย้งของฟังก์ชันได้รับการประเมินจากซ้ายไปขวาหรือจากขวาไปซ้ายหรือในลำดับอื่น ดังนั้นเราไม่สามารถพูดได้ว่าคอมไพเลอร์จะทำ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 ข้อใดในสามข้อที่พยายามจะ "ชนะ" ซึ่งในสามของค่าจริงจะได้รับการมอบหมายให้x? อีกครั้งและอาจน่าแปลกใจที่ไม่มีกฎใน 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 
}

Visual Studio ทำงานอย่างไรมันใช้วิธีอื่นประเมินการแสดงออกล่วงหน้าทั้งหมดในรอบแรกจากนั้นใช้ค่าตัวแปรในการดำเนินการในรอบที่สองกำหนดจาก 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 ในการประเมินผลอาร์กิวเมนต์ของฟังก์ชั่นเนื่องจากคำถามนี้มักถูกใช้ซ้ำซ้อน (ตัวอย่างสุดท้าย)
Antti Haapala

1
คำถามก็เกี่ยวกับcตอนนี้ไม่ใช่ C ++
Antti Haapala
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.