หลักเกณฑ์ทั่วไปเพื่อหลีกเลี่ยงการรั่วไหลของหน่วยความจำใน C ++ [ปิด]


130

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


26
ดูเหมือนจะสร้างสรรค์สำหรับฉัน
Shoerob

11
นี่เป็นสิ่งที่สร้างสรรค์ และคำตอบได้รับการสนับสนุนจากข้อเท็จจริงความเชี่ยวชาญการอ้างอิง ฯลฯ และดูจำนวนการโหวต / คำตอบ .. !!
Samitha Chathuranga

คำตอบ:


40

แทนที่จะจัดการหน่วยความจำด้วยตนเองให้พยายามใช้ตัวชี้อัจฉริยะหากทำได้
ลองดูที่ที่lib Boost , TR1และตัวชี้สมาร์ท
นอกจากนี้ยังชี้สมาร์ทตอนนี้เป็นส่วนหนึ่งของมาตรฐาน C ++ ที่เรียกว่าC ++ 11


1
ในการคอมไพล์โดยใช้ g ++ ต้องเพิ่มพารามิเตอร์: -std = c ++ 0x
Paweł Szczur

หรือคุณสามารถคอมไพล์ด้วย g ++ โดยใช้ค่าแฟล็ก -std = c ++ 11
Prabhash Rathore

200

ฉันรับรองคำแนะนำทั้งหมดเกี่ยวกับ RAII และตัวชี้อัจฉริยะอย่างละเอียด แต่ฉันต้องการเพิ่มเคล็ดลับระดับสูงขึ้นเล็กน้อย: หน่วยความจำที่ง่ายที่สุดในการจัดการคือหน่วยความจำที่คุณไม่เคยจัดสรร ซึ่งแตกต่างจากภาษาเช่น C # และ Java ที่ทุกอย่างเป็นข้อมูลอ้างอิงใน C ++ คุณควรวางวัตถุบนสแต็กทุกครั้งที่ทำได้ ดังที่ฉันได้เห็นหลายคน (รวมถึง Dr Stroustrup) ชี้ให้เห็นว่าสาเหตุหลักที่การเก็บขยะไม่เคยได้รับความนิยมใน C ++ คือ C ++ ที่เขียนอย่างดีนั้นไม่ได้สร้างขยะมากนักในตอนแรก

ไม่ต้องเขียน

Object* x = new Object;

หรือแม้กระทั่ง

shared_ptr<Object> x(new Object);

เมื่อคุณสามารถเขียนได้

Object x;

34
ฉันหวังว่าฉันจะให้ +10 นี้ นี่เป็นปัญหาใหญ่ที่สุดที่ฉันเห็นกับโปรแกรมเมอร์ C ++ ส่วนใหญ่ในปัจจุบันและฉันคิดว่าเป็นเพราะพวกเขาเรียนรู้ Java ก่อน C ++
Kristopher Johnson

ประเด็นที่น่าสนใจมาก - ฉันเคยสงสัยว่าทำไมฉันถึงมีปัญหาในการจัดการหน่วยความจำ C ++ บ่อยกว่าภาษาอื่น ๆ มากนัก แต่ตอนนี้ฉันเห็นแล้วว่าทำไม: จริง ๆ แล้วมันช่วยให้สิ่งต่าง ๆ ไปบนสแต็คได้เหมือนในวานิลลาซี
ArtOfWarfare

แล้วคุณจะทำอย่างไรถ้าคุณเขียน Object x; แล้วต้องการทิ้ง x ไป? บอกว่า x ถูกสร้างขึ้นในเมธอดหลัก
Yamcha

3
@ user1316459 C ++ ช่วยให้คุณสร้างขอบเขตได้ทันทีเช่นกัน สิ่งที่คุณต้องทำคือพันอายุการใช้งานของ x ไว้ภายในวงเล็บปีกกาดังนี้: {Object x; x.DoSomething; } หลังจากจบ '}' ตัวทำลายของ x จะถูกเรียกว่าปลดปล่อยทรัพยากรใด ๆ ที่มีอยู่ ถ้า x เป็นหน่วยความจำที่จะจัดสรรบนฮีปฉันขอแนะนำให้ห่อไว้ใน unique_ptr เพื่อให้ทำความสะอาดได้ง่ายและเหมาะสม
David Peterson

