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


34

หลักเกณฑ์ C ++ Core มีกฎES.20: เริ่มต้นวัตถุเสมอ

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

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

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

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

และในที่สุดก็มีเครื่องวิเคราะห์แบบสแตติกซึ่งสามารถดูโปรแกรมและบอกว่ามีการอ่านก่อนตั้งค่าบนพา ธ การเรียกใช้งานนั้น

ดังนั้นเราจึงมีเครื่องมือที่ทรงพลังมากมาย แต่ถ้าเราเริ่มต้นตัวแปร - sanitizers ไม่พบอะไรเลย

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

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


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

2
@Abyx: แม้ว่าจะเป็นบุคคลที่สามหากไม่จัดการกับบัฟเฟอร์ที่เริ่มต้นด้วย\0มันเป็นรถ หากมีการบันทึกไว้ว่าจะไม่จัดการกับสิ่งนั้นรหัสการโทรของคุณจะเป็นรถ หากคุณแก้ไขรหัสโทรศัพท์ของคุณเพื่อตรวจสอบbytes_read==0ก่อนใช้งานคุณจะกลับไปยังที่ที่คุณเริ่ม: รหัสของคุณจะสกปรกถ้าคุณไม่เริ่มต้นbytes_readปลอดภัยหากคุณทำ ( โดยปกติฟังก์ชั่นควรจะเติมพารามิเตอร์ของพวกเขาแม้ในกรณีที่มีข้อผิดพลาด : ไม่จริง ๆ บ่อยครั้งที่ผลลัพธ์ที่ได้จะถูกทิ้งไว้ตามลำพังหรือไม่ได้กำหนดไว้)
Mat

1
มีเหตุผลบางอย่างที่รหัสนี้ละเว้นการerr_tส่งคืนโดยmy_read()? หากมีข้อบกพร่องที่ใดก็ได้ในตัวอย่างนั่นก็คือ
Blrfl

1
ง่ายมากเพียงกำหนดค่าเริ่มต้นหากมีความหมาย หากไม่เป็นเช่นนั้น ฉันยอมรับได้ว่าการใช้ข้อมูล "จำลอง" เพื่อทำสิ่งนั้นไม่ดีเพราะซ่อนข้อบกพร่อง
Pieter B

1
"มีกฎอีกข้อคือ - หากการเรียกใช้โปรแกรมพบข้อผิดพลาดโปรแกรมควรจะตายโดยเร็วที่สุดไม่จำเป็นต้องทำให้มันมีชีวิตอีกต่อไปเพียงแค่เกิดความผิดพลาดเขียน crashdump มอบให้วิศวกรเพื่อทำการสอบสวน" ซอฟต์แวร์ควบคุม โชคดีที่กู้คืนการถ่ายโอนข้อมูลความผิดพลาดจากซากเครื่องบิน
Giorgio

คำตอบ:


44

การให้เหตุผลของคุณผิดพลาดในหลายบัญชี:

  1. ความผิดพลาดของการแบ่งกลุ่มอยู่ไกลจากที่แน่นอนที่จะเกิดขึ้น การใช้ผลการเตรียมตัวแปรในพฤติกรรมที่ไม่ได้กำหนด ความผิดพลาดในการแบ่งกลุ่มเป็นวิธีหนึ่งที่พฤติกรรมดังกล่าวสามารถเผยให้เห็นได้เอง แต่การปรากฏขึ้นเพื่อให้ทำงานได้ตามปกตินั้นเป็นไปได้
  2. คอมไพเลอร์จะไม่เติมหน่วยความจำที่ไม่มีการกำหนดค่าเริ่มต้นด้วยรูปแบบที่กำหนดไว้ (เช่น 0xCD) นี่คือสิ่งที่ debuggers บางคนทำเพื่อช่วยคุณในการค้นหาสถานที่ที่มีการใช้ตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้น หากคุณเรียกใช้โปรแกรมดังกล่าวนอกดีบักเกอร์ตัวแปรจะมีถังขยะแบบสุ่มสมบูรณ์ มันเป็นโอกาสที่เท่าเทียมกันเคาน์เตอร์เช่นbytes_readมีค่าเป็นว่ามันมีค่า100xcdcdcdcd
  3. แม้ว่าคุณกำลังเรียกใช้ในดีบักเกอร์ที่ตั้งค่าหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นให้เป็นรูปแบบคงที่พวกเขาจะทำเช่นนั้นเมื่อเริ่มต้นเท่านั้น ซึ่งหมายความว่ากลไกนี้ทำงานได้อย่างน่าเชื่อถือสำหรับตัวแปรสแตติก (และอาจจัดสรรฮีป) เท่านั้น สำหรับตัวแปรอัตโนมัติซึ่งได้รับการจัดสรรในสแต็กหรือมีชีวิตอยู่ในการลงทะเบียนเท่านั้นโอกาสสูงที่ตัวแปรจะถูกเก็บไว้ในตำแหน่งที่เคยใช้มาก่อนดังนั้นรูปแบบหน่วยความจำแบบบอกเล่าได้ถูกเขียนทับแล้ว

