เป็นวิธีที่ดีในการ NULL ตัวชี้หลังจากลบหรือไม่


150

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

รหัสต่อไปนี้มีปัญหาอะไร?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

นี่เป็นจุดประกายโดยคำตอบและแสดงความคิดเห็นต่อคำถามอื่น หนึ่งความคิดเห็นจากNeil Butterworthสร้าง upvotes ไม่กี่:

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

มีสถานการณ์มากมายที่ไม่สามารถช่วยได้ แต่จากประสบการณ์ของฉันมันไม่สามารถทำร้ายได้ ใครบางคนสอนฉัน


6
@Andre: ในทางเทคนิคมันไม่ได้กำหนด สิ่งที่น่าจะเกิดขึ้นคือคุณเข้าถึงหน่วยความจำแบบเดิม แต่ตอนนี้อาจมีการใช้อย่างอื่น หากคุณลบหน่วยความจำสองครั้งอาจทำให้การรันโปรแกรมของคุณผิดพลาดได้ยาก มันปลอดภัยสำหรับdeleteตัวชี้โมฆะซึ่งเป็นเหตุผลหนึ่งที่ทำให้ตัวชี้เป็นศูนย์ได้ดี
David Thornley

5
@ André Pena มันไม่ได้กำหนด บ่อยครั้งที่มันไม่สามารถทำซ้ำได้ คุณตั้งค่าตัวชี้เป็น NULL เพื่อทำให้ข้อผิดพลาดปรากฏขึ้นเมื่อทำการดีบักและอาจทำให้ซ้ำได้อีก
Mark Ransom

3
@ André: ไม่มีใครรู้ มันเป็นพฤติกรรมที่ไม่ได้กำหนด อาจมีปัญหากับการละเมิดการเข้าถึงหรืออาจเขียนทับหน่วยความจำที่ใช้โดยแอปพลิเคชันที่เหลือ มาตรฐานภาษาไม่รับประกันว่าจะเกิดอะไรขึ้นดังนั้นคุณจึงไม่สามารถเชื่อถือใบสมัครของคุณได้เมื่อมันเกิดขึ้น มันอาจยิงขีปนาวุธนิวเคลียร์หรือฟอร์แมตฮาร์ดไดรฟ์ของคุณ มันอาจทำให้หน่วยความจำของแอปเสียหายหรืออาจทำให้ปีศาจบินออกจากจมูกของคุณ การเดิมพันทั้งหมดจะปิด
jalf

16
ปีศาจบินได้เป็นคุณสมบัติไม่ใช่ข้อผิดพลาด
jball

3
คำถามนี้ไม่ซ้ำกันเนื่องจากคำถามอื่นเกี่ยวกับ C และคำถามนี้เกี่ยวกับ C ++ คำตอบมากมายขึ้นอยู่กับสิ่งต่างๆเช่นตัวชี้อัจฉริยะซึ่งไม่มีใน C ++
Adrian McCarthy

คำตอบ:


86

การตั้งตัวชี้เป็น 0 (ซึ่งเป็น "โมฆะ" ในมาตรฐาน C ++, ค่า NULL ที่กำหนดจาก C จะค่อนข้างแตกต่างกัน) หลีกเลี่ยงการขัดข้องในการลบสองครั้ง

พิจารณาสิ่งต่อไปนี้:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

โดย:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

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

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

ในที่สุด sidenote เกี่ยวกับการจัดการการจัดสรรวัตถุฉันขอแนะนำให้คุณดูstd::unique_ptrความเป็นเจ้าของที่เข้มงวด / เอกพจน์std::shared_ptrสำหรับการเป็นเจ้าของที่ใช้ร่วมกันหรือการใช้งานตัวชี้สมาร์ทอื่นขึ้นอยู่กับความต้องการของคุณ


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

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

11
เป็นความจริงที่การตั้งค่าตัวชี้NULLสามารถปกปิดข้อผิดพลาดในการลบซ้ำได้ (บางคนอาจคิดว่ามาสก์ตัวนี้เป็นวิธีแก้ปัญหา - แต่ก็ไม่ได้ดีมากเพราะมันไม่ได้เป็นต้นเหตุของปัญหา) แต่อย่าตั้งค่าเป็นมาสก์ NULL ไกล (ไกล) ปัญหาทั่วไปของการเข้าถึงข้อมูลหลังจากที่ถูกลบไปแล้ว
Adrian McCarthy