1
โรเบิร์ต: ใช่ รอสไม่ได้พูดว่า "ไม่ต้องเขียน [โค้ดใหม่]" เขาบอกว่า "อย่าเขียน [นั่น] เมื่อคุณสามารถ [วางลงบนสแต็ก]" วัตถุขนาดใหญ่บนฮีปจะยังคงเป็นการเรียกที่ถูกต้องในสถานการณ์ส่วนใหญ่โดยเฉพาะอย่างยิ่งสำหรับโค้ดที่เน้นประสิทธิภาพ
codetaku

104

ใช้RAII

  • ลืมการเก็บขยะ (ใช้ RAII แทน) โปรดทราบว่าแม้แต่ Garbage Collector ก็สามารถรั่วไหลได้เช่นกัน (หากคุณลืม "null" การอ้างอิงบางอย่างใน Java / C #) และ Garbage Collector จะไม่ช่วยคุณในการกำจัดทรัพยากร (หากคุณมีวัตถุที่ได้รับหมายเลขอ้างอิง ไฟล์ไฟล์จะไม่ถูกปล่อยโดยอัตโนมัติเมื่อออบเจ็กต์จะออกนอกขอบเขตหากคุณไม่ได้ทำด้วยตนเองใน Java หรือใช้รูปแบบ "ทิ้ง" ใน C #)
  • ลืมกฎ "หนึ่งผลตอบแทนต่อฟังก์ชั่น" นี่เป็นคำแนะนำ C ที่ดีเพื่อหลีกเลี่ยงการรั่วไหล แต่ C ++ ล้าสมัยเนื่องจากมีการใช้ข้อยกเว้น (ใช้ RAII แทน)
  • และในขณะที่ "รูปแบบแซนวิช"เป็นคำแนะนำ C ที่ดี แต่C ++ก็ล้าสมัยไปแล้ว เนื่องจากมีการใช้ข้อยกเว้น (ใช้ RAII แทน)

โพสต์นี้ดูเหมือนจะซ้ำ แต่ใน C ++ รูปแบบพื้นฐานที่สุดที่จะทราบว่าเป็นRAII

เรียนรู้การใช้ตัวชี้อัจฉริยะทั้งจาก boost, TR1 หรือแม้แต่ auto_ptr ที่ต่ำ (แต่มักจะมีประสิทธิภาพเพียงพอ) (แต่คุณต้องรู้ข้อ จำกัด )

RAII เป็นพื้นฐานของความปลอดภัยข้อยกเว้นและการกำจัดทรัพยากรใน C ++ และไม่มีรูปแบบอื่นใด (แซนวิช ฯลฯ ) จะให้ทั้งสองอย่าง (และโดยส่วนใหญ่แล้วจะไม่ให้คุณเลย)

ดูการเปรียบเทียบรหัส RAII และไม่ใช่ RAII ด้านล่าง:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

เกี่ยวกับRAII

เพื่อสรุป (หลังจากความคิดเห็นจากOgre Psalm33 ) RAII อาศัยแนวคิดสามประการ:

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

ซึ่งหมายความว่าในรหัส C ++ ที่ถูกต้องวัตถุส่วนใหญ่จะไม่ถูกสร้างขึ้นด้วยnewและจะประกาศบนสแต็กแทน และสำหรับผู้ที่สร้างโดยใช้newทั้งหมดจะถูกกำหนดขอบเขตไว้อย่างใดอย่างหนึ่ง(เช่นแนบกับตัวชี้อัจฉริยะ)

ในฐานะนักพัฒนาสิ่งนี้มีประสิทธิภาพมากเนื่องจากคุณไม่จำเป็นต้องสนใจเกี่ยวกับการจัดการทรัพยากรด้วยตนเอง (เช่นเดียวกับที่ทำใน C หรือสำหรับวัตถุบางอย่างใน Java ซึ่งใช้try/ finallyสำหรับกรณีนั้นอย่างเข้มข้น) ...

แก้ไข (2012-02-12)

"วัตถุที่ถูกกำหนดขอบเขต ... จะถูกทำลาย ... ไม่ว่าทางออก" นั้นไม่เป็นความจริงทั้งหมด มีวิธีโกง RAII รสชาติใด ๆ ของการยุติ () จะข้ามการล้างข้อมูล exit (EXIT_SUCCESS) เป็น oxymoron ในเรื่องนี้

- วิลเฮล์มเทลล์

Wilhelmtellค่อนข้างถูกต้องเกี่ยวกับเรื่องนี้: มีวิธีพิเศษในการโกง RAII ซึ่งทั้งหมดนี้นำไปสู่การหยุดชะงักทันที

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

แต่เรายังต้องรู้เกี่ยวกับกรณีเหล่านั้นเพราะแม้ว่าจะไม่ค่อยเกิดขึ้น แต่ก็ยังสามารถเกิดขึ้นได้

(ใครโทรมาterminateหรือใช้exitรหัส C ++ แบบสบาย ๆ ... ฉันจำได้ว่าต้องจัดการกับปัญหานั้นเมื่อเล่นกับGLUT : ไลบรารีนี้เน้น C มากไปจนถึงการออกแบบอย่างแข็งขันเพื่อให้สิ่งที่ยากสำหรับนักพัฒนา C ++ เช่นไม่ใส่ใจ เกี่ยวกับสแต็คการจัดสรรข้อมูลหรือมีการตัดสินใจที่ "น่าสนใจ" เกี่ยวกับการไม่เคยกลับมาจากวงหลักของพวกเขา ... ฉันจะไม่แสดงความคิดเห็นเกี่ยวกับที่)


ต้องไม่ใช่คลาส T ใช้ RAII เพื่อให้แน่ใจว่า doRAIIStatic () ไม่รั่วไหลหน่วยความจำ? ตัวอย่างเช่น T p (); p.doSandwich (); ฉันไม่ค่อยรู้เรื่องนี้มากนัก
Daniel O

@Ogre Psalm33: ขอบคุณสำหรับความคิดเห็น แน่นอนคุณพูดถูก ฉันเพิ่มลิงก์ทั้งสองไปยังหน้า RAII Wikipedia และสรุปเล็ก ๆ ว่า RAII คืออะไร
paercebal

1
@Shiftbit: สามวิธีตามลำดับความต้องการ: _ _ _ 1. ใส่วัตถุจริงในคอนเทนเนอร์ STL _ _ _ 2. ใส่ตัวชี้อัจฉริยะ (shared_ptr) ของวัตถุในคอนเทนเนอร์ STL _ _ _ 3 ใส่ตัวชี้ดิบภายในคอนเทนเนอร์ STL แต่ห่อคอนเทนเนอร์เพื่อควบคุมการเข้าถึงข้อมูลใด ๆ Wrapper จะตรวจสอบให้แน่ใจว่าตัวทำลายล้างจะปลดปล่อยอ็อบเจ็กต์ที่จัดสรรไว้และตัวเข้าถึง wrapper จะตรวจสอบให้แน่ใจว่าไม่มีอะไรเสียหายเมื่อเข้าถึง / แก้ไขคอนเทนเนอร์
paercebal

1
@Robert: ใน C ++ 03 คุณจะใช้ doRAIIDynamic ในฟังก์ชันที่ต้องมอบความเป็นเจ้าของให้กับฟังก์ชันลูกหรือพาเรนต์ (หรือขอบเขตส่วนกลาง) หรือเมื่อคุณได้รับอินเทอร์เฟซไปยังออบเจ็กต์โพลีมอร์ฟผ่านโรงงาน (ส่งคืนตัวชี้อัจฉริยะหากเขียนถูกต้อง) ใน C ++ 11 กรณีนี้น้อยกว่าเนื่องจากคุณสามารถทำให้วัตถุของคุณเคลื่อนย้ายได้ดังนั้นการให้ความเป็นเจ้าของวัตถุที่ประกาศบนสแต็กจึงง่ายกว่า ...
paercebal

2
@ โรเบิร์ต: ... โปรดทราบว่าการประกาศอ็อบเจ็กต์บนสแต็กไม่ได้หมายความว่าอ็อบเจ็กต์นั้นไม่ใช้ฮีพภายใน (สังเกตการปฏิเสธสองครั้ง ... :-) ... ) ตัวอย่างเช่น std :: string ที่ใช้กับ Small String Optimization จะมีบัฟเฟอร์ "บนสแต็กของคลาส" สำหรับสตริงขนาดเล็ก (~ 15 ตัวอักษร) และจะใช้ตัวชี้ไปยังหน่วยความจำในฮีปสำหรับสตริงขนาดใหญ่ ... แต่จากภายนอก std :: string ยังคงเป็นประเภทค่าที่คุณประกาศ (โดยปกติ) บนสแต็กและคุณใช้เช่นเดียวกับที่คุณจะใช้จำนวนเต็ม (ตรงข้ามกับ: เนื่องจากคุณจะใช้อินเทอร์เฟซสำหรับคลาสโพลีมอร์ฟ)
paercebal