แนวคิดเบื้องหลังคำแนะนำในการเริ่มต้นตัวแปรเสมอคือเปิดใช้งานสถานการณ์ทั้งสองนี้

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

  2. ตัวแปรมีค่าที่กำหนดซึ่งคุณสามารถทดสอบได้ในภายหลังเพื่อบอกว่าฟังก์ชั่นที่ชอบmy_readมีการอัพเดทค่าหรือไม่ หากไม่มีการเริ่มต้นคุณจะไม่สามารถบอกได้ว่าbytes_readจริงๆแล้วมีค่าที่ถูกต้องหรือไม่เพราะคุณไม่สามารถรู้ได้ว่ามูลค่านั้นเริ่มต้นด้วยอะไร


8
1) ทุกอย่างเกี่ยวกับความน่าจะเป็นเช่น 1% เทียบกับ 99% 2 และ 3) VC ++ สร้างรหัสการเริ่มต้นเช่นสำหรับตัวแปรในเครื่องเช่นกัน 3) ตัวแปรสแตติก (โกลบอล) จะถูกกำหนดค่าเริ่มต้นด้วย 0
Abyx

5
@Abyx: 1) จากประสบการณ์ของฉันน่าจะเป็น ~ 80% "ไม่เห็นความแตกต่างของพฤติกรรม" ทันที 10% "ทำสิ่งที่ผิด", 10% "segfault" สำหรับ (2) และ (3): VC ++ ทำสิ่งนี้เฉพาะในการสร้างการดีบัก การใช้สิ่งนี้เป็นความคิดที่แย่มากเพราะมันเลือกที่จะปล่อยสิ่งที่สร้างขึ้นและจะไม่ปรากฏในการทดสอบของคุณ
Christian Aichinger

8
ฉันคิดว่า "แนวคิดเบื้องหลังคำแนะนำ" เป็นส่วนที่สำคัญที่สุดของคำตอบนี้ คำแนะนำนั้นไม่ได้บอกให้คุณทำตามการประกาศตัวแปรทุก= 0;อย่าง ความตั้งใจของคำแนะนำคือการประกาศตัวแปร ณ จุดที่คุณจะมีค่าที่เป็นประโยชน์สำหรับมันและกำหนดค่านี้ทันที สิ่งนี้ถูกทำให้ชัดเจนในกฎต่อไปนี้ทันที ES21 และ ES22 ทั้งสามคนควรเข้าใจว่าทำงานร่วมกัน ไม่เป็นกฎที่ไม่เกี่ยวข้องกับบุคคล
GrandOpener

1
@GrandOpener อย่างแน่นอน หากไม่มีค่าที่มีความหมายในการกำหนด ณ จุดที่มีการประกาศตัวแปรขอบเขตของตัวแปรนั้นอาจผิด
Kevin Krumwiede

5
"คอมไพเลอร์ไม่เคยเติม" ไม่ควรที่จะไม่เคย ?
CodesInChaos

