เหตุใดจึงเป็นเช่นนี้สำหรับการออกจากลูปในบางแพลตฟอร์มและไม่ใช่ในแพลตฟอร์มอื่น


240

ฉันเพิ่งเริ่มเรียนรู้ C และฉันกำลังเรียนกับ C เป็นหัวเรื่อง ฉันกำลังเล่นกับลูปและฉันพบกับพฤติกรรมแปลก ๆ ซึ่งฉันไม่รู้จะอธิบายอย่างไร

#include <stdio.h>

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%d \n", sizeof(array)/sizeof(int));
  return 0;
}

ในแล็ปท็อปที่ใช้ Ubuntu 14.04 รหัสนี้จะไม่หยุด มันจะเสร็จสมบูรณ์ บนคอมพิวเตอร์ของโรงเรียนที่ใช้ CentOS 6.6 มันก็ทำงานได้ดี บน Windows 8.1 การวนซ้ำจะไม่สิ้นสุด

สิ่งที่แปลกยิ่งกว่านั้นคือเมื่อฉันแก้ไขเงื่อนไขของforลูปเป็น: i <= 11รหัสจะสิ้นสุดในแล็ปท็อปที่ใช้ Ubuntu เท่านั้น ไม่เคยยุติใน CentOS และ Windows

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

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


147
เนื่องจากคุณกำลังเอาชนะอาร์เรย์ดังนั้นพฤติกรรมที่ไม่ได้กำหนดจึงเกิดขึ้น พฤติกรรมที่ไม่ได้กำหนดหมายถึงสิ่งที่สามารถเกิดขึ้นได้รวมถึงพฤติกรรมที่ปรากฏ ดังนั้น "รหัสไม่ควรยุติ" จึงไม่ใช่ความคาดหวังที่ถูกต้อง
kaylum

37
ยินดีต้อนรับสู่ซีอาร์เรย์ของคุณมี 10 องค์ประกอบ - หมายเลข
0-9

14
@JonCav คุณทำลายรหัสแล้ว คุณได้รับพฤติกรรมที่ไม่ได้กำหนดซึ่งเป็นรหัสที่ใช้งานไม่ได้
kaylum

50
ทีนี้ประเด็นก็คือพฤติกรรมที่ไม่ได้กำหนดนั้นเป็นอย่างนั้น คุณไม่สามารถทดสอบได้อย่างน่าเชื่อถือและพิสูจน์สิ่งที่จะเกิดขึ้น สิ่งที่อาจจะเกิดขึ้นในเครื่อง Windows ของคุณคือการที่ตัวแปรiจะถูกเก็บไว้ทันทีหลังจากการสิ้นสุดของarray, array[10]=0;และคุณจะเขียนทับมันด้วย นี่อาจไม่ใช่กรณีในบิลด์ที่ปรับให้เหมาะสมบนแพลตฟอร์มเดียวกันซึ่งอาจเก็บไว้iในรีจิสเตอร์และไม่อ้างถึงในหน่วยความจำเลย
ข้าวเปลือก

46
เพราะการไม่สามารถคาดการณ์ได้เป็นคุณสมบัติพื้นฐานของพฤติกรรมที่ไม่ได้กำหนด คุณต้องเข้าใจสิ่งนี้ ... การเดิมพันทั้งหมดปิดแล้ว
ข้าวเปลือก

คำตอบ:


356

ในแล็ปท็อปที่ใช้ Ubuntu 14.04 รหัสนี้จะไม่หยุดทำงานจนเสร็จ บนคอมพิวเตอร์ของโรงเรียนที่ใช้ CentOS 6.6 มันก็ทำงานได้ดี บน Windows 8.1 การวนซ้ำจะไม่สิ้นสุด

สิ่งที่แปลกกว่าคือเมื่อฉันแก้ไขเงื่อนไขของforลูปเป็น: i <= 11รหัสจะสิ้นสุดในแล็ปท็อปที่ใช้ Ubuntu เท่านั้น CentOS และ Windows จะไม่มีวันยุติ

คุณเพิ่งค้นพบความทรงจำที่กระทืบ คุณสามารถอ่านเพิ่มเติมได้ที่นี่: “ ความจำกระทืบ” คืออะไร?