25

คุณจะต้องดูตัวชี้อัจฉริยะเช่นตัวชี้อัจฉริยะของบูสต์

แทน

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost :: shared_ptr จะลบโดยอัตโนมัติเมื่อจำนวนอ้างอิงเป็นศูนย์:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

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

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

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

เช่นเคยใช้ความคิดของคุณกับเครื่องมือใด ๆ ...



11

การรั่วไหลของหน่วยความจำส่วนใหญ่เป็นผลมาจากความไม่ชัดเจนเกี่ยวกับความเป็นเจ้าของวัตถุและอายุการใช้งาน

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

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

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

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


10

เอ๊ยเด็กน้อยกับคนเก็บขยะมือใหม่ ...

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

create a thing
use that thing
destroy that thing

บางครั้งจำเป็นต้องสร้างและทำลายในสถานที่ต่างๆ ฉันคิดอย่างหนักที่จะหลีกเลี่ยงสิ่งนั้น

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

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

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


ฉันเห็นด้วยทั้งหมด การทำงานในสภาพแวดล้อมแบบฝังคุณอาจไม่มีความหรูหราของไลบรารีของบุคคลที่สาม
ไซมอน

6
ฉันไม่เห็นด้วย. ในส่วนของ "ใช้สิ่งนั้น" หากมีการส่งคืนหรือการยกเว้นคุณจะพลาดการจัดสรร สำหรับประสิทธิภาพ std :: auto_ptr จะไม่เสียค่าใช้จ่ายใด ๆ ไม่ใช่ว่าฉันไม่เคยเขียนโค้ดแบบเดียวกับคุณ เพียงแค่มีความแตกต่างระหว่างรหัสปลอดภัย 100% และ 99% :-)
paercebal

