ตัวทำลาย C ++ ถูกเรียกเมื่อใด


118

คำถามพื้นฐาน: โปรแกรมเรียกเมธอดตัวทำลายคลาสใน C ++ เมื่อใด ฉันได้รับแจ้งว่ามันถูกเรียกเมื่อใดก็ตามที่วัตถุอยู่นอกขอบเขตหรืออยู่ภายใต้ไฟล์delete

คำถามเฉพาะเพิ่มเติม:

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

2) ติดตามคำถามที่ 1 สิ่งที่กำหนดเมื่อวัตถุอยู่นอกขอบเขต (ไม่เกี่ยวกับเวลาที่วัตถุออกจาก {block} ที่กำหนด) ดังนั้นกล่าวอีกนัยหนึ่งว่าผู้ทำลายถูกเรียกบนวัตถุในรายการที่เชื่อมโยงเมื่อใด

3) คุณต้องการโทรหาผู้ทำลายด้วยตนเองหรือไม่?


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

คำตอบ:


74

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

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

2) ติดตามคำถามที่ 1 สิ่งที่กำหนดเมื่อวัตถุอยู่นอกขอบเขต (ไม่เกี่ยวกับเวลาที่วัตถุออกจาก {block} ที่กำหนด) ดังนั้นกล่าวอีกนัยหนึ่งว่าผู้ทำลายถูกเรียกบนวัตถุในรายการที่เชื่อมโยงเมื่อใด

นั่นขึ้นอยู่กับการนำรายการที่เชื่อมโยงไปใช้ คอลเลกชันทั่วไปทำลายวัตถุที่มีอยู่ทั้งหมดเมื่อถูกทำลาย

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

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

3) คุณต้องการโทรหาผู้ทำลายด้วยตนเองหรือไม่?

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

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
ฉันคิดว่าตัวอย่างสุดท้ายของคุณประกาศฟังก์ชัน? เป็นตัวอย่างของ "การแยกวิเคราะห์ที่น่ารำคาญที่สุด" (อีกประเด็นที่น่าสนใจกว่านั้นคือฉันเดาว่าคุณหมายถึงnew Foo()ด้วยเมืองหลวง 'F')
Stuart Golodetz

1
ฉันคิดว่าFoo myfoo("foo")ไม่ใช่ Most Vexing Parse แต่char * foo = "foo"; Foo myfoo(foo);เป็น
โคไซน์

อาจจะเป็นคำถามโง่ ๆ แต่ก็ไม่ควรdelete myFooโทรมาก่อนFoo *myFoo = new Foo("foo");? มิฉะนั้นคุณจะลบวัตถุที่สร้างขึ้นใหม่ไม่?
Matheus Rocha

myFooก่อนFoo *myFoo = new Foo("foo");สายก็ไม่มี บรรทัดนั้นสร้างตัวแปรใหม่ที่เรียกว่าการmyFooแรเงาตัวแปรที่มีอยู่ แม้ว่าในกรณีนี้จะไม่มีสิ่งที่มีอยู่เนื่องจากmyFooข้างต้นอยู่ในขอบเขตของifซึ่งสิ้นสุดลงแล้ว
David Schwartz

1
@galactikuh "ตัวชี้อัจฉริยะ" คือสิ่งที่ทำหน้าที่เหมือนตัวชี้ไปที่วัตถุ แต่ยังมีคุณสมบัติที่ช่วยให้จัดการอายุการใช้งานของวัตถุนั้นได้ง่ายขึ้น
David Schwartz

20

คนอื่น ๆ ได้แก้ไขปัญหาอื่น ๆ แล้วดังนั้นฉันจะดูที่จุดเดียว: คุณเคยต้องการลบวัตถุด้วยตนเองหรือไม่

คำตอบคือใช่ @DavidSchwartz ยกตัวอย่างหนึ่ง แต่มันเป็นเรื่องที่ค่อนข้างผิดปกติ ฉันจะยกตัวอย่างที่อยู่ภายใต้ประทุนของสิ่งที่โปรแกรมเมอร์ C ++ จำนวนมากใช้ตลอดเวลา: std::vector(และstd::dequeแม้ว่าจะไม่ได้ใช้มากนัก)

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

ในการจัดการสิ่งvectorนั้นสิ่งที่ทำภายใต้การครอบคลุมคือการจัดสรรหน่วยความจำดิบผ่านAllocatorอ็อบเจ็กต์ (ซึ่งเว้นแต่คุณจะระบุเป็นอย่างอื่นหมายความว่าใช้::operator new) จากนั้นเมื่อคุณใช้ (ตัวอย่าง) push_backเพื่อเพิ่มไอเท็มลงในvectorเวกเตอร์ภายในจะใช้ a placement newเพื่อสร้างไอเท็มในส่วนที่ไม่ได้ใช้ (ก่อนหน้านี้) ของพื้นที่หน่วยความจำ