เมื่อคุณจัดสรรint array[10],i;ตัวแปรเหล่านั้นจะเข้าสู่หน่วยความจำ (โดยเฉพาะพวกมันจะถูกจัดสรรในสแต็กซึ่งเป็นบล็อกของหน่วยความจำที่เกี่ยวข้องกับฟังก์ชั่น) array[]และiอาจอยู่ติดกันในหน่วยความจำ มันดูเหมือนว่าบน Windows 8.1, ตั้งอยู่ที่i array[10]บน CentOS, ตั้งอยู่ที่i array[11]และใน Ubuntu ก็ไม่ได้อยู่ในจุดที่เหมาะสมarray[-1]นั้น

ลองเพิ่มคำสั่งการดีบักเหล่านี้ในรหัสของคุณ คุณควรแจ้งให้ทราบว่าในวันที่ซ้ำ 10 หรือ 11 จุดarray[i]i

#include <stdio.h>
 
int main() 
{ 
  int array[10],i; 
 
  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 

6
เฮ้ขอบคุณ! นั่นอธิบายได้ค่อนข้างน้อย ใน Windows จะระบุว่าถ้าชดเชย 10 จากอาเรย์ในขณะที่ทั้ง CentOS และ Ubuntu จะเป็น -1 สิ่งที่แปลกคือถ้าฉันใส่รหัสดีบักเกอร์ CentOS จะไม่สามารถเรียกใช้รหัส (แฮงค์) แต่ด้วยรหัสการดีบักของคุณมันจะทำงาน C ดูเหมือนจะเป็นภาษาที่ดีมากจนถึงตอนนี้ X_x
JonCav

12
@JonCav "it hangs" สามารถเกิดขึ้นได้หากการเขียนเพื่อarray[10]ทำลายกรอบสแต็กเช่น จะมีความแตกต่างระหว่างโค้ดที่มีหรือไม่มีเอาต์พุตการดีบักได้อย่างไร? ถ้าที่อยู่ของiจะไม่จำเป็นต้องใช้คอมไพเลอร์อาจเพิ่มประสิทธิภาพiออกไป เข้าไปลงทะเบียนจึงเปลี่ยนรูปแบบหน่วยความจำในกอง ...
ฮาเจนฟอน Eitzen

2
ฉันไม่คิดว่ามันแขวนอยู่ฉันคิดว่ามันอยู่ในวงวนไม่สิ้นสุดเพราะมันโหลดตัวนับลูปจากหน่วยความจำ (ซึ่งเพิ่งจะเป็นศูนย์โดยarray[10]=0ถ้าคุณคอมไพล์โค้ดของคุณด้วยการออปติไมซ์แล้วนี่อาจจะไม่เกิดขึ้น กฎนามแฝงที่ จำกัด การเข้าถึงหน่วยความจำประเภทใดที่ต้องสันนิษฐานว่าอาจทับซ้อนกับหน่วยความจำอื่น ๆ ในฐานะที่เป็นตัวแปรท้องถิ่นที่คุณไม่เคยใช้ที่อยู่ฉันคิดว่าคอมไพเลอร์ควรจะสามารถสันนิษฐานได้ว่าไม่มีนามแฝง ของอาเรย์เป็นพฤติกรรมที่ไม่ได้กำหนดพยายามอย่างหนักเพื่อหลีกเลี่ยงการใช้งานนั้น
Peter Cordes

4
อีกทางเลือกหนึ่งคือคอมไพเลอร์การออปติไมซ์จะทำการลบอาเรย์อย่างสมบูรณ์เนื่องจากมันไม่มีผลที่สังเกตได้ (ในรหัสดั้งเดิมของคำถาม) ดังนั้นโค้ดผลลัพธ์สามารถพิมพ์สตริงคงที่ได้สิบเอ็ดครั้งตามด้วยการพิมพ์ขนาดคงที่และทำให้โอเวอร์โฟลว์ไม่สามารถสังเกตเห็นได้ทั้งหมด
Holger

9
@ JonCav ฉันจะพูดโดยทั่วไปคุณไม่จำเป็นต้องรู้เพิ่มเติมเกี่ยวกับการจัดการหน่วยความจำและเพียงแค่รู้ว่าไม่ต้องเขียนโค้ดที่ไม่ได้กำหนดโดยเฉพาะอย่าเขียนที่ท้ายอาร์เรย์ ...
T. Kiley

98

ข้อผิดพลาดอยู่ระหว่างโค้ดเหล่านี้:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

ตั้งแต่ arrayมีเพียง 10 องค์ประกอบในการทำซ้ำครั้งล่าสุดarray[10] = 0;คือบัฟเฟอร์ล้น บัฟเฟอร์ล้นนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดซึ่งหมายความว่าพวกเขาอาจฟอร์แมตฮาร์ดไดรฟ์ของคุณหรือทำให้ปีศาจบินออกจากจมูกของคุณ

เป็นเรื่องปกติที่ตัวแปรสแต็กทั้งหมดจะถูกวางไว้ติดกัน หากiอยู่ในตำแหน่งที่array[10]เขียนไปดังนั้น UB จะรีเซ็ตiเป็น0จึงนำไปสู่การวนซ้ำที่ไม่สิ้นสุด

ในการแก้ไขปัญหาการเปลี่ยนแปลงสภาพ loop i < 10ไป


6
Nitpick: คุณไม่สามารถฟอร์แมตฮาร์ดไดรฟ์ในระบบปฏิบัติการใด ๆ ในตลาดได้เว้นแต่คุณจะใช้งานเป็นรูท (หรือเทียบเท่า)
Kevin

26
@Kevin เมื่อคุณเรียกใช้ UB คุณจะยกเลิกการเรียกร้องใด ๆ ต่อสติ
o11c

7
ไม่สำคัญว่ารหัสของคุณมีเหตุผลหรือไม่ ระบบปฏิบัติการจะไม่ยอมให้คุณทำเช่นนั้น
Kevin

2
@Kevin ตัวอย่างที่มีการจัดรูปแบบฮาร์ดไดรฟ์ของคุณมีมานานก่อนหน้านั้นเป็นกรณีนี้ แม้กระทั่งยูนิกซ์ของเวลา (ที่ต้นกำเนิดของ C) ก็มีความสุขมากที่อนุญาตให้คุณทำสิ่งต่าง ๆ เช่นนั้น - และแม้กระทั่งทุกวันนี้ความผิดเพี้ยนจำนวนมากจะช่วยให้คุณเริ่มลบทุกอย่างได้อย่างมีความสุขrm -rf /แม้ว่าคุณจะไม่รูทไม่ใช่ "ฟอร์แมต" ไดรฟ์ทั้งหมดแน่นอน แต่ยังคงทำลายข้อมูลทั้งหมดของคุณ อุ๊ยตาย
Luaan

5
@Kevin แต่พฤติกรรมที่ไม่ได้กำหนดสามารถใช้ช่องโหว่ของระบบปฏิบัติการแล้วยกระดับตัวเองเพื่อติดตั้งไดรเวอร์ฮาร์ดดิสก์ใหม่แล้วเริ่มขัดถูไดรฟ์
วงล้อประหลาด

38

ในสิ่งที่ควรจะเป็นการรันครั้งสุดท้ายของลูปคุณเขียนถึงarray[10]แต่มีเพียง 10 องค์ประกอบในอาเรย์ซึ่งมีหมายเลข 0 ถึง 9 ข้อมูลจำเพาะภาษา C บอกว่านี่คือ "พฤติกรรมที่ไม่ได้กำหนด" สิ่งนี้หมายความว่าในทางปฏิบัติคือโปรแกรมของคุณจะพยายามเขียนลงในintหน่วยความจำขนาดที่อยู่arrayในหน่วยความจำทันที สิ่งที่เกิดขึ้นนั้นขึ้นอยู่กับสิ่งที่เกิดขึ้นจริงแล้วนอนอยู่ที่นั่นและสิ่งนี้ไม่เพียง แต่ขึ้นอยู่กับระบบปฏิบัติการเท่านั้น เป็นต้นมันอาจแตกต่างกันไปจากการดำเนินการจนถึงการกระทำเช่นเนื่องจากการสุ่มพื้นที่ที่อยู่ (อาจไม่ได้อยู่ในตัวอย่างของเล่นนี้ แต่มันเกิดขึ้นในชีวิตจริง) ความเป็นไปได้บางอย่างรวมถึง:

  • ไม่ได้ใช้ตำแหน่ง การวนซ้ำยุติลงตามปกติ
  • ตำแหน่งถูกใช้สำหรับบางสิ่งที่เกิดขึ้นที่มีค่า 0 การวนซ้ำจะสิ้นสุดลงตามปกติ
  • ที่ตั้งมีที่อยู่ผู้ส่งของฟังก์ชั่น การวนซ้ำจะสิ้นสุดลงตามปกติ แต่แล้วโปรแกรมก็ขัดข้องเนื่องจากพยายามข้ามไปยังที่อยู่ 0
  • iสถานที่มีตัวแปร การวนซ้ำไม่สิ้นสุดเนื่องจากiรีสตาร์ทที่ 0
  • สถานที่มีตัวแปรอื่น ๆ การวนซ้ำยุติลงตามปกติ แต่แล้วสิ่งที่“ น่าสนใจ” ก็เกิดขึ้น
  • ตำแหน่งนี้เป็นที่อยู่หน่วยความจำที่ไม่ถูกต้องเช่นเนื่องจากarrayอยู่ด้านท้ายของหน้าหน่วยความจำเสมือนและหน้าถัดไปจะไม่ถูกแมป
  • ปีศาจบินออกจากจมูกของคุณ โชคดีที่คอมพิวเตอร์ส่วนใหญ่ไม่มีฮาร์ดแวร์ที่จำเป็น

สิ่งที่คุณสังเกตเห็นบน Windows ก็คือว่าคอมไพเลอร์ตัดสินใจที่จะวางตัวแปรiทันทีหลังจากอาร์เรย์ในหน่วยความจำเพื่อให้จบลงด้วยการมอบหมายให้array[10] = 0 iบน Ubuntu และ CentOS คอมไพเลอร์ไม่ได้อยู่ที่iนั่น การใช้งาน C เกือบทั้งหมดทำกลุ่มตัวแปรโลคอลในหน่วยความจำบนสแต็คหน่วยความจำโดยมีข้อยกเว้นหลักประการหนึ่ง: ตัวแปรโลคอลบางตัวสามารถวางได้ทั้งหมดในรีจิสเตอร์ได้ทั้งหมด แม้ว่าตัวแปรอยู่ในสแต็กลำดับของตัวแปรจะถูกกำหนดโดยคอมไพเลอร์และอาจขึ้นอยู่กับลำดับในไฟล์ต้นฉบับเท่านั้น แต่ยังอยู่ในประเภทของไฟล์เหล่านั้นด้วย ในชื่อของพวกเขาในค่าแฮชบางอย่างที่ใช้ในโครงสร้างข้อมูลภายในของคอมไพเลอร์ ฯลฯ

หากคุณต้องการทราบว่าคอมไพเลอร์ของคุณตัดสินใจทำอะไรคุณสามารถบอกให้คอมไพเลอร์แสดงรหัส โอ้และเรียนรู้ที่จะถอดรหัสแอสเซมเบลอร์ (ง่ายกว่าการเขียน) ด้วย GCC (และคอมไพเลอร์อื่น ๆ โดยเฉพาะในโลกของ Unix) ให้ส่งตัวเลือก-Sในการสร้างรหัสแอสเซมเบลอร์แทนไบนารี ตัวอย่างเช่นนี่คือตัวอย่างข้อมูลประกอบของลูปจากการรวบรวมกับ GCC ใน amd64 ด้วยตัวเลือกการเพิ่มประสิทธิภาพ-O0(ไม่มีการเพิ่มประสิทธิภาพ) โดยมีการเพิ่มความคิดเห็นด้วยตนเอง:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -52(%rbp)            ; compare i to 10
    jle     .L3

นี่คือตัวแปรที่i52 ไบต์ด้านล่างด้านบนของสแต็กในขณะที่อาร์เรย์เริ่ม 48 ไบต์ด้านล่างด้านบนของสแต็ก ดังนั้นคอมไพเลอร์นี้เกิดขึ้นiก่อนที่อาร์เรย์; คุณต้องการเขียนทับถ้าคุณเกิดขึ้นที่จะเขียนถึงi array[-1]ถ้าคุณเปลี่ยนarray[i]=0ไปarray[9-i]=0คุณจะได้รับห่วงอนันต์บนแพลตฟอร์มนี้โดยเฉพาะอย่างยิ่งกับตัวเลือกคอมไพเลอร์เหล่านี้โดยเฉพาะอย่างยิ่ง

gcc -O1ตอนนี้ขอรวบรวมโปรแกรมของคุณด้วย

    movl    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %ebx
    jne     .L3

นั่นสั้นกว่า! คอมไพเลอร์ไม่เพียง แต่ปฏิเสธที่จะจัดสรรตำแหน่งสแต็กสำหรับi- มันเคยถูกเก็บไว้ในรีจิสเตอร์ebxเท่านั้น แต่มันไม่ได้ใส่ใจที่จะจัดสรรหน่วยความจำใด ๆarrayหรือสร้างรหัสเพื่อตั้งองค์ประกอบเนื่องจากมันสังเกตว่าไม่มีองค์ประกอบใด ๆ เคยใช้

เพื่อให้ตัวอย่างนี้บอกได้มากขึ้นให้แน่ใจว่าการกำหนดอาเรย์จะดำเนินการโดยการให้คอมไพเลอร์กับสิ่งที่มันไม่สามารถเพิ่มประสิทธิภาพออกไป วิธีง่ายๆในการทำเช่นนั้นคือการใช้อาร์เรย์จากไฟล์อื่น - เนื่องจากการรวบรวมแยกกันคอมไพเลอร์ไม่ทราบว่าเกิดอะไรขึ้นในไฟล์อื่น (เว้นแต่ว่ามันจะปรับให้เหมาะสมในเวลาลิงค์gcc -O0หรือgcc -O1ไม่) สร้างไฟล์ต้นฉบับuse_array.cที่มี

void use_array(int *array) {}

และเปลี่ยนซอร์สโค้ดของคุณเป็น

#include <stdio.h>
void use_array(int *array);

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%zd \n", sizeof(array)/sizeof(int));
  use_array(array);
  return 0;
}

รวบรวมกับ

gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o

คราวนี้โค้ดแอสเซมเบลอร์จะเป็นดังนี้:

    movq    %rsp, %rbx
    leaq    44(%rsp), %rbp
.L3:
    movl    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

ตอนนี้อาร์เรย์อยู่บนสแต็ก 44 ไบต์จากด้านบน เกี่ยวกับiอะไร มันจะไม่ปรากฏที่ใดก็ได้! rbxแต่วงเคาน์เตอร์จะถูกเก็บไว้ในการลงทะเบียน มันไม่ตรงแต่ที่อยู่ของi array[i]คอมไพเลอร์ตัดสินใจว่าเนื่องจากiไม่เคยใช้ค่าของโดยตรงจึงไม่มีประเด็นในการคำนวณทางคณิตศาสตร์เพื่อคำนวณตำแหน่งที่จะเก็บ 0 ระหว่างการรันลูปแต่ละครั้ง แต่ที่อยู่นั้นเป็นตัวแปรลูปและการคำนวณทางคณิตศาสตร์เพื่อกำหนดขอบเขตได้ดำเนินการบางส่วน ณ เวลารวบรวม (คูณ 11 ซ้ำคูณด้วย 4 ไบต์ต่อองค์ประกอบอาเรย์เพื่อรับ 44) และบางส่วนในเวลาดำเนินการ แต่เพียงครั้งเดียว ทำการลบเพื่อรับค่าเริ่มต้น)