8

คำถามดีมาก!

หากคุณใช้ c ++ และคุณกำลังพัฒนาแอปพลิเคชั่น boud CPU และหน่วยความจำแบบเรียลไทม์ (เช่นเกม) คุณต้องเขียน Memory Manager ของคุณเอง

ฉันคิดว่าสิ่งที่ดีกว่าที่คุณทำได้คือการรวมผลงานที่น่าสนใจของผู้เขียนหลาย ๆ คนฉันสามารถให้คำแนะนำแก่คุณได้:

  • ตัวจัดสรรขนาดคงที่ถูกกล่าวถึงอย่างมากทุกที่ในเน็ต

  • Alexandrescu นำเสนอ Small Object Allocation ในปี 2544 ในหนังสือ "Modern c ++ design" ที่สมบูรณ์แบบของเขา

  • ความก้าวหน้าที่ยิ่งใหญ่ (พร้อมด้วยซอร์สโค้ดแจกจ่าย) สามารถพบได้ในบทความที่น่าทึ่งใน Game Programming Gem 7 (2008) ชื่อ "ตัวจัดสรรฮีปประสิทธิภาพสูง" ที่เขียนโดย Dimitar Lazarov

  • รายการที่ดีของทรัพยากรที่สามารถพบได้ในนี้บทความ

