เกิดอะไรขึ้นกับขยะใน C ++


52

Java มี GC อัตโนมัติที่จะหยุดชั่วขณะหนึ่ง แต่ดูแลขยะในกอง ขณะนี้แอปพลิเคชัน C / C ++ ไม่มีการหยุดทำงานแบบ STW เหล่านี้การใช้งานหน่วยความจำของพวกเขาจะไม่เพิ่มขึ้นอย่างไม่สิ้นสุด พฤติกรรมนี้สำเร็จได้อย่างไร วัตถุที่ตายแล้วได้รับการดูแลอย่างไร


38
หมายเหตุ: stop-the-world เป็นตัวเลือกการนำไปใช้ของนักสะสมขยะ แต่ไม่ทั้งหมด ตัวอย่างเช่นมี GCs ที่ทำงานพร้อมกันซึ่งทำงานพร้อมกับ mutator (นั่นคือสิ่งที่นักพัฒนา GC เรียกโปรแกรมจริง) ฉันเชื่อว่าคุณสามารถซื้อรุ่นโอเพ่นซอร์สของ IBM โอเพ่นซอร์ส JVM J9 ที่มีตัวสะสมที่หยุดชั่วคราวพร้อมกัน Azul Zing มี "pauseless" เก็บที่ไม่ได้เป็นจริง pauseless แต่อย่างรวดเร็วเพื่อให้ไม่มีที่เห็นได้ชัดหยุด (หยุด GC มันอยู่ในลำดับเดียวกันในฐานะที่เป็นสวิทช์บริบทด้ายระบบปฏิบัติการซึ่งมักจะไม่เห็นเป็นหยุดชั่วคราว) .
Jörg W Mittag

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

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

9
โปรดทราบว่ายังเป็นไปได้ที่จะมีหน่วยความจำรั่วไหลในภาษาที่รวบรวมขยะ ฉันไม่คุ้นเคยกับ Java แต่การรั่วไหลของหน่วยความจำค่อนข้างเป็นเรื่องธรรมดาใน GC โลกของ. NET ที่ได้รับการจัดการ วัตถุที่ถูกอ้างอิงทางอ้อมโดยเขตข้อมูลแบบสแตติกจะไม่ถูกรวบรวมโดยอัตโนมัติตัวจัดการเหตุการณ์เป็นแหล่งที่พบบ่อยมากของการรั่วไหลและลักษณะที่ไม่ได้กำหนดไว้ล่วงหน้าของการรวบรวมขยะทำให้ไม่สามารถขจัดความต้องการทรัพยากรด้วยตนเองได้อย่างสมบูรณ์ รูปแบบ) ทั้งหมดกล่าวว่ารูปแบบการจัดการหน่วยความจำ c ++ ใช้อย่างถูกต้องคือไกลดีกว่าการเก็บขยะ
โคดี้เกรย์

26
What happens to garbage in C++? มันมักจะไม่ได้รวบรวมเป็นปฏิบัติการหรือไม่
BJ Myers

คำตอบ:


101

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

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

เท่าที่บรรเทาปัญหานี้มีหลายสิ่งที่ทำให้ชีวิตของโปรแกรมเมอร์ง่ายขึ้นมาก เหล่านี้ได้รับการสนับสนุนหลักโดยธรรมชาติของขอบเขต

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

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

ขอบเขตของลักษณะนี้ขยายไปถึงคลาส:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

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

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

RAII ยังเป็นแรงผลักดันที่อยู่เบื้องหลังทั่วไปสมาร์ทชี้ ในไลบรารี c ++ มาตรฐานเหล่านี้เป็นstd::shared_ptr, std::unique_ptrและstd::weak_ptr; แม้ว่าฉันจะได้เห็นและใช้งานshared_ptr/ weak_ptrการใช้งานอื่น ๆที่เป็นไปตามแนวคิดเดียวกัน สำหรับสิ่งเหล่านี้ตัวนับการอ้างอิงจะติดตามจำนวนพอยน์เตอร์ที่มีต่อวัตถุที่กำหนดและจะdeleteเป็นวัตถุโดยอัตโนมัติเมื่อไม่มีการอ้างอิงอีกต่อไป

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


4
ลบผ่านdelete- นั่นคือสิ่งที่ฉันกำลังมองหา น่ากลัว
Ju Shua