แม้ในตัวอย่างง่ายๆนี้เราได้เห็นว่าการเปลี่ยนตัวเลือกคอมไพเลอร์ (เปิดการปรับให้เหมาะสม) หรือเปลี่ยนบางสิ่งเล็กน้อย ( array[i]เป็นarray[9-i]) หรือแม้กระทั่งการเปลี่ยนแปลงบางสิ่งที่ไม่เกี่ยวข้องอย่างเห็นได้ชัด (เพิ่มการเรียกไปยังuse_array) สามารถสร้างความแตกต่าง โดยคอมไพเลอร์ทำ การเพิ่มประสิทธิภาพคอมไพเลอร์สามารถทำหลายสิ่งหลายอย่างที่อาจปรากฏ unintuitive ในโปรแกรมที่เรียกพฤติกรรมที่ไม่ได้กำหนด นั่นเป็นสาเหตุที่พฤติกรรมที่ไม่ได้กำหนดนั้นไม่ได้กำหนดอย่างสมบูรณ์ เมื่อคุณเบี่ยงเบนไปจากแทร็กเล็กน้อยในโปรแกรมโลกแห่งความจริงมันยากที่จะเข้าใจความสัมพันธ์ระหว่างสิ่งที่รหัสทำกับสิ่งที่ควรทำแม้กระทั่งโปรแกรมเมอร์ที่มีประสบการณ์