อย่าเริ่มเขียน noob ตัวจัดสรรที่ไม่เป็นประโยชน์ด้วยตัวเอง ... ทำเอกสารด้วยตัวคุณเองก่อน


5

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

โดยทั่วไปปัญหาจะขึ้นอยู่กับความเป็นเจ้าของอย่างใดอย่างหนึ่ง ฉันขอแนะนำให้อ่านซีรีส์ C ++ ที่มีประสิทธิภาพโดย Scott Meyers และ Modern C ++ Design โดย Andrei Alexandrescu


5

มีหลายวิธีที่จะไม่รั่วไหล แต่หากคุณต้องการเครื่องมือที่จะช่วยติดตามการรั่วไหลให้ดูที่:


BoundsChecker คือ 404ing
TankorSmash

4

ตัวชี้อัจฉริยะของผู้ใช้ทุกที่ที่คุณทำได้! การรั่วไหลของหน่วยความจำทั้งคลาสจะหายไป


4

แบ่งปันและรู้กฎการเป็นเจ้าของหน่วยความจำในโครงการของคุณ การใช้กฎ COM ทำให้เกิดความสอดคล้องที่ดีที่สุด (พารามิเตอร์ [ใน] เป็นของผู้โทรผู้โทรต้องคัดลอก [out] พารามิเตอร์เป็นของผู้โทรผู้โทรต้องทำสำเนาหากมีการอ้างอิงเป็นต้น)


4

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

มีให้บริการใน Linux เกือบทุกรสชาติ (รวมถึง Android) และในดาร์วิน

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

แน่นอนคำแนะนำนี้ใช้ได้กับเครื่องมือตรวจสอบหน่วยความจำอื่น ๆ


3

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


2

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

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

เห็นได้ชัด แต่อย่าลืมพิมพ์ก่อนพิมพ์รหัสใด ๆ ในขอบเขต


2

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

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

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

อย่าใช้การอ้างอิงที่ไม่ใช่ const ในรายการอาร์กิวเมนต์ ไม่มีความชัดเจนมากเมื่ออ่านรหัสผู้โทรซึ่งผู้โทรอาจเก็บไว้อ้างอิงถึงพารามิเตอร์

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


2

คำแนะนำตามลำดับความสำคัญ:

- เคล็ดลับ # 1 อย่าลืมประกาศผู้ทำลายของคุณ "เสมือน" เสมอ

- เคล็ดลับ # 2 ใช้ RAII

-Tip # 3 ใช้ smartpointers ของ boost

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

- เคล็ดลับ # 5 หากบางส่วนที่ไม่เป็นทางการ / ไม่มีประสิทธิภาพที่สำคัญ (เช่นเดียวกับในเกมที่มีวัตถุหลายพันชิ้น) ให้ดูที่คอนเทนเนอร์ตัวชี้เพิ่มของ Thorsten Ottosen

- เคล็ดลับ # 6 ค้นหาส่วนหัวการตรวจจับการรั่วไหลสำหรับแพลตฟอร์มที่คุณเลือกเช่นส่วนหัว "vld" ของ Visual Leak Detection


ฉันอาจจะพลาดเคล็ดลับ แต่ 'เกม' และ 'ไม่สำคัญต่อประสิทธิภาพ' จะอยู่ในประโยคเดียวกันได้อย่างไร?
Adam Naylor

เกมเป็นตัวอย่างของสถานการณ์ที่สำคัญแน่นอน อาจจะไม่ชัดเจนที่นั่น
Robert Gould

เคล็ดลับ # 1 ควรใช้ก็ต่อเมื่อชั้นเรียนมีวิธีเสมือนอย่างน้อยหนึ่งวิธี ฉันจะไม่กำหนดตัวทำลายเสมือนที่ไร้ประโยชน์ในคลาสที่ไม่ได้มีไว้เพื่อใช้เป็นคลาสพื้นฐานในโครงสร้างการสืบทอดแบบหลายรูปแบบ
ก่อน