3
คุณอาจต้องการเพิ่มเกี่ยวกับกลไกการกำหนดขอบเขตที่ให้ไว้ใน c ++ ที่อนุญาตให้สร้างใหม่และลบส่วนใหญ่โดยอัตโนมัติ
whatsisname

9
@whatsisname มันไม่ได้เป็นที่ใหม่และลบโดยอัตโนมัติจะทำก็คือว่าพวกเขาไม่ได้เกิดขึ้นที่ทุกคนในหลายกรณี
Caleth

10
deleteเรียกว่าให้คุณโดยอัตโนมัติโดยตัวชี้สมาร์ทถ้าคุณใช้พวกเขาดังนั้นคุณควรพิจารณาการใช้พวกเขาทุกครั้งเมื่อมีการจัดเก็บข้อมูลโดยอัตโนมัติไม่สามารถใช้
Marian Spanik

11
@JuShua โปรดทราบว่าเมื่อเขียน C ++ ที่ทันสมัยคุณไม่จำเป็นต้องมีdeleteรหัสแอปพลิเคชันของคุณ (และจาก C ++ 14 เป็นต้นไปเหมือนกันnew) แต่ให้ใช้สมาร์ทพอยน์เตอร์และ RAII เพื่อลบวัตถุฮีป std::unique_ptrประเภทและstd::make_uniqueฟังก์ชั่นเป็นการทดแทนโดยตรงที่ง่ายที่สุดnewและdeleteในระดับรหัสแอปพลิเคชัน
hyde

83

C ++ ไม่มีการรวบรวมขยะ

ต้องใช้แอปพลิเคชัน C ++ เพื่อกำจัดขยะของตัวเอง

โปรแกรมเมอร์แอปพลิเคชัน C ++ จำเป็นต้องเข้าใจสิ่งนี้

เมื่อพวกเขาลืมผลลัพธ์จะถูกเรียกว่า "หน่วยความจำรั่ว"


22
แน่นอนว่าคุณต้องแน่ใจว่าคำตอบของคุณไม่มีขยะใด ๆ รวมทั้งไม่ต้องใช้
หม้อไอน้ำ

15
@leftaroundabout: ขอบคุณ ฉันคิดว่าเป็นคำชม
John R. Strohm

1
ตกลงคำตอบที่ไม่ใช้ขยะนี้มีคำสำคัญในการค้นหา: หน่วยความจำรั่ว นอกจากนี้ยังต้องการจะมีความสุขอย่างใดพูดถึงและnew delete
Ruslan

4
@Ruslan เดียวกันยังนำไปใช้mallocและfreeหรือnew[]และdelete[]หรือ allocators อื่น ๆ (เช่น Windows ของGlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ... ) และจัดสรรหน่วยความจำสำหรับคุณ (เช่นผ่านทางfopen)
user253751

43

ใน C, C ++ และระบบอื่น ๆ ที่ไม่มี Garbage Collector ผู้พัฒนาจะได้รับสิ่งอำนวยความสะดวกโดยภาษาและไลบรารีของมันเพื่อระบุว่าเมื่อใดที่หน่วยความจำสามารถเรียกคืนได้

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

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

ในกรณีนี้คอมไพเลอร์รับผิดชอบในการรู้ว่าค่าเหล่านั้นไม่ได้ใช้และเรียกคืนหน่วยความจำที่เกี่ยวข้อง

เมื่อใช้การจัดเก็บข้อมูลแบบไดนามิกใน C, หน่วยความจำจะถูกจัดสรรกับประเพณีและยึดด้วยmalloc freeใน C ++, หน่วยความจำจะถูกจัดสรรกับประเพณีและยึดด้วยnewdelete

C ไม่ได้เปลี่ยนแปลงอะไรมากในช่วงหลายปีที่ผ่านมาอย่างไรก็ตาม eschews C ++ ที่ทันสมัยnewและdeleteสมบูรณ์และอาศัยแทนสิ่งอำนวยความสะดวกห้องสมุด (ซึ่งตัวเองใช้newและdeleteเหมาะสม):

  • พอยน์เตอร์อัจฉริยะมีชื่อเสียงมากที่สุด: std::unique_ptrและstd::shared_ptr
  • แต่ภาชนะบรรจุที่มีมากขึ้นอย่างกว้างขวางจริง: std::string, std::vector, std::map... ทุกหน่วยความจำภายในจัดการจัดสรรโปร่งใส

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