25

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

ให้กล่าวถึงปัญหาในแง่ของตัวอย่างของคุณ: สมมติว่าmy_readฟังก์ชั่นมีสัญญาเป็นลายลักษณ์อักษรเพื่อเริ่มต้นbytes_readภายใต้สถานการณ์ทั้งหมด แต่มันไม่ได้ในกรณีที่มีข้อผิดพลาดดังนั้นจึงเป็นความผิดพลาดอย่างน้อยสำหรับกรณีนี้ ความตั้งใจของคุณคือการใช้สภาพแวดล้อมรันไทม์เพื่อแสดงข้อผิดพลาดนั้นโดยไม่เริ่มต้นbytes_readพารามิเตอร์ก่อน ตราบใดที่คุณทราบอย่างแน่นอนว่ามีการฆ่าเชื้อที่อยู่นั้นเป็นวิธีที่เป็นไปได้ในการตรวจสอบข้อผิดพลาดดังกล่าว ในการแก้ไขข้อบกพร่องเราต้องเปลี่ยนmy_readฟังก์ชั่นภายใน

แต่มีมุมมองที่แตกต่างกันซึ่งอย่างน้อยก็ใช้ได้เช่นกัน: พฤติกรรมผิดพลาดเกิดขึ้นจากการรวมกันของการไม่เริ่มต้นbytes_readเท่านั้นและการโทรmy_readหลังจากนั้น (ด้วยความคาดหวังbytes_readจะเริ่มต้นหลังจากนั้น) นี่เป็นสถานการณ์ที่จะเกิดขึ้นบ่อยครั้งในองค์ประกอบในโลกแห่งความจริงเมื่อข้อมูลจำเพาะของฟังก์ชันเช่นเขียนmy_readไม่ชัดเจน 100% หรือแม้แต่ผิดเกี่ยวกับพฤติกรรมในกรณีที่เกิดข้อผิดพลาด อย่างไรก็ตามตราบใดที่bytes_readค่าเริ่มต้นเป็นศูนย์ก่อนที่จะมีการเรียกโปรแกรมจะทำงานแบบเดียวกับถ้าการเริ่มต้นถูกดำเนินการภายในmy_readดังนั้นมันจะทำงานอย่างถูกต้องในการรวมกันนี้ไม่มีข้อผิดพลาดในโปรแกรม

ดังนั้นคำแนะนำของฉันที่ตามมาจากนั้นคือใช้วิธีการไม่เริ่มต้นหาก

  • คุณต้องการทดสอบว่าฟังก์ชั่นหรือบล็อครหัสเริ่มต้นพารามิเตอร์ที่เฉพาะเจาะจง
  • คุณมั่นใจได้ 100% ว่าฟังก์ชันในสเตคมีสัญญาที่ผิดแน่นอนที่จะไม่กำหนดค่าให้กับพารามิเตอร์นั้น
  • คุณมั่นใจ 100% ว่าสภาพแวดล้อมสามารถจับสิ่งนี้ได้

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

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


4
นี่คือสิ่งที่ฉันคิดเมื่อฉันอ่าน มันไม่ได้กวาดสิ่งใต้พรมมันกวาดลงในถังขยะ!
corsiKa

22

เริ่มต้นตัวแปรของคุณเสมอ

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

ลองพิจารณาตัวอย่างสมมุติที่อาจเกิดขึ้นกับพนักงานสมมุติในโปรแกรมจำลองสถานการณ์ ทีมสมมุตินี้พยายามสร้างแบบจำลองที่กำหนดขึ้นเพื่อแสดงให้เห็นว่าผลิตภัณฑ์ที่พวกเขาขายตรงตามความต้องการ

ตกลงฉันจะหยุดด้วยการฉีดคำ ฉันคิดว่าคุณได้รับคะแนน ;-)

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

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