1

ถ้าทำได้ให้ใช้ boost shared_ptr และ standard C ++ auto_ptr สิ่งเหล่านี้สื่อถึงความหมายของความเป็นเจ้าของ

เมื่อคุณส่งคืน auto_ptr คุณกำลังบอกผู้โทรว่าคุณกำลังให้พวกเขาเป็นเจ้าของหน่วยความจำ

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

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


1

คนอื่น ๆ ได้กล่าวถึงวิธีการหลีกเลี่ยงการรั่วไหลของหน่วยความจำในตอนแรก (เช่นตัวชี้อัจฉริยะ) แต่เครื่องมือวิเคราะห์โปรไฟล์และหน่วยความจำมักเป็นวิธีเดียวในการติดตามปัญหาหน่วยความจำเมื่อคุณมี

Valgrind memcheckเป็นฟรีที่ยอดเยี่ยม


1

สำหรับ MSVC เท่านั้นให้เพิ่มสิ่งต่อไปนี้ที่ด้านบนของไฟล์. cpp แต่ละไฟล์:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

จากนั้นเมื่อทำการดีบักด้วย VS2003 ขึ้นไปคุณจะได้รับแจ้งเกี่ยวกับการรั่วไหลเมื่อโปรแกรมของคุณออก (ติดตามใหม่ / ลบ) มันเป็นเรื่องพื้นฐาน แต่ที่ผ่านมามันช่วยฉันได้



1

หากคุณจะจัดการหน่วยความจำด้วยตนเองคุณมีสองกรณี:

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

หากคุณต้องการฝ่าฝืนกฎเหล่านี้โปรดบันทึกไว้

เป็นข้อมูลเกี่ยวกับความเป็นเจ้าของตัวชี้


1
  • พยายามหลีกเลี่ยงการจัดสรรวัตถุแบบไดนามิก ตราบใดที่คลาสมีตัวสร้างและตัวทำลายที่เหมาะสมให้ใช้ตัวแปรของประเภทคลาสไม่ใช่ตัวชี้ไปที่มันและคุณจะหลีกเลี่ยงการจัดสรรและการจัดสรรแบบไดนามิกเนื่องจากคอมไพลเลอร์จะดำเนินการให้คุณ
    จริงๆแล้วนั่นเป็นกลไกที่ใช้โดย "ตัวชี้อัจฉริยะ" และเรียกว่า RAII โดยนักเขียนคนอื่น ๆ ;-)
  • เมื่อคุณส่งผ่านวัตถุไปยังฟังก์ชันอื่น ๆ ให้เลือกพารามิเตอร์อ้างอิงมากกว่าตัวชี้ เพื่อหลีกเลี่ยงข้อผิดพลาดที่อาจเกิดขึ้นได้
  • ประกาศพารามิเตอร์ const หากเป็นไปได้โดยเฉพาะชี้ไปที่วัตถุ ด้วยวิธีนี้ไม่สามารถปลดปล่อยวัตถุ "โดยไม่ได้ตั้งใจ" ได้ (ยกเว้นถ้าคุณโยน const ออกไป ;-)))
  • ลดจำนวนตำแหน่งในโปรแกรมที่คุณทำการจัดสรรหน่วยความจำและการยกเลิกการจัดสรร เช่น. หากคุณจัดสรรหรือปลดปล่อยประเภทเดียวกันหลาย ๆ ครั้งให้เขียนฟังก์ชันสำหรับมัน (หรือวิธีการจากโรงงาน ;-))
    ด้วยวิธีนี้คุณสามารถสร้างเอาต์พุตการดีบัก (ที่อยู่ที่ถูกจัดสรรและยกเลิกการจัดสรร, ... ) ได้อย่างง่ายดายหากจำเป็น
  • ใช้ฟังก์ชันโรงงานเพื่อจัดสรรอ็อบเจ็กต์ของคลาสที่เกี่ยวข้องหลายคลาสจากฟังก์ชันเดียว
  • หากคลาสของคุณมีคลาสพื้นฐานทั่วไปที่มีตัวทำลายเสมือนคุณสามารถปลดปล่อยคลาสทั้งหมดได้โดยใช้ฟังก์ชันเดียวกัน (หรือวิธีการแบบคงที่)
  • ตรวจสอบโปรแกรมของคุณด้วยเครื่องมือเช่น purify (น่าเสียดายที่ $ / € / ... )