AFAIK, มาตรฐาน :: auto_ptr ได้รับการคัดค้านในคที่จะเกิดขึ้น ++ มาตรฐาน
rafak

ฉันจะไม่บอกว่าเลิกใช้แล้วมันทำให้ดูเหมือนว่าความคิดหายไป ค่อนข้างจะถูกแทนที่ด้วยunique_ptrซึ่งทำในสิ่งที่auto_ptrพยายามจะทำด้วยความหมายย้าย
GManNickG

55

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

  • คุณเพียงต้องการบางสิ่งที่จัดสรรไว้บนกอง ในกรณีนี้การห่อไว้ในวัตถุ RAII นั้นปลอดภัยและสะอาดกว่ามาก จบขอบเขตของวัตถุ RAII เมื่อคุณไม่ต้องการวัตถุอีกต่อไป นั่นเป็นวิธีstd::vectorทำงานและมันจะแก้ปัญหาของการออกจากพอยน์เตอร์โดยไม่ตั้งใจไปยังหน่วยความจำที่จัดสรรคืน ไม่มีพอยน์เตอร์
  • หรือบางทีคุณต้องการความหมายของความเป็นเจ้าของร่วมที่ซับซ้อน ตัวชี้ที่ส่งคืนจากnewอาจไม่เหมือนกับตัวชี้ที่deleteเรียกใช้ วัตถุหลายรายการอาจใช้วัตถุพร้อมกันในเวลาเดียวกัน ในกรณีนั้นตัวชี้ที่ใช้ร่วมกันหรือบางสิ่งที่คล้ายกันน่าจะเป็นที่นิยมกว่า

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


15
ดังนั้นคุณกำลังโต้แย้งว่าไม่ควรมีตัวชี้แบบดิบมาก่อนและสิ่งใดที่เกี่ยวข้องกับตัวชี้ดังกล่าวไม่ควรได้รับพรด้วยคำว่า "แนวปฏิบัติที่ดี"? ยุติธรรมพอสมควร
Mark Ransom

7
ไม่มากก็น้อย ฉันจะไม่พูดว่าไม่มีสิ่งใดที่เกี่ยวข้องกับตัวชี้แบบดิบสามารถเรียกได้ว่าเป็นการปฏิบัติที่ดี เพียงแค่ว่ามันเป็นข้อยกเว้นมากกว่ากฎ โดยปกติแล้วการมีตัวชี้เป็นตัวบ่งชี้ว่ามีบางอย่างผิดปกติในระดับที่ลึกกว่า
jalf

3
แต่เพื่อตอบคำถามทันทีไม่ฉันไม่เห็นว่าการตั้งค่าพอยน์เตอร์เป็นโมฆะอาจทำให้เกิดข้อผิดพลาดได้อย่างไร
jalf

7
ฉันไม่เห็นด้วย - มีหลายกรณีที่ตัวชี้ใช้งานได้ดี ตัวอย่างเช่นมี 2 ตัวแปรในสแต็กและคุณต้องการเลือกหนึ่งในนั้น หรือคุณต้องการส่งผ่านตัวแปรเสริมไปยังฟังก์ชัน newผมจะบอกว่าคุณไม่ควรใช้เป็นตัวชี้ดิบร่วมกับ
rlbond

4
เมื่อตัวชี้ไม่อยู่ในขอบเขตฉันไม่เห็นว่าจะมีอะไรหรือใครก็ตามที่ต้องจัดการกับมัน
jalf

43

ฉันมีแนวปฏิบัติที่ดียิ่งขึ้น: ถ้าเป็นไปได้ยุติขอบเขตของตัวแปร!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

17
ใช่ RAII เป็นเพื่อนของคุณ ห่อไว้ในชั้นเรียนและมันจะง่ายขึ้นกว่าเดิม หรือไม่จัดการหน่วยความจำด้วยตัวคุณเองโดยใช้ STL!
Brian