ทีมงานทั้งหมดอาจถูกระงับและใช้เวลาส่วนที่ดีกว่าของ 2 เดือนในการต่อสู้กับ codebase จำลองทั้งหมดเพื่อแก้ไขข้อผิดพลาดของค่าเริ่มต้นแทนการใช้งานและทดสอบคุณสมบัติ พนักงานไม่ต้องพูดว่า "ฉันบอกคุณแล้ว" ข้ามไปและช่วยให้นักพัฒนาซอฟต์แวร์คนอื่นเข้าใจถึงคุณค่าที่ไม่ได้กำหนดไว้ น่าแปลกที่มาตรฐานการเข้ารหัสเปลี่ยนไปไม่นานหลังจากเหตุการณ์นี้กระตุ้นให้นักพัฒนาเริ่มต้นตัวแปรของตนเสมอ

และนี่คือช็อตเตือน นี่เป็นกระสุนที่เล็มหญ้าไปทั่วจมูกของคุณ ปัญหาที่เกิดขึ้นจริงนั้นอยู่ห่างไกลยิ่งกว่าที่คุณคิด

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

หรืออาจไม่ทำอะไรเลย

นกขมิ้นของฉันในเหมืองถ่านหินสำหรับ UB เป็นกรณีจากเครื่องยนต์ SQL ที่ฉันอ่าน ยกโทษให้ฉันที่ไม่ได้เชื่อมโยงฉันล้มเหลวในการค้นหาบทความอีกครั้ง มีปัญหาบัฟเฟอร์โอเวอร์รันในเอ็นจิน SQL เมื่อคุณส่งผ่านขนาดบัฟเฟอร์ที่ใหญ่กว่าไปยังฟังก์ชันแต่ใช้กับ Debian เวอร์ชันที่ระบุเท่านั้น ข้อผิดพลาดได้รับการบันทึกตามหน้าที่และสำรวจแล้ว ส่วนที่ตลกคือ: ตรวจสอบบัฟเฟอร์ที่โอเวอร์รันแล้ว มีรหัสในการจัดการบัฟเฟอร์โอเวอร์รัน มันดูเหมือนว่า:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

ฉันได้เพิ่มความคิดเห็นเพิ่มเติมในการแปลของฉัน แต่ความคิดนั้นเหมือนกัน หากput + dataLengthล้อมรอบมันจะมีขนาดเล็กกว่าputตัวชี้ (พวกเขามีการตรวจสอบเวลารวบรวมเพื่อให้แน่ใจว่า int ที่ไม่ได้ลงชื่อคือขนาดของตัวชี้สำหรับผู้อยากรู้อยากเห็น) หากสิ่งนี้เกิดขึ้นเรารู้ว่าอัลกอริธึมบัฟเฟอร์มาตรฐานอาจทำให้เกิดความสับสนโดยโอเวอร์โฟลว์นี้ดังนั้นเรากลับ 0 หรือเรา?

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

ในกรณีที่มีข้อผิดพลาดนี้, Debian ที่เกิดขึ้นในการเลือกที่จะใช้รุ่นใหม่ของ GCC ว่าไม่มีของอื่น ๆ รสชาติลินุกซ์ที่สำคัญมีการปรับปรุงเพื่อให้ในรุ่นการผลิตของตน gcc รุ่นใหม่นี้มีเครื่องมือเพิ่มประสิทธิภาพโค้ดตายที่ก้าวร้าวมากขึ้น คอมไพเลอร์เห็นพฤติกรรมที่ไม่ได้กำหนดและตัดสินใจว่าผลลัพธ์ของifข้อความจะเป็น "สิ่งใดก็ตามที่ทำให้การเพิ่มประสิทธิภาพของโค้ดดีที่สุด" ซึ่งเป็นการแปลที่ถูกกฎหมายอย่างแท้จริงของ UB ดังนั้นจึงสันนิษฐานว่าเนื่องจากptr+dataLengthไม่สามารถอยู่ด้านล่างptrหากไม่มีตัวชี้ล้นของ UB ifคำสั่งจะไม่เรียกใช้และปรับการตรวจสอบบัฟเฟอร์ที่เกินขอบเขตให้เหมาะสม

