เหตุใดการใช้ 'ใหม่' จึงทำให้หน่วยความจำรั่ว?


131

ฉันเรียนรู้ C # ก่อนและตอนนี้ฉันเริ่มต้นด้วย C ++ ตามที่ฉันเข้าใจโอเปอเรเตอร์newใน C ++ ไม่เหมือนกับตัวดำเนินการใน C #

คุณสามารถอธิบายสาเหตุของการรั่วไหลของหน่วยความจำในโค้ดตัวอย่างนี้ได้หรือไม่?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

คำตอบ:


464

เกิดอะไรขึ้น

เมื่อคุณเขียนT t;คุณกำลังสร้างวัตถุของการพิมพ์ที่Tมีระยะเวลาการจัดเก็บข้อมูลโดยอัตโนมัติ ระบบจะล้างข้อมูลโดยอัตโนมัติเมื่ออยู่นอกขอบเขต

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

ใหม่โดยไม่ต้องล้างข้อมูล

คุณต้องส่งตัวชี้ไปที่ตัวชี้deleteเพื่อทำความสะอาด:

การสร้างใหม่ด้วยการลบ

อย่างไรก็ตามตัวอย่างที่สองของคุณแย่กว่านั้น: คุณกำลังยกเลิกการอ้างอิงตัวชี้และสร้างสำเนาของวัตถุ ด้วยวิธีนี้คุณจะสูญเสียตัวชี้ไปยังวัตถุที่สร้างด้วยnewดังนั้นคุณจึงไม่สามารถลบได้แม้ว่าคุณจะต้องการก็ตาม!

การสร้างใหม่ด้วย deref

สิ่งที่คุณควรทำ

คุณควรเลือกระยะเวลาการจัดเก็บอัตโนมัติ ต้องการวัตถุใหม่เพียงเขียน:

A a; // a new object of type A
B b; // a new object of type B

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

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

การสร้างใหม่ด้วย automatic_pointer

นี่เป็นสำนวนทั่วไปที่ใช้ชื่อ RAII ที่ไม่ค่อยมีคำอธิบาย ( Resource Acquisition Is Initialization ) เมื่อคุณได้รับทรัพยากรที่ต้องการการล้างข้อมูลคุณจะติดไว้ในวัตถุที่มีระยะเวลาจัดเก็บอัตโนมัติดังนั้นคุณจึงไม่ต้องกังวลกับการล้าง สิ่งนี้ใช้ได้กับทรัพยากรใด ๆ ไม่ว่าจะเป็นหน่วยความจำไฟล์ที่เปิดการเชื่อมต่อเครือข่ายหรืออะไรก็ตามที่คุณต้องการ

automatic_pointerสิ่งนี้มีอยู่แล้วในรูปแบบต่างๆฉันเพิ่งให้มันเป็นตัวอย่าง std::unique_ptrชั้นที่คล้ายกันมากที่มีอยู่ในห้องสมุดมาตรฐานที่เรียกว่า

นอกจากนี้ยังมีชื่อเก่า (ก่อน C ++ 11) auto_ptrแต่ตอนนี้เลิกใช้แล้วเพราะมีพฤติกรรมการคัดลอกที่แปลกประหลาด

จากนั้นก็มีตัวอย่างที่ชาญฉลาดกว่าเช่นstd::shared_ptrที่ช่วยให้ตัวชี้หลายตัวไปยังวัตถุเดียวกันและทำความสะอาดเมื่อตัวชี้ตัวสุดท้ายถูกทำลายเท่านั้น


4
@ user1131997: ดีใจที่คุณตั้งคำถามนี้อีกครั้ง อย่างที่คุณเห็นมันไม่ง่ายเลยที่จะอธิบายในความคิดเห็น :)
R. Martinho Fernandes

@ R.MartinhoFernandes: คำตอบที่ยอดเยี่ยม เพียงคำถามเดียว เหตุใดคุณจึงใช้ return by reference ในฟังก์ชัน operator * ()
Destructor

@Destructor ตอบช้า: D. การส่งกลับโดยการอ้างอิงช่วยให้คุณสามารถแก้ไขพอยน์เตอร์ได้ดังนั้นคุณสามารถทำได้เช่นเช่น*p += 2เดียวกับที่คุณทำกับตัวชี้ปกติ หากไม่ส่งคืนโดยการอ้างอิงจะไม่เลียนแบบพฤติกรรมของตัวชี้ปกติซึ่งเป็นความตั้งใจที่นี่
R. Martinho Fernandes

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

35

คำอธิบายทีละขั้นตอน:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

เมื่อสิ้นสุดสิ่งนี้คุณมีวัตถุบนฮีปโดยไม่มีตัวชี้ไปที่มันจึงไม่สามารถลบได้

ตัวอย่างอื่น ๆ :

A *object1 = new A();

เป็นหน่วยความจำรั่วเฉพาะเมื่อคุณลืมdeleteหน่วยความจำที่จัดสรรไว้:

delete object1;

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