24
ใช่แน่นอนว่าเป็นตัวเลือกที่ดีที่สุด ไม่ตอบคำถาม
Mark Ransom

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

2
ลองคิดดูถ้าคุณสามารถทำสิ่งนี้ได้ทำไมคุณจะไม่ลืมฮีปและดึงหน่วยความจำทั้งหมดออกจากสแต็ก
San Jacinto

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

31

ฉันตั้งค่าตัวชี้เป็นNULL(ตอนนี้nullptr) เสมอหลังจากลบวัตถุที่ชี้ไป

  1. มันสามารถช่วยในการตรวจสอบการอ้างอิงจำนวนมากไปยังหน่วยความจำที่เพิ่มขึ้น

  2. มันจะไม่จับการอ้างอิงทั้งหมดไปยังหน่วยความจำที่ว่างถ้าคุณมีสำเนาของตัวชี้วางอยู่รอบ ๆ แต่บางคนก็ยังดีกว่าไม่มีเลย

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

  4. ในหลายกรณีคอมไพเลอร์จะทำการปรับให้เหมาะสม ดังนั้นการโต้แย้งว่ามันไม่จำเป็นไม่ได้ชักชวนฉัน

  5. หากคุณใช้ RAII อยู่แล้วก็มีไม่มากdeleteในรหัสของคุณที่จะเริ่มต้นด้วยดังนั้นการโต้แย้งว่าการมอบหมายพิเศษทำให้เกิดความยุ่งเหยิงไม่ได้ชักชวนฉัน

  6. บ่อยครั้งที่สะดวกเมื่อทำการดีบั๊กเพื่อดูค่า Null แทนที่จะเป็นตัวชี้ค้าง

  7. หากสิ่งนี้ยังรบกวนคุณอยู่ให้ใช้ตัวชี้สมาร์ทหรือการอ้างอิงแทน

ฉันยังตั้งค่าการจัดการทรัพยากรประเภทอื่น ๆ ให้เป็นค่าที่ไม่มีทรัพยากรเมื่อทรัพยากรนั้นว่าง (ซึ่งโดยทั่วไปจะอยู่ใน destructor ของ Wrapper ของ RAII ที่เขียนเพื่อห่อหุ้มทรัพยากร)

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

อัปเดต: Microsoft เชื่อว่าเป็นวิธีปฏิบัติที่ดีสำหรับความปลอดภัยและแนะนำการปฏิบัติในนโยบาย SDL ของพวกเขา Apparently MSVC ++ 11 จะเหยียบตัวชี้ที่ถูกลบโดยอัตโนมัติ (ในหลาย ๆ สถานการณ์) หากคุณคอมไพล์ด้วยตัวเลือก / SDL


12

ประการแรกมีคำถามที่มีอยู่มากมายในหัวข้อนี้และหัวข้อที่เกี่ยวข้องอย่างใกล้ชิดตัวอย่างเช่นเหตุใดจึงไม่ลบการตั้งค่าตัวชี้เป็น NULL.

ในรหัสของคุณปัญหาสิ่งที่เกิดขึ้นใน (ใช้ p) ตัวอย่างเช่นหากบางแห่งคุณมีรหัสเช่นนี้:

Foo * p2 = p;

จากนั้นตั้งค่า p เป็น NULL ให้ผลน้อยมากเนื่องจากคุณยังมีตัวชี้ p2 ที่ต้องกังวล

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


1
ฉันยอมรับว่ามีบางครั้งที่มันไม่ช่วย แต่คุณดูเหมือนจะบ่งบอกว่ามันอาจเป็นอันตรายได้ นั่นเป็นความตั้งใจของคุณหรือว่าฉันอ่านผิด
Mark Ransom

1
ไม่ว่าจะมีสำเนาของตัวชี้ sa ที่ไม่เกี่ยวข้องกับคำถามว่าควรจะตั้งค่าตัวแปรตัวชี้เป็น NULL หรือไม่ การตั้งค่าเป็น NULL เป็นวิธีปฏิบัติที่ดีในบริเวณเดียวกันการทำความสะอาดอาหารหลังจากเสร็จสิ้นการรับประทานอาหารค่ำเป็นวิธีปฏิบัติที่ดี - ในขณะที่มันไม่ได้ป้องกันข้อผิดพลาดทั้งหมดที่รหัสสามารถมีได้
Franci Penov