การใช้ "sane" UB จริง ๆ แล้วทำให้ผลิตภัณฑ์ SQL ที่สำคัญมีบัฟเฟอร์ overrun ใช้ประโยชน์จากมันมีการเขียนรหัสเพื่อหลีกเลี่ยง!

อย่าพึ่งพาพฤติกรรมที่ไม่ได้กำหนด เคย


สำหรับการอ่านอย่างไม่น่าพึงพอใจเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดไว้software.intel.com/th-th/blogs/2013/01/06/…เป็นบทความที่เขียนขึ้นอย่างน่าประหลาดใจเกี่ยวกับความเลวร้ายที่มันจะเกิดขึ้น อย่างไรก็ตามโพสต์นั้นอยู่ในการดำเนินการปรมาณูซึ่งสร้างความสับสนให้มากที่สุดดังนั้นฉันจึงหลีกเลี่ยงการแนะนำให้เป็นไพรเมอร์สำหรับ UB และวิธีการที่มันผิดไป
Cort Ammon

1
ฉันต้องการให้ C มีอินทรินซิลิตี้เพื่อตั้งค่า lvalue หรืออาเรย์ของพวกมันให้เป็นค่าเริ่มต้นที่ไม่กำหนดค่าไม่ดักหรือค่าที่ไม่ระบุหรือเปลี่ยนค่าที่น่ารังเกียจเป็นค่าที่ไม่น่ารังเกียจ คอมไพเลอร์สามารถใช้คำสั่งดังกล่าวเพื่อช่วยเพิ่มประสิทธิภาพที่มีประโยชน์และโปรแกรมเมอร์สามารถใช้พวกเขาเพื่อหลีกเลี่ยงการเขียนโค้ดที่ไม่มีประโยชน์ในขณะที่ปิดกั้นการ "เพิ่มประสิทธิภาพ" เมื่อใช้สิ่งต่าง ๆ เช่นเทคนิค sparse-matrix
supercat

@supercat มันจะเป็นคุณสมบัติที่ดีสมมติว่าคุณกำลังกำหนดเป้าหมายแพลตฟอร์มที่เป็นโซลูชันที่ถูกต้อง หนึ่งในตัวอย่างของปัญหาที่ทราบคือความสามารถในการสร้างรูปแบบหน่วยความจำซึ่งไม่เพียง แต่ไม่ถูกต้องสำหรับประเภทหน่วยความจำ แต่เป็นไปไม่ได้ที่จะบรรลุด้วยวิธีการทั่วไป boolเป็นตัวอย่างที่ยอดเยี่ยมที่มีปัญหาชัดเจน แต่พวกเขาปรากฏตัวที่อื่นยกเว้นคุณคิดว่าคุณกำลังทำงานบนแพลตฟอร์มที่มีประโยชน์มากเช่น x86 หรือ ARM หรือ MIPS ซึ่งปัญหาเหล่านี้ทั้งหมดได้รับการแก้ไขในเวลาที่ opcode
Cort Ammon

พิจารณากรณีที่เครื่องมือเพิ่มประสิทธิภาพสามารถพิสูจน์ได้ว่าค่าที่ใช้สำหรับ a switchน้อยกว่า 8 เนื่องจากขนาดของเลขคณิตเลขจำนวนเต็มดังนั้นพวกเขาจึงสามารถใช้คำแนะนำอย่างรวดเร็วซึ่งสันนิษฐานว่าไม่มีความเสี่ยงของค่า "ใหญ่" เข้ามาทันใดนั้น ค่าที่ไม่ระบุ (ซึ่งไม่สามารถสร้างขึ้นได้โดยใช้กฎของคอมไพเลอร์) จะปรากฏขึ้นทำสิ่งที่ไม่คาดคิดและทันใดนั้นคุณก็กระโดดใหญ่จากจุดสิ้นสุดของตารางกระโดด การอนุญาตผลลัพธ์ที่ไม่ระบุที่นี่หมายถึงคำสั่ง switch ทุกคำสั่งในโปรแกรมจะต้องมีกับดักพิเศษเพื่อรองรับกรณีเหล่านี้ที่อาจ "ไม่เคยเกิดขึ้น"
Cort Ammon

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

