การจัดทำดัชนีตัวชี้


11

ฉันกำลังอ่านหนังสือชื่อ "Numerical Recipes in C" ในหนังสือเล่มนี้ผู้เขียนให้รายละเอียดว่าอัลกอริทึมบางอย่างทำงานได้ดีกว่าโดยธรรมชาติถ้าเรามีดัชนีที่ขึ้นต้นด้วย 1 (ฉันไม่ได้ทำตามอาร์กิวเมนต์ทั้งหมดของเขาและนั่นไม่ใช่จุดของโพสต์นี้) แต่ C จะทำดัชนีอาร์เรย์ของมันเสมอ เพื่อหลีกเลี่ยงสิ่งนี้เขาแนะนำให้ลดทอนพอยน์เตอร์หลังจากการจัดสรรเช่น:

float *a = malloc(size);
a--;

เขาบอกว่าสิ่งนี้จะให้ตัวชี้ที่มีดัชนีเริ่มต้นด้วย 1 อย่างมีประสิทธิภาพซึ่งจะทำให้คุณเป็นอิสระด้วย:

free(a + 1);

เท่าที่ฉันรู้ว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดโดยมาตรฐาน C เห็นได้ชัดว่านี่เป็นหนังสือที่มีชื่อเสียงอย่างมากในชุมชน HPC ดังนั้นฉันไม่ต้องการเพียงแค่เพิกเฉยต่อสิ่งที่เขาพูด แต่เพียงการลดตัวชี้นอกช่วงที่จัดสรรนั้น นี่เป็นพฤติกรรม "อนุญาต" ใน C หรือไม่ ฉันได้ทำการทดสอบโดยใช้ทั้ง gcc และ icc และผลลัพธ์ทั้งสองนั้นปรากฏขึ้นเพื่อระบุว่าฉันไม่ต้องกังวลอะไร แต่ฉันต้องการที่จะเป็นบวกอย่างแน่นอน


3
สิ่งที่C มาตรฐานคุณดู? ฉันถามเพราะความทรงจำของฉัน "Numerical Recipes in C" ได้รับการเผยแพร่ในปี 1990 ในสมัยโบราณของ K&R และอาจ ANSI C
gnat

2
คำถาม SO ที่เกี่ยวข้อง: stackoverflow.com/questions/10473573/…
dan04

3
"ฉันได้ทำการทดสอบโดยใช้ทั้ง gcc และ icc และผลลัพธ์ทั้งสองนั้นปรากฏขึ้นเพื่อระบุว่าฉันไม่ต้องกังวลอะไร แต่ฉันต้องการที่จะเป็นบวกอย่างแน่นอน" อย่าสันนิษฐานว่าเพราะคอมไพเลอร์ของคุณอนุญาตให้ใช้ภาษา C จึงอนุญาต แน่นอนว่าคุณไม่มีปัญหากับโค้ดของคุณที่จะแตกหักในอนาคต
Doval

5
"Recipies ตัวเลข" โดยทั่วไปถือว่าเป็นหนังสือที่มีประโยชน์รวดเร็วและสกปรกไม่ใช่กระบวนทัศน์ของการพัฒนาซอฟต์แวร์หรือการวิเคราะห์เชิงตัวเลข ลองอ่านบทความ Wikipedia เกี่ยวกับ "Numerical Recipies" เพื่อหาบทสรุปของการวิพากษ์วิจารณ์บางส่วน
Charles E. Grant

1
นี่คือเหตุผลที่เราจัดทำดัชนีจากศูนย์: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Russell Borogove

คำตอบ:


16

คุณถูกรหัสนั้นเช่น

float a = malloc(size);
a--;

ให้ผลการทำงานที่ไม่ได้กำหนดตามมาตรฐาน ANSI C, ส่วน 3.3.6:

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

สำหรับรหัสเช่นนี้คุณภาพของรหัส C ในหนังสือ (ย้อนกลับไปเมื่อฉันใช้มันในปลายปี 1990) ไม่ถือว่าสูงมาก

ปัญหาเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดคือไม่ว่าผลลัพธ์ที่คอมไพเลอร์จะผลิตออกมานั้นเป็นผลที่ได้จากการนิยามที่ถูกต้อง (แม้ว่าจะมีการทำลายล้างสูง
โชคดีที่มีคอมไพเลอร์เพียงไม่กี่ตัวที่พยายามทำให้เกิดพฤติกรรมที่ไม่คาดคิดสำหรับกรณีดังกล่าวและการmallocใช้งานโดยทั่วไปบนเครื่องที่ใช้สำหรับ HPC มีข้อมูลการทำบัญชีบางอย่างก่อนที่มันจะส่งกลับที่อยู่ดังนั้นการลดลงจะทำให้ ไม่ใช่ความคิดที่ดีที่จะเขียน แต่การสร้างตัวชี้นั้นไม่เป็นอันตรายต่อระบบเหล่านั้น

เพิ่งทราบว่ารหัสอาจแตกเมื่อสภาพแวดล้อมรันไทม์ได้รับการเปลี่ยนแปลงหรือเมื่อรหัสถูกพอร์ตไปยังสภาพแวดล้อมที่แตกต่างกัน


4
เป็นไปได้ในสถาปัตยกรรมหลายธนาคารที่ malloc สามารถให้ที่อยู่ 0 ในธนาคารและการลดลงอาจทำให้เกิดกับดัก CPU ที่มีอันเดอร์โฟล์หนึ่ง
Vality

1
ฉันไม่เห็นด้วยนั่นคือ "โชคดี" ฉันคิดว่ามันจะดีกว่ามากถ้าคอมไพเลอร์ส่งโค้ดที่ล้มเหลวทันทีเมื่อใดก็ตามที่คุณเรียกใช้พฤติกรรมที่ไม่ได้กำหนด
David Conrad

4
@DavidConrad: จากนั้น C ไม่ใช่ภาษาสำหรับคุณ พฤติกรรมที่ไม่ได้กำหนดจำนวนมากใน C ไม่สามารถตรวจพบได้ง่ายหรือมีการโจมตีที่รุนแรง
Bart van Ingen Schenau

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

@BartvanIngenSchenau ขึ้นอยู่กับสิ่งที่คุณหมายถึงโดย 'การทำงานที่รุนแรง' มีการดำเนินการเชิงสัญลักษณ์สำหรับ C (เช่นเสียงดังกราว + klee) เช่นเดียวกับ sanatizers (asan, tsan, ubsan, valgrind เป็นต้น) ซึ่งมีประโยชน์มากสำหรับการแก้ไขข้อบกพร่อง
Maciej Piechotka

10

อย่างเป็นทางการมันเป็นพฤติกรรมที่ไม่ได้กำหนดที่จะมีการจุดชี้นอกอาร์เรย์ (ยกเว้นหนึ่งที่ผ่านมาสิ้นสุด), ถึงแม้ว่ามันจะไม่เคย dereferenced

ในทางปฏิบัติหากหน่วยประมวลผลของคุณมีรูปแบบหน่วยความจำแบบแบน (ตรงข้ามกับหน่วยความจำแปลก ๆ เช่นx86-16 ) และหากคอมไพเลอร์ไม่ให้ข้อผิดพลาดรันไทม์หรือการเพิ่มประสิทธิภาพที่ไม่ถูกต้องหากคุณสร้างตัวชี้ที่ไม่ถูกต้อง ได้ดี


1
นั่นทำให้รู้สึก น่าเสียดายที่มันมากเกินไปสำหรับฉัน
wolfPack88

3
ประเด็นสุดท้ายคือ IMHO ที่เป็นปัญหามากที่สุด เนื่องจากคอมไพเลอร์เวลาเหล่านี้ไม่เพียง แต่เกิดขึ้นไม่ว่าแพลตฟอร์ม "โดยธรรมชาติ" จะเป็นอย่างไรในกรณีของ UB แต่ตัวเพิ่มประสิทธิภาพใช้ประโยชน์อย่างจริงจังในเชิงรุกฉันจึงไม่เล่นกับมันอย่างเบาบาง
Matteo Italia

3

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

โดยไม่สนใจคุณอาจลบ 1 หรือ 2 หรือ 1980 ตัวอย่างเช่นถ้าฉันมีข้อมูลทางการเงินสำหรับปี 1980 ถึง 2013 ฉันอาจลบ 1980 ตอนนี้ถ้าเราใช้ float * a = malloc (ขนาด); มีแน่นอนบางอย่างคงที่ k ขนาดใหญ่เช่นว่า - k เป็นตัวชี้โมฆะ ในกรณีนี้เราคาดหวังว่ามีบางอย่างผิดปกติ

ตอนนี้รับโครงสร้างขนาดใหญ่พูดขนาดเมกะไบต์ จัดสรรตัวชี้ p ชี้ไปที่สอง structs p - 1 อาจเป็นตัวชี้โมฆะ p - 1 อาจล้อมรอบ (ถ้า struct เป็นเมกะไบต์และบล็อก malloc คือ 900 KB จากจุดเริ่มต้นของพื้นที่ที่อยู่) ดังนั้นจึงอาจไม่มีความอาฆาตพยาบาทของคอมไพเลอร์ใด ๆ ที่ p - 1> p สิ่งที่อาจน่าสนใจ


1

... การลดทอนตัวชี้นอกช่วงที่จัดสรรนั้นดูเหมือนว่าจะเป็นภาพร่างที่สมบูรณ์สำหรับฉัน นี่เป็นพฤติกรรม "อนุญาต" ใน C หรือไม่

อนุญาตหรือไม่ ใช่. ความคิดที่ดี? ไม่ปกติ

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

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

อาร์เรย์ไม่ใช่สิ่งที่อยู่ใน C พวกมันเป็นเพียงพอยน์เตอร์สำหรับช่วงของหน่วยความจำต่อเนื่องที่ทำงานเหมือนอาร์เรย์ []ประกอบการจดชวเลขสำหรับการทำเลขคณิตชี้และ dereferencing ดังนั้นจริงหมายถึงa[x]*(a + x)

มีเหตุผลที่ถูกต้องในการทำข้างต้นเช่นอุปกรณ์ I / O บางคนคู่ของมีdoubles แมปลงและ0xdeadbee7 0xdeadbeefโปรแกรมน้อยมากที่จะต้องทำเช่นนั้น

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

หากคุณต้องการ 1-based arrays ใน C คุณสามารถทำได้อย่างปลอดภัยด้วยค่าใช้จ่ายในการจัดสรรองค์ประกอบเพิ่มเติมอีกหนึ่งองค์ประกอบที่จะไม่ถูกใช้งาน:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

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


ภาคผนวก:

บางบทและร้อยกรองจากC99 ฉบับร่าง (ขออภัยนั่นคือทั้งหมดที่ฉันสามารถเชื่อมโยงไป):

§6.5.2.1.1บอกว่านิพจน์ที่สอง ("อื่น ๆ ") ที่ใช้กับโอเปอเรเตอร์ตัวห้อยเป็นประเภทจำนวนเต็ม -1เป็นจำนวนเต็มและทำให้p[-1]ถูกต้องและทำให้ตัวชี้&(p[-1])ใช้ได้เช่นกัน นี่ไม่ได้หมายความว่าการเข้าถึงหน่วยความจำในตำแหน่งนั้นจะสร้างพฤติกรรมที่กำหนด แต่ตัวชี้ยังคงเป็นตัวชี้ที่ถูกต้อง

§6.5.2.2บอกว่าอาร์เรย์ห้อยประเมินผู้ประกอบการที่จะเทียบเท่าของการเพิ่มจำนวนองค์ประกอบที่จะชี้จึงจะเทียบเท่ากับp[-1] *(p + (-1))ยังคงใช้ได้ แต่อาจไม่ก่อให้เกิดพฤติกรรมที่พึงประสงค์

§6.5.6.8พูดว่า (เน้นการทำเหมือง):

เมื่อนิพจน์ที่มีชนิดจำนวนเต็มถูกเพิ่มหรือลบออกจากตัวชี้ผลลัพธ์จะมีชนิดของตัวถูกดำเนินการตัวชี้

... ถ้านิพจน์Pชี้ไปที่iองค์ประกอบ -th ของวัตถุอาร์เรย์นิพจน์(P)+N(เท่ากันN+(P)) และ(P)-N (โดยที่Nมีค่าn) ชี้ไปที่ตามลำดับองค์ประกอบที่ -th i+nและ i−n-th ของวัตถุอาร์เรย์หากพวกเขามีอยู่ .

ซึ่งหมายความว่าผลลัพธ์ของการคำนวณทางคณิตศาสตร์ต้องชี้ไปที่องค์ประกอบในอาร์เรย์ ไม่ได้บอกว่าต้องใช้เลขคณิตทั้งหมดในครั้งเดียว ดังนั้น:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

ฉันแนะนำให้ทำสิ่งนี้ด้วยวิธีนี้หรือไม่? ฉันทำไม่ได้และคำตอบของฉันอธิบายว่าทำไม


8
-1 ความหมายของ 'ได้รับอนุญาต' ซึ่งรวมถึงรหัสมาตรฐาน C ประกาศว่าการสร้างผลลัพธ์ที่ไม่ได้กำหนดไม่ได้มีประโยชน์
Pete Kirkham

คนอื่น ๆ ชี้ให้เห็นว่ามันเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นคุณไม่ควรพูดว่า "อนุญาต" อย่างไรก็ตามคำแนะนำในการจัดสรรองค์ประกอบที่ไม่ได้ใช้เป็นพิเศษนั้นเป็นสิ่งที่ดี
200_success

สิ่งนี้ไม่ถูกต้องอย่างน้อยโปรดทราบว่านี่เป็นสิ่งต้องห้ามตามมาตรฐาน C
Vality

@PeteKirkham: ฉันไม่เห็นด้วย ดูภาคผนวกของคำตอบของฉัน
Blrfl

4
@Blrfl 6.5.6 ของสถานะมาตรฐาน ISO C11 ในกรณีของการเพิ่มจำนวนเต็มไปยังตัวชี้: "ถ้าทั้งตัวถูกดำเนินการตัวชี้และผลชี้ไปที่องค์ประกอบของวัตถุอาร์เรย์เดียวกันหรือหนึ่งองค์ประกอบสุดท้ายของวัตถุอาร์เรย์ที่ผ่านมา การประเมินผลจะไม่ทำให้เกิดการไหลล้นมิฉะนั้นพฤติกรรมจะไม่ได้กำหนดไว้ "
Vality
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.