เป็นผลให้การรั่วไหลของหน่วยความจำที่ไม่ได้เป็นปัญหาใน C ++แม้สำหรับผู้ใช้ใหม่ตราบเท่าที่พวกเขาละเว้นจากการใช้new, หรือdelete std::shared_ptrสิ่งนี้แตกต่างจาก C ซึ่งจำเป็นต้องมีระเบียบวินัยที่เข้มงวดและโดยทั่วไปไม่เพียงพอ


แต่คำตอบนี้จะไม่สมบูรณ์โดยไม่เอ่ยคู่น้องสาวของการรั่วไหลของหน่วยความจำ: ห้อยชี้

ตัวชี้ห้อย (หรือห้อยต่องแต่งอ้างอิง) เป็นอันตรายที่สร้างขึ้นโดยการรักษาตัวชี้หรือการอ้างอิงไปยังวัตถุที่ตายแล้ว ตัวอย่างเช่น:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

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

พฤติกรรมที่ไม่ได้กำหนดเป็นปัญหาที่ใหญ่ที่สุดของ C และ C ++ จนถึงทุกวันนี้ในแง่ของความปลอดภัย / ความถูกต้องของโปรแกรม คุณอาจต้องการที่จะตรวจสอบสนิมสำหรับภาษาที่ไม่มี Garbage Collector และไม่มีพฤติกรรมที่ไม่ได้กำหนด


17
Re: "การใช้ตัวชี้ห้อยหรือการอ้างอิงเป็นพฤติกรรมที่ไม่ได้กำหนดโดยทั่วไปโชคดีที่นี่เป็นความผิดพลาดทันที": จริงเหรอ? นั่นไม่ตรงกับประสบการณ์ของฉันเลย ในทางตรงกันข้ามประสบการณ์ของฉันคือการใช้ตัวชี้ห้อยเกือบจะไม่ทำให้เกิดความผิดพลาดทันที . .
ruakh

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

2
"เนื่องจากการรั่วไหลของหน่วยความจำไม่ได้เป็นปัญหาใน C ++" แน่นอนพวกมันมีการผูก C ไว้กับไลบรารี่เสมอรวมทั้ง shared_ptrs แบบเรียกซ้ำหรือแม้แต่แบบเรียกซ้ำแบบเอกซ์พอยท์และสถานการณ์อื่น ๆ
Mooing Duck

3
“ ไม่ใช่ปัญหาใน C ++ แม้สำหรับผู้ใช้ใหม่” - ฉันจะผ่านการคัดเลือกให้เป็น“ ผู้ใช้ใหม่ที่ไม่ได้มาจากภาษาที่เหมือน Java หรือ C
leftaroundabout

3
@leftaroundabout: มันมีคุณสมบัติ "ตราบเท่าที่พวกเขาละเว้นจากการใช้new, deleteและshared_ptr"; ภายนอกnewและshared_ptrคุณมีกรรมสิทธิ์โดยตรงดังนั้นจึงไม่มีการรั่วไหล แน่นอนว่าคุณน่าจะมีตัวชี้ห้อยอยู่ ฯลฯ แต่ฉันกลัวว่าคุณต้องออกจาก C ++ เพื่อกำจัดสิ่งเหล่านี้
Matthieu M.

27

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

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


9
พอยน์เตอร์ที่ใช้ร่วมกัน (ซึ่งใช้ RAII) เป็นวิธีที่ทันสมัยในการสร้างรอยรั่ว สมมติว่าอ็อบเจกต์ A และ B อ้างอิงซึ่งกันและกันผ่านพอยน์เตอร์ที่แชร์และไม่มีอะไรอื่นที่อ้างอิงออบเจค A หรือออบเจกต์ B ผลลัพธ์ที่ได้คือการรั่วไหล การอ้างอิงซึ่งกันและกันนี้ไม่ใช่ปัญหาในภาษาที่มีการรวบรวมขยะ
David Hammen

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