5

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

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

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


5

ไม่มันไม่ได้ซ่อนข้อบกพร่อง แต่จะทำให้พฤติกรรมที่กำหนดไว้ในลักษณะที่ว่าหากผู้ใช้พบข้อผิดพลาดนักพัฒนาสามารถทำซ้ำได้


1
และการเริ่มต้นด้วย -1 นั้นมีความหมายจริงๆ โดยที่ "int bytes_read = 0" ไม่ดีเพราะคุณสามารถอ่าน 0 ไบต์ได้จริงการเริ่มต้นด้วย -1 ทำให้ชัดเจนมากไม่พยายามอ่านไบต์สำเร็จและคุณสามารถทดสอบได้
Pieter B

4

TL; DR: มีสองวิธีในการทำให้โปรแกรมนี้ถูกต้องเริ่มต้นตัวแปรของคุณและอธิษฐาน มีเพียงรายการเดียวที่ให้ผลลัพธ์อย่างสม่ำเสมอ


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

หากคุณไม่ต้องการอ่านบทความเหล่านั้น TL; DR คือ:

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

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

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

ฉันพบตัวอย่างในตอนที่ 2 ด้านบนโดดเด่น:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

เปลี่ยนเป็น:

void contains_null_check(int *P) {
  *P = 4;
}

เพราะเห็นได้ชัดว่าPไม่สามารถทำได้0เนื่องจากไม่ได้รับการยืนยันก่อนที่จะถูกตรวจสอบ


สิ่งนี้มีผลกับตัวอย่างของคุณอย่างไร

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

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

ให้เราจินตนาการว่าความหมายของmy_readคือ:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

และดำเนินการต่อไปตามที่คาดหวังของคอมไพเลอร์ที่ดีพร้อมอินไลน์:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

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

  1. ไม่ควรใช้ตัวแปรใด ๆ
  2. bytes_readจะถูกใช้โดยไม่กำหนดค่าเริ่มต้นหากresultไม่ใช่0
  3. นักพัฒนามีแนวโน้มที่resultจะไม่เป็น0!

ดังนั้นresultไม่เคย0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

โอ้resultไม่เคยใช้:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

โอ้เราสามารถเลื่อนการประกาศของbytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

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

ฉันได้รับการลงที่ถนนเข้าใจปัญหาเมื่อพฤติกรรมที่คาดหวังและการชุมนุมไม่ตรงกันจริงๆไม่สนุก


บางครั้งฉันคิดว่าคอมไพเลอร์ควรให้โปรแกรมลบไฟล์ต้นฉบับเมื่อพวกเขาเรียกใช้พา ธ UB โปรแกรมเมอร์จะเรียนรู้ว่า UB มีความหมายอย่างไรต่อผู้ใช้ปลายทางของพวกเขา ....
mattnz

1

ลองดูโค้ดตัวอย่างของคุณให้ละเอียดยิ่งขึ้น:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

นี่เป็นตัวอย่างที่ดี หากเราคาดว่าจะเกิดข้อผิดพลาดเช่นนี้เราสามารถแทรกบรรทัดassert(bytes_read > 0);และตรวจจับข้อผิดพลาดนี้ที่รันไทม์ซึ่งเป็นไปไม่ได้กับตัวแปรที่ไม่กำหนดค่าเริ่มต้น

use(buffer)แต่สมมติว่าเราทำไม่ได้และเราพบว่ามีข้อผิดพลาดในฟังก์ชัน เราโหลดโปรแกรมขึ้นในตัวดีบั๊กตรวจสอบ backtrace และพบว่ามันถูกเรียกจากรหัสนี้ ดังนั้นเราจึงวางเบรกพอยต์ที่ด้านบนของตัวอย่างนี้ทำงานอีกครั้งและสร้างข้อผิดพลาด เราก้าวผ่านการพยายามที่จะจับมัน