ทีนี้จะเกิดอะไรขึ้นเมื่อ / ถ้าคุณeraseเป็นรายการจากเวกเตอร์ มันไม่สามารถใช้ได้delete- นั่นจะปล่อยบล็อกหน่วยความจำทั้งหมด จำเป็นต้องทำลายวัตถุหนึ่งชิ้นในหน่วยความจำนั้นโดยไม่ทำลายสิ่งอื่นใดหรือปล่อยบล็อกหน่วยความจำใด ๆ ที่ควบคุม (ตัวอย่างเช่นถ้าคุณerase5 รายการจากเวกเตอร์จากนั้นpush_backอีก 5 รายการทันทีรับประกันว่าเวกเตอร์จะไม่จัดสรรใหม่ ความทรงจำเมื่อคุณทำเช่นนั้น

ต้องการทำเช่นนั้นเวกเตอร์โดยตรงทำลายวัตถุในหน่วยความจำโดยชัดเจนโทร destructor ที่ไม่ได้deleteโดยใช้

ถ้าเป็นไปได้มีคนอื่นเขียนคอนเทนเนอร์โดยใช้การจัดเก็บที่ต่อเนื่องกันโดยประมาณเช่น a vector(หรือตัวแปรบางอย่างเช่นstd::dequeนั้นจริงๆ) คุณเกือบจะต้องการใช้เทคนิคเดียวกัน

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

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

ซึ่งแตกต่างจากภาชนะมาตรฐานนี้ใช้operator newและoperator deleteโดยตรง สำหรับการใช้งานจริงคุณอาจต้องการใช้คลาสตัวจัดสรร แต่ในขณะนี้การเบี่ยงเบนความสนใจมากกว่าการมีส่วนร่วม (IMO)


9
  1. เมื่อคุณสร้างวัตถุที่มีคุณเป็นผู้รับผิดชอบสำหรับการโทรnew deleteเมื่อคุณสร้างวัตถุด้วยmake_sharedผลลัพธ์shared_ptrจะรับผิดชอบในการนับและการโทรdeleteเมื่อจำนวนการใช้ไปเป็นศูนย์
  2. การออกนอกขอบเขตหมายถึงการออกจากบล็อก นี่คือเมื่อเรียกตัวทำลายโดยสมมติว่าวัตถุไม่ได้ถูกจัดสรรด้วยnew(กล่าวคือเป็นวัตถุสแต็ก)
  3. เกี่ยวกับเวลาเท่านั้นที่คุณจะต้องเรียก destructor อย่างชัดเจนคือเมื่อคุณจัดสรรวัตถุที่มีตำแหน่งnew

1
มีการนับการอ้างอิง (shared_ptr) แม้ว่าจะไม่ชัดเจนสำหรับพอยน์เตอร์ธรรมดา
Pubby

1
@Pubby: จุดดีขอส่งเสริมการปฏิบัติที่ดี คำตอบที่แก้ไข
MSalters

6

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

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

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


3

เพื่อให้คำตอบโดยละเอียดสำหรับคำถามที่ 3: ใช่มีโอกาส (หายาก) ที่คุณอาจเรียกผู้ทำลายอย่างชัดเจนโดยเฉพาะอย่างยิ่งในฐานะคู่หูของตำแหน่งใหม่ตามที่ dasblinkenlight สังเกต

เพื่อเป็นตัวอย่างที่เป็นรูปธรรมของสิ่งนี้:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

จุดประสงค์ของสิ่งนี้คือการแยกการจัดสรรหน่วยความจำออกจากการสร้างวัตถุ


2
  1. พอยน์เตอร์ - พอยน์เตอร์ทั่วไปไม่รองรับ RAII โดยไม่ต้องชัดเจนdeleteจะมีขยะ โชคดีที่ C ++ มีตัวชี้อัตโนมัติที่จัดการสิ่งนี้ให้คุณ!

  2. ขอบเขต - นึกถึงเมื่อตัวแปรมองไม่เห็นในโปรแกรมของคุณ โดยปกติแล้วจะเป็นตอนท้าย{block}ตามที่คุณชี้ให้เห็น

  3. การทำลายด้วยตนเอง - อย่าพยายามทำเช่นนี้ เพียงแค่ปล่อยให้ขอบเขตและ RAII ทำเวทมนตร์ให้คุณ


หมายเหตุ: auto_ptr เลิกใช้งานแล้วตามที่ลิงก์ของคุณกล่าวถึง
tnecniv

std::auto_ptrเลิกใช้งานใน C ++ 11 ใช่ หาก OP มี C ++ 11 จริงควรใช้std::unique_ptrสำหรับเจ้าของคนเดียวหรือstd::shared_ptrสำหรับเจ้าของหลายคนที่นับการอ้างอิง
chrisaycock

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

@MartinJames คุณสามารถโพสต์ตัวอย่างการเรียกระบบที่คอมไพเลอร์ไม่เข้าใจได้หรือไม่? และคุณกำลังดำเนินการตามคิวอย่างไร? ไม่std::queue<std::shared_ptr>?ฉันพบว่าpipe()ระหว่างผู้ผลิตและเธรดผู้บริโภคทำให้การทำงานพร้อมกันง่ายขึ้นมากหากการคัดลอกไม่แพงเกินไป
chrisaycock

myObject = myClass ใหม่ (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

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


0

ใช่ตัวทำลาย (aka dtor) ถูกเรียกเมื่อวัตถุอยู่นอกขอบเขตถ้าอยู่บนสแตกหรือเมื่อคุณเรียกdeleteตัวชี้ไปยังวัตถุ

  1. หากตัวชี้ถูกลบผ่านdeletedtor จะถูกเรียกใช้ หากคุณกำหนดตัวชี้ใหม่โดยไม่เรียกdeleteก่อนคุณจะได้รับหน่วยความจำรั่วเนื่องจากวัตถุยังคงอยู่ในหน่วยความจำที่ใดที่หนึ่ง ในกรณีหลังนี้จะไม่มีการเรียก dtor

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

  3. ฉันสงสัย แต่ฉันจะไม่แปลกใจถ้ามีสถานการณ์แปลก ๆ อยู่ที่นั่น


1
"หากคุณกำหนดตัวชี้ใหม่โดยไม่เรียกลบก่อนคุณจะได้รับหน่วยความจำรั่วเนื่องจากวัตถุยังคงอยู่ในหน่วยความจำที่ใดที่หนึ่ง" ไม่จำเป็น. อาจถูกลบผ่านตัวชี้อื่น
Matthew Flaschen

0

หากวัตถุไม่ได้ถูกสร้างขึ้นผ่านตัวชี้ (ตัวอย่างเช่น A a1 = A ();) ตัวทำลายจะถูกเรียกเมื่อวัตถุถูกทำลายเสมอเมื่อฟังก์ชันที่วัตถุอยู่เสร็จสิ้นตัวอย่างเช่น:

void func()
{
...
A a1 = A();
...
}//finish


ตัวทำลายถูกเรียกเมื่อโค้ดถูกเรียกใช้เพื่อขึ้นบรรทัด "เสร็จสิ้น"

หากวัตถุถูกสร้างผ่านตัวชี้ (ตัวอย่างเช่น A * a2 = new A ();) ตัวทำลายจะถูกเรียกเมื่อตัวชี้ถูกลบ (ลบ a2;) หากผู้ใช้ไม่ได้ลบจุดอย่างชัดเจนหรือกำหนดให้ ที่อยู่ใหม่ก่อนที่จะลบมันเกิดการรั่วไหลของหน่วยความจำ นั่นคือจุดบกพร่อง

ในรายการที่เชื่อมโยงหากเราใช้ std :: list <> เราไม่จำเป็นต้องสนใจเกี่ยวกับ desctructor หรือหน่วยความจำรั่วเพราะ std :: list <> ได้ทำสิ่งเหล่านี้ให้เราหมดแล้ว ในรายการที่เชื่อมโยงที่เขียนโดยตัวเราเองเราควรเขียน desctructor และลบตัวชี้อย่างชัดเจนมิฉะนั้นจะทำให้หน่วยความจำรั่วไหล

เราไม่ค่อยเรียกตัวทำลายด้วยตนเอง เป็นฟังก์ชันที่จัดเตรียมไว้สำหรับระบบ

ขอโทษที่ภาษาอังกฤษไม่ดีของฉัน!


ไม่เป็นความจริงที่คุณไม่สามารถเรียกผู้ทำลายด้วยตนเองได้ - คุณสามารถ (ดูรหัสในคำตอบของฉันเป็นต้น) สิ่งที่เป็นความจริงก็คือเวลาส่วนใหญ่ที่คุณไม่ควร :)
Stuart Golodetz

0

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

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