2
@ ฟรานซิผู้คนจำนวนมากดูเหมือนจะไม่เห็นด้วยกับคุณ และไม่ว่าจะมีสำเนาเกี่ยวข้องหรือไม่หากคุณพยายามใช้สำเนาหลังจากลบต้นฉบับ

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

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

7

หากมีรหัสเพิ่มเติมหลังจากdeleteใช่ เมื่อตัวชี้ถูกลบในนวกรรมิกหรือท้ายเมธอดหรือฟังก์ชันเลขที่

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

แนวทางปฏิบัติที่ดียิ่งขึ้นคือการใช้ Smart Pointers (แบ่งใช้หรือกำหนดขอบเขต) ซึ่งลบวัตถุเป้าหมายโดยอัตโนมัติ


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

3

อย่างที่คนอื่น ๆ พูดกันว่าdelete ptr; ptr = 0;จะไม่ทำให้ปีศาจบินออกจากจมูกคุณ อย่างไรก็ตามมันสนับสนุนให้ใช้ptrเป็นธงแปลก ๆ รหัสกลายเป็นที่เกลื่อนไปด้วยและการตั้งค่าตัวชี้ไปยังdelete NULLขั้นตอนต่อไปคือการกระจายif (arg == NULL) return;รหัสของคุณเพื่อป้องกันการใช้งานNULLตัวชี้โดยไม่ตั้งใจ ปัญหาเกิดขึ้นเมื่อการตรวจสอบกับNULLวิธีการหลักของคุณในการตรวจสอบสถานะของวัตถุหรือโปรแกรม

ฉันแน่ใจว่ามีรหัสกลิ่นเกี่ยวกับการใช้ตัวชี้เป็นธงที่ไหนสักแห่ง แต่ฉันไม่พบมัน


9
ไม่มีอะไรผิดปกติกับการใช้ตัวชี้เป็นธง หากคุณกำลังใช้ตัวชี้และNULLไม่ใช่ค่าที่ถูกต้องคุณควรใช้การอ้างอิงแทน
Adrian McCarthy

2

ฉันจะเปลี่ยนคำถามของคุณเล็กน้อย:

คุณจะใช้ตัวชี้เริ่มต้นหรือไม่? คุณรู้หรือไม่ว่าคุณไม่ได้ตั้งค่าเป็น NULL หรือจัดสรรหน่วยความจำที่ชี้ไป

มีสองสถานการณ์ที่การตั้งค่าตัวชี้เป็น NULL สามารถข้ามได้:

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

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


(A) "ดูเหมือนว่าคุณไม่ควรแก้ไขข้อผิดพลาด" การตั้งค่าตัวชี้เป็น NULL ไม่ใช่ข้อผิดพลาด (B) "แต่การตั้งค่าให้เป็นโมฆะจริงจะทำให้เกิดข้อผิดพลาดตรงเดียวกัน" ครั้งที่การตั้งกลองโมฆะสองเท่าลบ (C) สรุป: การตั้งค่าเป็น NULL ซ่อนการลบสองครั้ง แต่จะเปิดเผยการอ้างอิงที่เก่า การไม่ตั้งค่าเป็น NULL สามารถซ่อนการอ้างอิงเก่า ๆ แต่จะลบการลบสองครั้ง ทั้งสองฝ่ายเห็นพ้องกันว่าปัญหาที่แท้จริงคือการแก้ไขการอ้างอิงเก่าและลบสองครั้ง
Mooing Duck

2

หากคุณไม่มีข้อ จำกัด อื่นใดที่บังคับให้คุณตั้งค่าหรือไม่ตั้งค่าตัวชี้เป็น NULL หลังจากคุณลบ (ข้อ จำกัด ดังกล่าวถูกกล่าวถึงโดยNeil Butterworth ) การตั้งค่าส่วนตัวของฉันคือปล่อยไว้

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

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


2

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

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

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

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

นั่นคือทารกในน้ำ C ++ ทุกตัวไม่ควรโยนทิ้ง :)