หากเราไม่ได้เริ่มต้นbytes_readจะมีขยะ ไม่จำเป็นต้องมีขยะเหมือนกันทุกครั้ง my_read(buffer, &bytes_read);เรามีขั้นตอนที่ผ่านมาสาย ตอนนี้ถ้ามันเป็นค่าที่แตกต่างกว่าก่อนหน้านี้เราอาจไม่สามารถทำซ้ำข้อบกพร่องของเราได้เลย! อาจใช้งานได้ในครั้งต่อไปในอินพุตเดียวกันโดยไม่ได้ตั้งใจ ถ้ามันเป็นศูนย์อย่างสม่ำเสมอเราจะได้รับพฤติกรรมที่สอดคล้องกัน

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

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


1

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

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

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


0

คำตอบสำหรับคำถามของคุณจะต้องแบ่งออกเป็นตัวแปรประเภทต่าง ๆ ที่ปรากฏในโปรแกรม:


ตัวแปรท้องถิ่น

โดยปกติการประกาศควรอยู่ที่จุดที่ตัวแปรแรกได้รับค่าของมัน อย่าประกาศตัวแปรล่วงหน้าเหมือนในแบบเก่า C:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

สิ่งนี้จะลบความจำเป็นในการเริ่มต้นได้ถึง 99% ตัวแปรมีค่าสุดท้ายจากการปิด ข้อยกเว้นบางประการที่การเริ่มต้นขึ้นอยู่กับเงื่อนไขบางอย่าง:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

ฉันเชื่อว่าเป็นความคิดที่ดีที่จะเขียนกรณีเหล่านี้:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e. ยืนยันอย่างชัดเจนว่ามีการดำเนินการกำหนดค่าเริ่มต้นที่เหมาะสมสำหรับตัวแปรของคุณ


ตัวแปรสมาชิก

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


บัฟเฟอร์

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

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

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

ฉันขอแนะนำอย่างยิ่งให้เพิ่มเป้าหมายในระบบการสร้างของคุณที่รัน testuite ทั้งหมดภายใต้valgrindหรือเครื่องมือที่คล้ายกันเพื่อแสดงข้อบกพร่องการใช้งานก่อนเริ่มต้นและการรั่วไหลของหน่วยความจำ สิ่งนี้มีค่ามากกว่าการกำหนดค่าเริ่มต้นล่วงหน้าของตัวแปรทั้งหมด ว่าvalgrindเป้าหมายควรจะดำเนินการเป็นประจำที่สำคัญที่สุดก่อนรหัสใด ๆ ไปที่สาธารณะ


ตัวแปรทั่วโลก

คุณไม่สามารถมีตัวแปรทั่วโลกที่ไม่ได้เริ่มต้น (อย่างน้อยใน C / C ++ ฯลฯ ) ดังนั้นตรวจสอบให้แน่ใจว่าการเริ่มต้นนี้เป็นสิ่งที่คุณต้องการ


สังเกตว่าคุณสามารถเขียนการกำหนดค่าเริ่มต้นตามเงื่อนไขด้วยตัวดำเนินการแบบไตรภาคเช่น Base& b = foo() ? new Derived1 : new Derived2;
Davislor

@Lorehead ที่อาจใช้งานได้กับกรณีง่าย ๆ แต่มันจะไม่ทำงานสำหรับกรณีที่ซับซ้อนมากขึ้น: คุณไม่ต้องการทำสิ่งนี้หากคุณมีสามกรณีขึ้นไปและ constructors ของคุณมีอาร์กิวเมนต์สามข้อขึ้นไปเพื่อให้อ่านง่าย เหตุผล และนั่นก็ไม่ได้พิจารณาการคำนวณใด ๆ ที่อาจต้องทำเช่นค้นหาอาร์กิวเมนต์สำหรับการเริ่มต้นหนึ่งสาขาในลูป
cmaster