1
@gbjbaanb ในตัวอย่างที่มีตัวชี้สมาร์ทตัวชี้อัจฉริยะไม่เคยออกนอกขอบเขตนั่นคือสาเหตุที่มีการรั่วไหล เนื่องจากทั้งสองวัตถุสมาร์ทตัวชี้ได้รับการจัดสรรในขอบเขตแบบไดนามิกไม่ใช่คำศัพท์พวกเขาแต่ละคนพยายามที่จะรออีกคนหนึ่งก่อนที่จะทำลาย ความจริงที่ว่าตัวชี้สมาร์ทวัตถุจริงใน C ++ และไม่เพียง แต่ชี้เป็นสิ่งที่ทำให้เกิดการรั่วไหลที่นี่ - เพิ่มเติมวัตถุชี้สมาร์ทในขอบเขตสแต็คที่ยังชี้ไปยังวัตถุภาชนะที่ไม่สามารถ deallocate เมื่อพวกเขาทำลายตัวเองเพราะ refcount คือ ไม่ใช่ศูนย์
Leushenko

2
วิธี. NET คือการไม่เชยมันบนพื้น มันแค่เก็บไว้ในที่ที่มันเป็นอยู่จนกระทั่งสาวใช้เข้ามา และเนื่องจากวิธีที่. NET จัดสรรหน่วยความจำในทางปฏิบัติ (ไม่ใช่สัญญา) ฮีปจึงเป็นเหมือนสแต็กการเข้าถึงแบบสุ่ม มันเหมือนกับมีสัญญาและเอกสารกองซ้อนมากมายและจะผ่านเป็นครั้งคราวเพื่อทิ้งสิ่งที่ไม่ถูกต้องอีกต่อไป และเพื่อให้ง่ายขึ้นผู้ที่รอดชีวิตจากการละทิ้งแต่ละคนจะได้รับการเลื่อนไปยังกองซ้อนที่แตกต่างกันเพื่อให้คุณสามารถหลีกเลี่ยงการเดินทางข้ามกองทั้งหมดได้เกือบทุกครั้ง
Luaan

@ Luaan มันเป็นการเปรียบเทียบ ... ฉันคิดว่าคุณคงจะมีความสุขกว่านี้ถ้าฉันบอกว่าปล่อยให้มันนอนอยู่บนโต๊ะจนกระทั่งแม่บ้านทำความสะอาด
gbjbaanb

26

ควรสังเกตว่ามันเป็นในกรณีของ C ++ ความเข้าใจผิดทั่วไปว่า "คุณต้องจัดการหน่วยความจำด้วยตนเอง" จริงๆแล้วคุณไม่ได้จัดการหน่วยความจำในรหัสของคุณ

วัตถุขนาดคงที่ (พร้อมอายุการใช้งานขอบเขต)

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

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

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

วัตถุที่จัดการข้อมูลไดนามิก (พร้อมอายุการใช้งานขอบเขต)

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

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

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