2

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

มีข้อโต้แย้งหลายประการเกี่ยวกับการมอบหมาย0แนะนำว่าทำได้ซ่อนข้อบกพร่องหรือควบคุมการไหลที่ซับซ้อนได้ โดยพื้นฐานแล้วนั่นเป็นข้อผิดพลาดต้นน้ำ (ไม่ใช่ความผิดของคุณ (ขออภัยในความผิดพลาด pun)) หรือข้อผิดพลาดอื่น ๆ ในนามของโปรแกรมเมอร์ - บางทีแม้แต่ข้อบ่งชี้ว่าการไหลของโปรแกรมนั้นซับซ้อนเกินไป

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

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

จากที่นั่นคุณมีขอบเขตและบริบทที่ จำกัด มากซึ่งอาจมีตัวชี้ว่าง (หรืออนุญาต)

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

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


1

ให้ฉันขยายสิ่งที่คุณใส่ไว้ในคำถามของคุณ

นี่คือสิ่งที่คุณใส่ไว้ในคำถามของคุณในรูปแบบหัวข้อย่อย:


การตั้งค่าพอยน์เตอร์เป็น NULL หลังการลบไม่ใช่แนวปฏิบัติสากลที่ดีใน C ++ มีหลายครั้งที่:

  • มันเป็นสิ่งที่ดีที่จะทำ
  • และเวลาที่ไม่มีจุดหมายและสามารถซ่อนข้อผิดพลาด

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

ดังนั้นหากมีข้อสงสัย

ต้องบอกว่าถ้าคุณรู้สึกว่าคุณต้องโมฆะตัวชี้บางอย่างชัดเจนแล้วสำหรับฉันนี้ดูเหมือนว่าคุณยังไม่ได้แยกวิธีพอและควรดูวิธีการ refactoring เรียกว่า "วิธีการแยก" เพื่อแยกวิธีการเป็น แยกชิ้นส่วน


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

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

@Carson: ประสบการณ์ของฉันค่อนข้างตรงกันข้าม: การยกเลิก nullptr จะทำให้แอปพลิเคชั่นเกือบทั้งหมดขัดข้องและสามารถแก้ไขได้โดยดีบักเกอร์ การอ้างอิงตัวชี้ห้อยมักจะไม่สร้างปัญหาทันที แต่มักจะนำไปสู่ผลลัพธ์ที่ไม่ถูกต้องหรือข้อผิดพลาดอื่น ๆ ในบรรทัด
MikeMB

@MikeMB ฉันเห็นด้วยอย่างสมบูรณ์มุมมองของฉันเกี่ยวกับเรื่องนี้ได้เปลี่ยนไปอย่างมากในอดีต ~ 6.5 ปี
Carson Myers

ในแง่ของการเป็นโปรแกรมเมอร์เราทุกคนเป็นคนอื่นเมื่อ 6-7 ปีที่แล้ว :) ฉันไม่แน่ใจด้วยซ้ำว่าฉันจะกล้าตอบคำถาม C / C ++ ในวันนี้ :)
Lasse V. Karlsen

1

ใช่.

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

หากคุณไม่ทำเช่นนั้นคุณจะมีตัวชี้ที่น่ารังเกียจบางข้อบกพร่องหนึ่งวัน

ฉันมักจะใช้แมโครเพื่อลบ:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(และคล้ายกันสำหรับอาร์เรย์, ฟรี (), ปล่อยที่จับ)

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

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

แก้ไข

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

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

แน่นอนตัวอย่างข้างต้นเป็นเพียงขั้นตอนต่อตัวชี้อัตโนมัติ ซึ่งฉันไม่ได้แนะนำเพราะ OP ถูกถามโดยเฉพาะเกี่ยวกับกรณีที่ไม่ได้ใช้ตัวชี้อัตโนมัติ


2
มาโครเป็นความคิดที่ไม่ดีโดยเฉพาะเมื่อดูเหมือนฟังก์ชั่นปกติ หากคุณต้องการทำสิ่งนี้ให้ใช้ฟังก์ชั่น templated