คิดว่าคุณควรจะมีสำหรับวัตถุทุกจัดสรรdeletenew

แก้ไข

มาคิดดูobject2ไม่จำเป็นต้องเป็นความทรงจำรั่ว

รหัสต่อไปนี้เป็นเพียงการกำหนดประเด็นเป็นความคิดที่ไม่ดีไม่เคยชอบรหัสแบบนี้:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

ในกรณีนี้ตั้งแต่ถูกส่งผ่านโดยการอ้างอิงก็จะเป็นวัตถุที่แน่นอนชี้ไปตามother new B()ดังนั้นการรับที่อยู่โดยการ&otherลบตัวชี้จะทำให้หน่วยความจำว่าง

แต่ฉันเครียดไม่พออย่าทำแบบนี้ มันมาที่นี่เพื่อสร้างประเด็น


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

2
การเขียนคำตอบ "ทำได้ แต่ทำไม่ได้" เป็นเรื่องที่น่าดึงดูดเสมอ! :-) ฉันรู้ความรู้สึก
Kos

11

ให้ "วัตถุ" สองชิ้น:

obj a;
obj b;

พวกเขาจะไม่ใช้ตำแหน่งเดียวกันในหน่วยความจำ กล่าวอีกนัยหนึ่ง&a != &b

การกำหนดค่าของค่าหนึ่งให้กับค่าอื่นจะไม่ทำให้ตำแหน่งของพวกเขาเปลี่ยนไป แต่จะเปลี่ยนเนื้อหา:

obj a;
obj b = a;
//a == b, but &a != &b

โดยสัญชาตญาณตัวชี้ "วัตถุ" จะทำงานในลักษณะเดียวกัน:

obj *a;
obj *b = a;
//a == b, but &a != &b

ตอนนี้มาดูตัวอย่างของคุณ:

A *object1 = new A();

นี้จะกำหนดค่าของการnew A() object1ค่าที่เป็นตัวชี้ความหมายแต่object1 == new A() &object1 != &(new A())(โปรดทราบว่าตัวอย่างนี้ไม่ใช่รหัสที่ถูกต้องเป็นเพียงเพื่อการอธิบายเท่านั้น)

เนื่องจากค่าของตัวชี้ถูกรักษาไว้เราจึงสามารถเพิ่มหน่วยความจำที่ชี้ไปได้: delete object1;เนื่องจากกฎของเราสิ่งนี้จะทำงานเหมือนกับค่าdelete (new A());ที่ไม่มีการรั่วไหล


สำหรับตัวอย่างที่สองคุณกำลังคัดลอกวัตถุที่ชี้ไปที่ ค่าคือเนื้อหาของวัตถุนั้นไม่ใช่ตัวชี้จริง &object2 != &*(new A())เช่นเดียวกับในทุกกรณีอื่น ๆ

B object2 = *(new B());

เราสูญเสียตัวชี้ไปยังหน่วยความจำที่จัดสรรแล้วดังนั้นเราจึงไม่สามารถปลดปล่อยมันได้ delete &object2;อาจดูเหมือนว่าจะใช้งานได้ แต่เนื่องจาก&object2 != &*(new A())ไม่เทียบเท่าdelete (new A())และไม่ถูกต้อง


9

ใน C # และ Java คุณใช้ new เพื่อสร้างอินสแตนซ์ของคลาสใด ๆ จากนั้นคุณไม่จำเป็นต้องกังวลว่าจะทำลายมันในภายหลัง

C ++ ยังมีคีย์เวิร์ด "new" ซึ่งสร้างอ็อบเจกต์ แต่ไม่เหมือนใน Java หรือ C # ไม่ใช่วิธีเดียวในการสร้างอ็อบเจกต์

C ++ มีสองกลไกในการสร้างวัตถุ:

  • อัตโนมัติ
  • พลวัต

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

ในฟังก์ชั่นคุณจะสร้างมันด้วยวิธีนี้:

int func()
{
   A a;
   B b( 1, 2 );
}

ภายในชั้นเรียนปกติคุณจะสร้างด้วยวิธีนี้:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

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

ในกรณีหลังวัตถุ b ถูกทำลายพร้อมกับตัวอย่างของ A ซึ่งเป็นสมาชิก

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

หนึ่งในอ็อบเจ็กต์คือ shared_ptr ซึ่งจะเรียกใช้ตรรกะ "deleter" แต่ก็ต่อเมื่ออินสแตนซ์ทั้งหมดของ shared_ptr ที่แชร์อ็อบเจ็กต์ถูกทำลาย

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

ผู้ทำลายของคุณไม่ควรโยนข้อยกเว้น

หากคุณทำเช่นนี้คุณจะมีหน่วยความจำรั่วเพียงเล็กน้อย


4
มีมากกว่าและautomatic นอกจากนี้ยังมีdynamic static
หมูปิ้ง

9
B object2 = *(new B());

เส้นนี้เป็นสาเหตุของการรั่วไหล มาแยกกันหน่อย ..