25

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


สำหรับอาร์เรย์:

int array[10]

ดัชนีเท่านั้นที่ถูกต้องในช่วงที่จะ0 9อย่างไรก็ตามคุณพยายามที่จะ:

for (i = 0; i <=10 ; i++)

เข้าถึงarray[10]ที่นี่เปลี่ยนเงื่อนไขเป็นi < 10


6
การทำอย่างไม่ตั้งใจนั้นนำไปสู่พฤติกรรมที่ไม่ได้กำหนด - คอมไพเลอร์ไม่สามารถบอกได้! ;-)
Toby Speight

1
เพียงใช้มาโครเพื่อแสดงข้อผิดพลาดของคุณเป็นคำเตือน: #define UNINTENDED_MISTAKE (EXP) printf ("คำเตือน:" #EXP "ผิดพลาด \ n");
lkraider

1
ฉันหมายความว่าหากคุณทำผิดพลาดกับวัตถุประสงค์คุณอาจระบุเช่นนี้และทำให้ปลอดภัยเพื่อหลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนด D
lkraider

19

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

array[10]ไม่ถูกต้อง มันมี 10 องค์ประกอบarray[0]ผ่านarray[9]และarray[10]เป็น 11 ลูปของคุณควรเขียนให้หยุดก่อน 10ดังนี้:

for (i = 0; i < 10; i++)

ไหนarray[10]ดินแดนคือการดำเนินการที่กำหนดไว้และสนุกสนานในสองของแพลตฟอร์มที่คุณต้องการได้ที่ดินบนซึ่งแพลตฟอร์มเหล่านั้นเห็นได้ชัดว่าออกวางโดยตรงหลังจาก i ถูกตั้งค่าเป็นศูนย์และการวนซ้ำจะดำเนินต่อไปตลอดกาล สำหรับแพลตฟอร์มอื่น ๆ ของคุณอาจอยู่ก่อนหรืออาจมีช่องว่างภายในarrayiiarrayarray


ฉันไม่คิดว่า valgrind สามารถจับสิ่งนี้ได้เพราะมันยังเป็นสถานที่ที่ถูกต้อง แต่ ASAN สามารถทำได้
o11c

13

คุณประกาศint array[10]หมายถึงarrayมีดรรชนี0ถึง9( 10องค์ประกอบจำนวนเต็มทั้งหมดที่สามารถเก็บได้) แต่ห่วงต่อไปนี้

for (i = 0; i <=10 ; i++)

ประสงค์ห่วง0จะ10หมายถึง11เวลา ดังนั้นเมื่อi = 10มันจะล้นบัฟเฟอร์และก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนด

ลองทำสิ่งนี้:

for (i = 0; i < 10 ; i++)

หรือ,

for (i = 0; i <= 9 ; i++)

7

มันไม่ได้กำหนดที่array[10]และให้พฤติกรรมที่ไม่ได้กำหนดตามที่อธิบายไว้ก่อน คิดเกี่ยวกับสิ่งนี้:

ฉันมี 10 รายการในรถเข็นขายของชำของฉัน พวกเขาเป็น:

0: กล่องซีเรียล
1: ขนมปัง
2: นม
3: พาย
4: ไข่
5: เค้ก
6: โซดา 2 ลิตร
7: สลัด
8: เบอร์เกอร์
9: ไอศครีม

cart[10]ไม่ได้กำหนดและอาจให้ข้อยกเว้นนอกขอบเขตในคอมไพเลอร์บางรายการ แต่เห็นได้ชัดว่าไม่มาก รายการที่ 11 ที่ชัดเจนคือรายการที่ไม่ได้อยู่ในตะกร้า รายการที่ 11 ชี้ไปที่สิ่งที่ฉันจะเรียกว่าเป็น "รายการโพลเทอเรจิสต์" มันไม่เคยมีอยู่จริง แต่มันอยู่ที่นั่น

ทำไมคอมไพเลอร์บางตัวให้iดัชนีarray[10]หรือarray[11]หรือarray[-1]เป็นเพราะคำสั่งเริ่มต้น / ประกาศของคุณ คอมไพเลอร์บางคนตีความสิ่งนี้ว่า:

  • "จัดสรร 10 บล็อกของints สำหรับarray[10]และอีกintบล็อกหนึ่งเพื่อให้ง่ายขึ้นวางไว้ถัดจากกันและกัน"
  • เช่นเดียวกับก่อน แต่ย้ายพื้นที่หรือสองออกไปเพื่อให้ไม่ได้ชี้ไปarray[10]i
  • ทำแบบเดียวกันก่อน แต่จัดสรรiที่array[-1](เพราะดัชนีของอาร์เรย์ไม่สามารถหรือไม่ควรเป็นลบ) หรือจัดสรรที่จุดที่แตกต่างอย่างสิ้นเชิงเพราะระบบปฏิบัติการสามารถจัดการกับมันได้และปลอดภัยกว่า