2
ว้าว ... ฉันไม่เคยเห็นอะไรเลยเช่นการanObject->Delete(anObject)ทำให้anObjectตัวชี้ใช้งานไม่ได้ นั่นเป็นเพียงที่น่ากลัว คุณควรสร้างวิธีการคงที่เพื่อให้คุณถูกบังคับให้ทำTreeItem::Delete(anObject)อย่างน้อยที่สุด
D.Shawley

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

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

1

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

ฉันเห็นปัญหาสองข้อ: รหัสง่าย ๆ :

delete myObj;
myobj = 0

กลายเป็น for-liner ในสภาพแวดล้อมแบบมัลติเธรด:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

"แนวปฏิบัติที่ดีที่สุด" ของ Don Neufeld ไม่ได้นำมาใช้เสมอ เช่นในโครงการยานยนต์เราต้องตั้งค่าพอยน์เตอร์เป็น 0 แม้จะอยู่ในอุปกรณ์ทำลายล้าง ฉันจินตนาการได้ในซอฟต์แวร์ที่สำคัญต่อความปลอดภัยกฎดังกล่าวไม่ใช่เรื่องแปลก มันง่ายกว่า (และฉลาดกว่า) ในการติดตามพวกเขามากกว่าการพยายามชักชวนทีม / ผู้ตรวจสอบรหัสสำหรับตัวชี้แต่ละตัวใช้ในรหัสว่าเส้นที่เป็นโมฆะตัวชี้นี้ซ้ำซ้อน

อันตรายอีกอย่างคือการพึ่งพาเทคนิคนี้ในการใช้รหัสข้อยกเว้น:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

ในรหัสเช่นคุณผลิตทรัพยากรรั่วไหลและเลื่อนปัญหาหรือกระบวนการขัดข้อง

ดังนั้นปัญหาสองข้อนี้เกิดขึ้นเองในหัวของฉัน (Herb Sutter ต้องการบอกเพิ่มเติม) ทำให้ฉันมีคำถามทุกข้อว่า "วิธีหลีกเลี่ยงการใช้ตัวชี้อัจฉริยะและทำงานอย่างปลอดภัยกับตัวชี้ปกติ" ล้าสมัย


ฉันล้มเหลวในการดูว่า 4 ซับมีความซับซ้อนมากกว่า 3 ซับอย่างใดอย่างหนึ่ง (ควรใช้ lock_guards อยู่แล้ว) และหากผู้ทำลายล้างของคุณขว้างคุณไปด้วยความลำบาก
MikeMB

เมื่อฉันเห็นคำตอบนี้ครั้งแรกฉันไม่เข้าใจว่าทำไมคุณถึงต้องการลบตัวชี้ใน destructor แต่ตอนนี้ฉันทำแล้ว - ในกรณีที่วัตถุที่เป็นเจ้าของตัวชี้ถูกใช้หลังจากที่ถูกลบ!
Mark Ransom

0

มีตัวชี้อันตรายที่ต้องกังวลอยู่เสมอ


1
จะเป็นการยากไหมถ้าจะวาง "พอยน์เตอร์ห้อย" แทน "อันนี้"? :) อย่างน้อยมันก็ให้คำตอบกับคุณสารบางอย่าง
GManNickG

4
มันเป็นเรื่องของเคล็ดลับ QA ของ W3C ว่า "อย่าใช้ 'คลิกที่นี่' เป็นข้อความลิงก์": w3.org/QA/Tips/noClickHere
outis

0

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

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

แก้ไข: ตามที่ Thomas Matthews กล่าวไว้ในคำตอบก่อนหน้านี้หากตัวชี้ถูกลบใน destructor ไม่จำเป็นต้องกำหนด NULL ให้กับมันเนื่องจากจะไม่ถูกใช้อีกเพราะวัตถุจะถูกทำลายไปแล้ว


0

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


0

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

shared_ptr<Foo> p(new Foo);
//No more need to call delete

จะดำเนินการนับการอ้างอิงและปลอดภัยต่อเธรด คุณสามารถค้นหาได้ในเนมสเปซ tr1 (std :: tr1, #include <memory>) หรือหากคอมไพเลอร์ของคุณไม่ได้ให้มันรับจากการเพิ่ม

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