(ฉันคิดว่ามันเป็นการตัดสินใจออกแบบที่ตลกที่จะใช้ตัวดำเนินการNOT แบบไบนารี~เพื่อบ่งบอกถึง destructor เมื่อใช้กับตัวเลขมันจะทำการสลับบิตในการเปรียบเทียบที่นี่เป็นการบ่งบอกว่าสิ่งที่ Constructor ทำนั้นคว่ำ

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

วัตถุ Polymorphic และอายุการใช้งานเกินขอบเขต

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

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

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

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

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

สิ่งที่ดีที่สุด: สิ่งนี้ใช้ได้กับทรัพยากรทุกประเภท

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

ตัวอย่างเช่นฟังก์ชั่นล็อค mutex มักจะเขียนเช่นนี้ใน C ++:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

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

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


7
นี่เป็นคำตอบเดียวที่ไม่ได้บิดเบือนผู้คนหรือทาสี C ++ ยากกว่าหรืออันตรายกว่าที่เป็นจริง
อเล็กซานเด Revo

6
BTW จะถือว่าเป็นการปฏิบัติที่ไม่ดีเท่านั้นที่จะใช้ตัวชี้ raw เป็นเจ้าของทรัพยากร ไม่มีอะไรผิดปกติเกี่ยวกับการใช้พวกเขาหากพวกเขาชี้ไปที่สิ่งที่รับประกันว่าจะอยู่ได้นานกว่าตัวชี้
Alexander Revo

8
ฉันสองอเล็กซานเด ฉันงุนงงที่จะเห็น "C ++ ไม่มีการจัดการหน่วยความจำอัตโนมัติลืมdeleteและคุณตาย" คำตอบพุ่งสูงกว่า 30 คะแนนและได้รับการยอมรับในขณะที่อันนี้มีห้า ไม่มีใครใช้ C ++ ที่นี่จริงหรือ
เควนติน

8

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

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

ตัวอย่างเช่นนี่คือฟังก์ชันในการจัดสรรอาเรย์ 2 มิติที่ไม่ต่อเนื่องกัน ลักษณะการทำงานที่นี่คือว่าหากเกิดความล้มเหลวในการจัดสรรตรงกลางผ่านกระบวนการเราย้อนกลับทุกอย่างและส่งกลับข้อบ่งชี้ข้อผิดพลาดโดยใช้ตัวชี้ NULL:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

รหัสนี้เป็นที่น่าเกลียดกับgotos เหล่านั้นแต่ในกรณีที่ไม่มีกลไกการจัดการข้อยกเว้นที่มีโครงสร้างใด ๆ นี่เป็นวิธีเดียวที่จะจัดการกับปัญหาโดยไม่เพียงแค่ประกันตัวออกไปโดยเฉพาะอย่างยิ่งโดยเฉพาะอย่างยิ่งหากรหัสการจัดสรรทรัพยากรของคุณซ้อนกันมากขึ้น ลึกกว่าหนึ่งวง นี่เป็นหนึ่งในไม่กี่ครั้งที่gotoจริง ๆ แล้วเป็นตัวเลือกที่น่าสนใจ มิฉะนั้นคุณจะใช้ธงและifงบ พิเศษ

คุณสามารถทำให้ชีวิตง่ายขึ้นด้วยตัวคุณเองโดยการเขียนฟังก์ชั่นจัดสรร / deallocator เฉพาะสำหรับทรัพยากรแต่ละอย่างเช่น

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}

1
นี่คือคำตอบที่ดีแม้จะมีgotoงบ ข้อปฏิบัตินี้แนะนำให้ใช้ในบางพื้นที่ เป็นรูปแบบที่ใช้กันทั่วไปเพื่อป้องกันข้อยกเว้นเทียบเท่าใน C ลองดูที่รหัสเคอร์เนล Linux ซึ่งเป็นข้อความที่เต็มไปด้วยgotoคำสั่ง - และที่ไม่รั่วไหล
David Hammen

"โดยปราศจากการประกันตัวออกมาอย่างสมบูรณ์" -> ในความยุติธรรมหากคุณต้องการพูดคุยเกี่ยวกับ C นี่อาจเป็นวิธีปฏิบัติที่ดี C เป็นภาษาที่ใช้ดีที่สุดในการจัดการบล็อกหน่วยความจำที่มาจากที่อื่นหรือแยกหน่วยความจำขนาดเล็กออกเป็นโพรซีเดอร์อื่น ๆ แต่ไม่ควรทำทั้งสองอย่างในเวลาเดียวกัน หากคุณกำลังใช้ "วัตถุ" แบบคลาสสิกใน C คุณอาจไม่ได้ใช้ภาษาเพื่อจุดแข็ง
Leushenko

ที่สองgotoคือภายนอก มันต้องการจะอ่านได้มากขึ้นถ้าคุณเปลี่ยนแปลงgoto done;ไปreturn arr;และการarr=NULL;done:return arr; return NULL;แม้ว่าในกรณีที่ซับซ้อนมากขึ้นอาจมีหลายgotos จริงเริ่มที่จะคลี่คลายในระดับความพร้อมที่แตกต่างกัน
Ruslan

2

ฉันได้เรียนรู้ที่จะจำแนกปัญหาความทรงจำออกเป็นหลายประเภท

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

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

  • การอ้างอิงซึ่งกันและกัน หากวัตถุ A และ B อ้างอิงถึงกันผ่านตัวชี้ที่ใช้ร่วมกันคุณต้องทำอะไรเป็นพิเศษไม่ว่าจะในการออกแบบของคลาสเหล่านั้นหรือในรหัสที่ใช้ / ใช้คลาสเหล่านั้นเพื่อทำลายวงกลม (นี่ไม่ใช่ปัญหาสำหรับภาษาที่รวบรวมขยะ)

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

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


-6

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

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