คอมไพเลอร์บางคนต้องการให้สิ่งต่าง ๆ เร็วขึ้นและคอมไพเลอร์บางคนต้องการความปลอดภัย มันเกี่ยวกับบริบท ถ้าฉันกำลังพัฒนาแอพสำหรับ BREW OS โบราณ (ระบบปฏิบัติการของโทรศัพท์พื้นฐาน) ตัวอย่างเช่นมันจะไม่สนใจเรื่องความปลอดภัย ถ้าฉันพัฒนาสำหรับ iPhone 6 ก็สามารถทำงานได้อย่างรวดเร็วไม่ว่าจะเกิดอะไรขึ้นดังนั้นฉันจะต้องให้ความสำคัญกับความปลอดภัย (คุณอ่านแนวทางของ Apple App Store อย่างจริงจังหรืออ่านเกี่ยวกับการพัฒนา Swift และ Swift 2.0 หรือไม่)


หมายเหตุ: ฉันพิมพ์รายการเพื่อให้เป็น "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" แต่ภาษามาร์กอัปของ SO นั้นกำหนดตำแหน่งของรายการที่ฉันสั่งไว้
DDPWNAGE

6

เนื่องจากคุณสร้างอาร์เรย์ขนาด 10 สำหรับเงื่อนไขลูปควรเป็นดังนี้:

int array[10],i;