Base &b = base_factory(which);สำหรับกรณีที่มีความซับซ้อนมากขึ้นคุณสามารถห่อรหัสการเริ่มต้นในการทำงานโรงงาน: สิ่งนี้มีประโยชน์มากที่สุดถ้าคุณต้องการเรียกรหัสมากกว่าหนึ่งครั้งหรือถ้ามันให้คุณทำให้ผลลัพธ์คงที่
Davislor

@Lorehead นั่นเป็นความจริงและแน่นอนว่าจะต้องทำอย่างไรถ้าตรรกะที่ต้องการนั้นไม่ใช่เรื่องง่าย อย่างไรก็ตามฉันเชื่อว่ามีพื้นที่สีเทาขนาดเล็กในระหว่างที่การเริ่มต้นผ่าน?:เป็น PITA และฟังก์ชั่นโรงงานยังคงเกินความจริง กรณีเหล่านี้มีอยู่น้อยและอยู่ห่างไกลกัน แต่มีอยู่จริง
cmaster

-2

คอมไพเลอร์ C, C ++ หรือ Objective-C ที่เหมาะสมพร้อมชุดตัวเลือกคอมไพเลอร์ที่เหมาะสมจะบอกคุณ ณ เวลารวบรวมหากมีการใช้ตัวแปรก่อนที่จะมีการตั้งค่า เนื่องจากในภาษาเหล่านี้ที่ใช้ค่าของตัวแปร uninitialised เป็นพฤติกรรมที่ไม่ได้กำหนด "การตั้งค่าก่อนที่คุณจะใช้" ไม่ใช่คำใบ้หรือแนวทางหรือแนวปฏิบัติที่ดีมันเป็นข้อกำหนด 100% มิฉะนั้นโปรแกรมของคุณจะเสียอย่างแน่นอน ในภาษาอื่น ๆ เช่น Java และ Swift คอมไพเลอร์จะไม่อนุญาตให้คุณใช้ตัวแปรก่อนที่จะเริ่มต้น

มีความแตกต่างทางตรรกะระหว่าง "initialise" และ "set a value" ถ้าฉันต้องการค้นหาอัตราการแปลงระหว่างดอลลาร์และยูโรและเขียน "double rate = 0.0;" ดังนั้นตัวแปรมีชุดค่า แต่ไม่ได้กำหนดค่าเริ่มต้น 0.0 ที่เก็บไว้ที่นี่ไม่มีอะไรเกี่ยวข้องกับผลลัพธ์ที่ถูกต้อง ในสถานการณ์นี้ถ้าเป็นเพราะข้อผิดพลาดที่คุณไม่เคยเก็บอัตราการแปลงที่ถูกต้องคอมไพเลอร์ไม่มีโอกาสที่จะบอกคุณ หากคุณเพิ่งเขียน "double rate;" และไม่เคยเก็บอัตราการแปลงที่มีความหมายไว้คอมไพเลอร์จะบอกคุณ

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

อย่าเริ่มต้นตัวแปรเพียงเพราะคอมไพเลอร์อาจบอกคุณว่ามันถูกใช้โดยไม่ต้องเริ่มต้น คุณกำลังซ่อนปัญหาอีกครั้ง

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

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

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


สิ่งนี้ฟังดูดีมาก แต่ต้องอาศัยความถูกต้องของการเตือนค่าที่ไม่ได้กำหนดค่ามากเกินไป การได้รับสิ่งที่ถูกต้องอย่างสมบูรณ์เหล่านี้เทียบเท่ากับปัญหาการหยุดชะงักและผู้ผลิตคอมไพเลอร์สามารถและต้องประสบกับการปฏิเสธที่ผิดพลาด (เช่นพวกเขาไม่ได้วิเคราะห์ตัวแปรที่ไม่เตรียมการ ดูตัวอย่างข้อผิดพลาดของ GCC 18501ซึ่งไม่ได้รวมกันมานานกว่าสิบปีแล้ว
zwol

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

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