0

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

นอกจากนี้ยังสามารถทำได้ในเวลาคอมไพล์โดยการแทนที่ตัวดำเนินการใหม่และลบและฟังก์ชันการจัดสรรหน่วยความจำอื่น ๆ

ตัวอย่างเช่นตรวจสอบในไซต์นี้[การดีบักการจัดสรรหน่วยความจำใน C ++] หมายเหตุ: มีเคล็ดลับสำหรับการลบตัวดำเนินการเช่นนี้:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

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

นอกจากนี้คุณยังสามารถลองอะไรบางอย่างเช่นBoundsCheckerภายใต้ Visual Studio ซึ่งค่อนข้างน่าสนใจและใช้งานง่าย


0

เรารวมฟังก์ชันการจัดสรรทั้งหมดของเราด้วยเลเยอร์ที่ต่อท้ายสตริงสั้น ๆ ที่ด้านหน้าและแฟล็กทหารรักษาการณ์ที่ส่วนท้าย ตัวอย่างเช่นคุณมีสายเรียกไปที่ "myalloc (pszSomeString, iSize, iAlignment); หรือ new (" description ", iSize) MyObject (); ซึ่งจะจัดสรรขนาดที่ระบุไว้ภายในและมีพื้นที่เพียงพอสำหรับส่วนหัวและ Sentinel ของคุณแน่นอน อย่าลืมแสดงความคิดเห็นสำหรับบิวด์ที่ไม่มีการดีบัก! ต้องใช้หน่วยความจำมากกว่านี้เล็กน้อย แต่ประโยชน์ที่ได้รับนั้นมีมากกว่าค่าใช้จ่าย

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


0

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


0

หนึ่งในตัวอย่างเดียวเกี่ยวกับการจัดสรรและทำลายในที่ต่างๆคือการสร้างเธรด (พารามิเตอร์ที่คุณส่งผ่าน) แต่แม้ในกรณีนี้จะง่าย นี่คือฟังก์ชั่น / วิธีการสร้างเธรด:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

ที่นี่แทนฟังก์ชันเธรด

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

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

param.release();

ถูกเรียกในฟังก์ชันหลัก / วิธีการ? ไม่มีอะไร! เพราะเราจะ 'บอก' ให้ auto_ptr เพิกเฉยต่อการยกเลิกการจัดสรร การจัดการหน่วยความจำ C ++ เป็นเรื่องง่ายใช่หรือไม่? ไชโย

Ema!


0

จัดการหน่วยความจำแบบเดียวกับที่คุณจัดการทรัพยากรอื่น ๆ (จัดการไฟล์การเชื่อมต่อฐานข้อมูลซ็อกเก็ต ... ) GC ก็ไม่ช่วยคุณเช่นกัน


-3

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

เป็นเรื่องง่ายเกินไปที่จะทำผิดเป็นอย่างอื่น:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.

คำตอบของคุณไม่ตรงกับโค้ดตัวอย่างที่นี่? ฉันเห็นด้วยกับคำตอบ "ส่งคืนเดียว" แต่โค้ดตัวอย่างแสดงว่าไม่ต้องทำอะไร
simon

1
ประเด็นของ C ++ RAII คือการหลีกเลี่ยงรหัสที่คุณเขียน ใน C นี่น่าจะเป็นสิ่งที่ถูกต้อง แต่ใน C ++ โค้ดของคุณมีข้อบกพร่อง ตัวอย่างเช่น: เกิดอะไรขึ้นถ้า b () ใหม่พ่น? คุณรั่ว.
paercebal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.