for (i = 0; i <10 ; i++)
{

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


5

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


4

นอกเหนือจากความเป็นไปได้ที่หน่วยความจำอาจวางเพื่อให้ความพยายามในการเขียนเพื่อa[10]เขียนทับจริงiก็เป็นไปได้ว่าคอมไพเลอร์ที่ปรับให้เหมาะสมอาจพิจารณาว่าการทดสอบลูปไม่สามารถเข้าถึงได้ด้วยค่าที่iมากกว่าสิบโดยไม่มีรหัส a[10]องค์ประกอบอาร์เรย์ที่ไม่มีอยู่จริง

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


3

เมื่อคุณวนซ้ำในอดีตi==9คุณจะกำหนดศูนย์ให้กับ 'รายการอาร์เรย์' ซึ่งอยู่เหนืออาร์เรย์ดังนั้นคุณจึงเขียนทับข้อมูลอื่น ๆ ส่วนใหญ่อาจจะเขียนทับตัวแปรซึ่งตั้งอยู่หลังi a[]ด้วยวิธีนี้คุณเพียงแค่รีเซ็ตiตัวแปรให้เป็นศูนย์แล้วจึงรีสตาร์ทลูป

คุณสามารถค้นพบว่าตัวคุณเองถ้าคุณพิมพ์iในลูป:

      printf("test i=%d\n", i);

แทนที่จะเป็นเพียงแค่

      printf("test \n");

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


0

ข้อผิดพลาดอยู่ในส่วนอาร์เรย์ [10] w / c ยังเป็นที่อยู่ของฉัน (int อาร์เรย์ [10], i;) เมื่อ array [10] ถูกตั้งค่าเป็น 0 ดังนั้นฉันจะเป็น 0 w / c รีเซ็ตลูปทั้งหมดและทำให้ลูปไม่สิ้นสุด จะมีลูปไม่สิ้นสุดถ้าอาร์เรย์ [10] อยู่ระหว่าง 0-10. ลูปที่ถูกต้องควรเป็น (i = 0; i <10; i ++) {... } อาร์เรย์ int [10], i; สำหรับ (i = 0; i <= 10; i ++) อาร์เรย์ [i] = 0;


0

ฉันจะแนะนำสิ่งที่ฉันหาพบข้างต้น:

ลองกำหนดอาร์เรย์ [i] = 20;

ฉันเดาว่าสิ่งนี้ควรยุติโค้ดทุกที่ .. (ให้คุณเก็บ i <= 10 หรือ ll)

หากการดำเนินการนี้คุณสามารถตัดสินใจได้อย่างมั่นใจว่าคำตอบที่ระบุไว้ที่นี่นั้นถูกต้องแล้ว [คำตอบที่เกี่ยวข้องกับหน่วยความจำกระทืบหนึ่งสำหรับอดีต]


-9

มีสองสิ่งผิดปกติที่นี่ int i จริง ๆ แล้วเป็นองค์ประกอบอาร์เรย์อาร์เรย์ [10] ตามที่เห็นในสแต็ก เนื่องจากคุณอนุญาตให้การสร้างดัชนีทำให้อาร์เรย์ [10] = 0, ดัชนีลูป, i, จะไม่เกิน 10 ทำให้เป็นfor(i=0; i<10; i+=1)จริง

i ++ คือตามที่K&Rเรียกมันว่า 'รูปแบบไม่ดี' มันเป็นการเพิ่มค่า i ตามขนาดของ i ไม่ใช่ 1 i ++ สำหรับคณิตศาสตร์พอยน์เตอร์และ i + = 1 สำหรับพีชคณิต ในขณะนี้ขึ้นอยู่กับคอมไพเลอร์ แต่ก็ไม่ได้เป็นข้อตกลงที่ดีสำหรับการพกพา


5
-1 ผิดโดยสิ้นเชิง ตัวแปรiคือองค์ประกอบอาเรน NOTan a[10]ไม่มีข้อผูกมัดหรือแม้แต่ข้อเสนอแนะสำหรับคอมไพเลอร์ที่จะวางมันลงในสแต็คทันทีหลังจาก a[] - มันสามารถถูกวางไว้ข้างหน้าอาเรย์หรือคั่นด้วยพื้นที่เพิ่มเติมบางส่วน สามารถจัดสรรได้นอกหน่วยความจำหลักเช่นใน CPU register นอกจากนี้ยังไม่จริง++สำหรับตัวชี้และไม่ใช่จำนวนเต็ม ผิดอย่างสมบูรณ์คือ 'i ++ กำลังเพิ่มขึ้น i ตามขนาดของ i' - อ่านคำอธิบายโอเปอเรเตอร์ในการกำหนดภาษา!
CiaPan

นี่คือเหตุผลว่าทำไมมันจึงใช้งานได้ในบางแพลตฟอร์มและไม่ใช่สำหรับบางแพลตฟอร์ม มันเป็นเพียงคำอธิบายที่สมเหตุสมผลว่าทำไมมันจึงวนซ้ำบนหน้าต่างตลอดไป สำหรับ I ++ มันเป็นตัวชี้ทางคณิตศาสตร์ไม่ใช่จำนวนเต็ม อ่านพระคัมภีร์ ... 'ภาษาการเขียนโปรแกรม C' โดย Kernigan และ Ritche หากคุณต้องการฉันมีสำเนาลายเซ็นต์และได้รับการเขียนโปรแกรมในคตั้งแต่ปี 1981
SkipBerne

1
อ่านซอร์สโค้ดโดย OP และค้นหาการประกาศตัวแปรi- มันเป็นintชนิด มันเป็นจำนวนเต็มไม่ใช่ตัวชี้ จำนวนเต็มใช้เป็นดัชนีไปarray,
CiaPan

1
ฉันทำและนั่นคือเหตุผลที่ฉันแสดงความคิดเห็นเช่นเดียวกับฉัน บางทีคุณควรตระหนักว่าหากคอมไพเลอร์มีการตรวจสอบสแต็กและในกรณีนี้มันไม่สำคัญว่าการอ้างอิงสแต็กเมื่อ I = 10 จะเป็นการอ้างอิงจริงในคอมไพล์บางดัชนีอาเรย์และอยู่ภายในขอบเขตของพื้นที่สแต็ก คอมไพเลอร์ไม่สามารถแก้ไขความโง่ได้ คอมไพล์อาจทำการแก้ไขตามที่ปรากฎนี้ แต่การตีความอย่างแท้จริงของภาษาการเขียนโปรแกรม c จะไม่รองรับการประชุมนี้
SkipBerne

@SkipBerne: พิจารณาที่จะลบคำตอบของคุณก่อนที่คุณจะได้รับ "ได้รับ" ที่มีคะแนนเชิงลบมากขึ้น
Peter VARGA
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.