object2 เป็นตัวแปรประเภท B เก็บไว้ที่ say address 1 (ใช่ฉันกำลังเลือกตัวเลขโดยพลการที่นี่) ทางด้านขวาคุณขอ B ใหม่หรือตัวชี้ไปยังวัตถุประเภท B โปรแกรมยินดีให้สิ่งนี้กับคุณและกำหนด B ใหม่ของคุณให้เป็นที่อยู่ 2 และยังสร้างตัวชี้ในที่อยู่ 3 ตอนนี้ วิธีเดียวในการเข้าถึงข้อมูลในที่อยู่ 2 คือผ่านตัวชี้ในที่อยู่ 3 ถัดไปคุณยกเลิกการอ้างอิงตัวชี้โดยใช้*เพื่อรับข้อมูลที่ตัวชี้ชี้ไป (ข้อมูลในที่อยู่ 2) สิ่งนี้จะสร้างสำเนาของข้อมูลนั้นอย่างมีประสิทธิภาพและกำหนดให้กับ object2 ซึ่งกำหนดในที่อยู่ 1 โปรดจำไว้ว่ามันเป็น COPY ไม่ใช่ต้นฉบับ

ตอนนี้นี่คือปัญหา:

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

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


8

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

วิธีการทำงานของโรงแรมนี้คือคุณจองห้องและแจ้งพนักงานยกกระเป๋าเมื่อคุณจะออกไป

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

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

นี่ไม่ใช่การเปรียบเทียบที่แน่นอน แต่อาจช่วยได้


5
ฉันค่อนข้างชอบการเปรียบเทียบแบบนั้นมันไม่สมบูรณ์แบบ แต่เป็นวิธีที่ดีในการอธิบายการรั่วไหลของหน่วยความจำสำหรับคนที่เพิ่งเริ่มใช้งาน!
AdamM

1
ฉันใช้สิ่งนี้ในการสัมภาษณ์วิศวกรอาวุโสที่ Bloomberg ในลอนดอนเพื่ออธิบายการรั่วไหลของหน่วยความจำกับสาว HR ฉันผ่านการสัมภาษณ์ครั้งนั้นเพราะฉันสามารถอธิบายการรั่วไหลของหน่วยความจำ (และปัญหาการเธรด) ให้กับผู้ที่ไม่ใช่โปรแกรมเมอร์ในแบบที่เธอเข้าใจได้
Stefan

7

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


3
การใช้ที่อยู่ของข้อมูลอ้างอิงเพื่อลบวัตถุนั้นเป็นวิธีปฏิบัติที่ไม่ดี ใช้ตัวชี้อัจฉริยะ
Tom Whittock

3
การปฏิบัติที่ไม่ดีอย่างไม่น่าเชื่อใช่มั้ย? คุณคิดว่าตัวชี้อัจฉริยะใช้อะไรอยู่เบื้องหลัง?
Blindy

3
@Blindy สมาร์ทพอยน์เตอร์ (อย่างน้อยก็ใช้อย่างเหมาะสม) ใช้พอยน์เตอร์โดยตรง
Luchian Grigore

2
พูดตามตรงจริงๆแล้วไอเดียทั้งหมดไม่ได้ยอดเยี่ยมขนาดนั้นใช่ไหม อันที่จริงฉันไม่แน่ใจด้วยซ้ำว่ารูปแบบที่ลองใช้ใน OP นั้นจะมีประโยชน์อย่างไร
Mario

7

คุณสร้างการรั่วไหลของหน่วยความจำถ้าคุณไม่ได้ปล่อยหน่วยความจำที่คุณจัดสรรโดยใช้ตัวnewดำเนินการโดยส่งตัวชี้ไปยังหน่วยความจำนั้นไปยังตัวdeleteดำเนินการ

ในสองกรณีของคุณข้างต้น:

A *object1 = new A();

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

และที่นี่

B object2 = *(new B());

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


7

สายนี้รั่วทันที:

B object2 = *(new B());

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

บรรทัดนี้ไม่รั่วทันที:

A *object1 = new A();

จะมีการรั่วไหลถ้าคุณไม่เคยdeleteวันที่object1แม้ว่า


4
โปรดอย่าใช้ฮีป / สแต็กเมื่ออธิบายการจัดเก็บแบบไดนามิก / อัตโนมัติ
Pubby

2
@Pubby ทำไมไม่ใช้? เนื่องจากการจัดเก็บข้อมูลแบบไดนามิก / อัตโนมัติเป็นแบบฮีปเสมอไม่ใช่กองซ้อน? และนั่นเป็นเหตุผลที่ไม่จำเป็นต้องลงรายละเอียดเกี่ยวกับ stack / heap ฉันพูดถูกหรือเปล่า?

4
@ user1131997 Heap / stack คือรายละเอียดการใช้งาน เป็นสิ่งสำคัญที่ต้องรู้ แต่ไม่เกี่ยวข้องกับคำถามนี้
Pubby

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