เมื่อใดที่จะใช้ destructors เสมือนจริง?


1486

ฉันมีความเข้าใจอย่างถ่องแท้เกี่ยวกับทฤษฎี OO ส่วนใหญ่ แต่สิ่งหนึ่งที่ทำให้ฉันสับสนมากคือการทำลายล้างเสมือนจริง

ฉันคิดว่า destructor มักถูกเรียกว่าไม่ว่าอะไรและสำหรับทุกวัตถุในโซ่

คุณตั้งใจจะทำให้พวกเขาเป็นเสมือนจริงเมื่อใดและเพราะเหตุใด


6
ดูสิ่งนี้: Virtual Destructor
Naveen

146
ทุก destructor ลงได้รับเรียกว่าไม่ว่าสิ่งที่ virtualทำให้แน่ใจว่าเริ่มต้นที่ด้านบนแทนที่จะเป็นกึ่งกลาง
Mooing Duck


@MooingDuck ที่ค่อนข้างทำให้เข้าใจผิดความคิดเห็น
Euri Pinhollow

1
@ FranklinYu เป็นเรื่องที่ดีที่คุณถามเพราะตอนนี้ฉันไม่เห็นปัญหาใด ๆ กับความคิดเห็นนั้น (ยกเว้นพยายามให้คำตอบในความคิดเห็น)
Euri Pinhollow

คำตอบ:


1572

destructors เสมือนมีประโยชน์เมื่อคุณอาจลบอินสแตนซ์ของคลาสที่ได้รับผ่านตัวชี้ไปยังคลาสพื้นฐาน:

class Base 
{
    // some virtual methods
};

class Derived : public Base
{
    ~Derived()
    {
        // Do some important cleanup
    }
};

ที่นี่คุณจะสังเกตเห็นว่าฉันไม่ได้ประกาศตัวทำลายฐานของvirtualมัน ตอนนี้เรามาดูตัวอย่างต่อไปนี้:

Base *b = new Derived();
// use b
delete b; // Here's the problem!

ตั้งแต่ destructor ฐานไม่ได้virtualและbเป็นBase*ที่ชี้ไปยังDerivedวัตถุdelete bที่มีลักษณะการทำงานที่ไม่ได้กำหนด :

[ในdelete b] หากประเภทคงที่ของวัตถุที่จะลบแตกต่างจากประเภทแบบไดนามิกประเภทคงที่จะต้องเป็นชั้นฐานของประเภทแบบไดนามิกของวัตถุที่จะถูกลบและประเภทคงที่จะต้องมี destructor เสมือนหรือ พฤติกรรมจะไม่ได้กำหนด

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

เพื่อสรุปให้สร้าง destructors คลาสพื้นฐานเสมอvirtualเมื่อมันหมายถึงการจัดการ polymorphically

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

คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ virtuality และเสมือน destructor ชั้นฐานในบทความนี้จาก Sutter


174
นี่จะอธิบายได้ว่าทำไมฉันถึงมีรอยรั่วขนาดใหญ่โดยใช้โรงงานที่ฉันทำไว้ก่อนหน้านี้ ทุกอย่างเข้าท่าแล้ว ขอบคุณ
Lodle

8
นี่เป็นตัวอย่างที่ไม่ดีเนื่องจากไม่มีข้อมูลสมาชิก เกิดอะไรขึ้นถ้าBaseและDerivedมีทุกตัวแปรจัดเก็บโดยอัตโนมัติ? นั่นคือไม่มี "พิเศษ" หรือรหัสที่กำหนดเองเพิ่มเติมเพื่อดำเนินการใน destructor ถ้าอย่างนั้นมันจะโอเคที่จะเขียนตัวทำลายหรือไม่? หรือคลาสที่ได้รับจะยังมีหน่วยความจำรั่วหรือไม่
bobobobo


28
จากบทความของ Herb Sutter: "Guideline # 4: ตัวทำลายคลาสพื้นฐานควรเป็นแบบสาธารณะและเสมือนหรือแบบป้องกันและแบบไม่เสมือน"
ซันเดย์

3
นอกจากนี้จากบทความ - 'ถ้าคุณลบ polymorphically โดยไม่มีตัวทำลายเสมือนคุณเรียกปีศาจที่น่าสยดสยองของพฤติกรรมที่ไม่ได้กำหนดไว้ว่า lol
Bondolin

219

ตัวสร้างเสมือนเป็นไปไม่ได้ แต่ตัวทำลายเสมือนเป็นไปได้ ให้เราทดลอง .......

#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

รหัสข้างต้นส่งออกต่อไปนี้:

Base Constructor Called
Derived constructor called
Base Destructor called

การสร้างวัตถุที่ได้รับนั้นเป็นไปตามกฎการก่อสร้าง แต่เมื่อเราลบตัวชี้ "b" (ตัวชี้ฐาน) เราพบว่ามีเพียงตัวทำลายฐานเท่านั้นที่ถูกเรียก แต่สิ่งนี้จะต้องไม่เกิดขึ้น ในการทำสิ่งที่เหมาะสมเราต้องทำให้ตัวทำลายฐานเป็นเสมือน ตอนนี้ให้ดูสิ่งที่เกิดขึ้นในสิ่งต่อไปนี้:

#include <iostream>

using namespace std;

class Base
{ 
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    virtual ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

เอาต์พุตเปลี่ยนดังนี้:

Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called

ดังนั้นการทำลายของตัวชี้ฐาน (ซึ่งใช้เวลาการจัดสรรบนวัตถุที่ได้รับ!) ตามกฎการทำลายคือครั้งแรกที่ได้รับมาแล้วฐาน ในทางกลับกันไม่มีอะไรเหมือนคอนสตรัคเตอร์เสมือน


1
"ตัวสร้างเสมือนเป็นไปไม่ได้" หมายความว่าคุณไม่จำเป็นต้องเขียนตัวสร้างเสมือนด้วยตัวคุณเอง การก่อสร้างของวัตถุที่ได้มาจะต้องเป็นไปตามสายการก่อสร้างจากที่ได้มาถึงฐาน ดังนั้นคุณไม่จำเป็นต้องเขียนคำหลักเสมือนสำหรับตัวสร้างของคุณ ขอบคุณ
Tunvir Rahman Tusher

4
@Murkantilism "การสร้างเสมือนไม่สามารถทำได้" เป็นความจริงแน่นอน ตัวสร้างไม่สามารถทำเครื่องหมายเสมือนได้
cmeub

1
@ cmeub แต่มีสำนวนที่จะบรรลุสิ่งที่คุณต้องการจากตัวสร้างเสมือน ดูparashift.com/c++-faq-lite/virtual-ctors.html
cape1232

@TunvirRahmanTusher คุณช่วยอธิบายหน่อยได้ไหมว่าทำไม Base Destructor ถึงถูกเรียกว่า ??
rimalonfire

@rimiro มันเป็นอัตโนมัติโดย c ++ คุณสามารถติดตามลิงก์stackoverflow.com/questions/677620/…
Tunvir Rahman Tusher

195

ประกาศ destructors เสมือนในคลาสพื้นฐาน polymorphic นี้เป็นรายการที่ 7 ในสกอตต์เมเยอร์สที่มีประสิทธิภาพ C ++ เมเยอร์สจะสรุปว่าถ้าคลาสใดมีฟังก์ชันเสมือนมันควรมี destructor เสมือนจริงและคลาสนั้นไม่ได้ออกแบบให้เป็นคลาสพื้นฐานหรือไม่ได้ออกแบบมาเพื่อใช้ polymorphically ไม่ควรประกาศ destructors เสมือน


14
+ "ถ้าคลาสใดมีฟังก์ชั่นเสมือนจริงมันควรจะมี destructor เสมือนจริงและคลาสนั้นไม่ได้ออกแบบให้เป็นคลาสพื้นฐานหรือไม่ได้ออกแบบมาให้ใช้ polymorphically ไม่ควรประกาศ destructors เสมือน": มีกรณีที่เหมาะสมหรือไม่ ผิดกฎนี้ไหม ถ้าไม่มันจะทำให้รู้สึกว่าคอมไพเลอร์ตรวจสอบสภาพนี้และปัญหาข้อผิดพลาดมันไม่เป็นที่พอใจ?
Giorgio

@Giorgio ฉันไม่ทราบว่ามีข้อยกเว้นกฎ แต่ฉันจะไม่ให้คะแนนตัวเองในฐานะผู้เชี่ยวชาญ C ++ ดังนั้นคุณอาจต้องการโพสต์คำถามนี้เป็นคำถามแยกต่างหาก คำเตือนคอมไพเลอร์ (หรือคำเตือนจากเครื่องมือวิเคราะห์แบบคงที่) ทำให้รู้สึกถึงฉัน
Bill the Lizard

10
คลาสสามารถออกแบบให้ไม่ถูกลบผ่านตัวชี้บางประเภท แต่ยังมีฟังก์ชันเสมือน - ตัวอย่างทั่วไปคืออินเทอร์เฟซการเรียกกลับ หนึ่งไม่ได้ลบการใช้งานของเขาผ่านตัวชี้อินเทอร์เฟซการเรียกกลับที่เป็นเพียงสำหรับการสมัครสมาชิก แต่มันมีฟังก์ชันเสมือน
dascandy

3
@dascandy อย่างแน่นอน - นั่นหรือสถานการณ์อื่น ๆอีกมากมายที่เราใช้พฤติกรรม polymorphic แต่ไม่ทำการจัดการพื้นที่เก็บข้อมูลผ่านพอยน์เตอร์ - เช่นการบำรุงรักษาวัตถุอัตโนมัติหรือคงที่ระยะเวลาคงที่ด้วยพอยน์เตอร์ใช้เป็นเส้นทางการสังเกตเท่านั้น ไม่จำเป็นต้องมีวัตถุประสงค์ในการใช้ virtual destructor ในกรณีดังกล่าว เนื่องจากเราเพิ่งพูดถึงผู้คนที่นี่ฉันชอบ Sutter จากด้านบน: "Guideline # 4: ตัวทำลายคลาสพื้นฐานควรเป็นแบบสาธารณะและเสมือนหรือแบบป้องกันหรือแบบเสมือน" หลังช่วยให้มั่นใจได้ว่าทุกคนที่พยายามลบผ่านตัวชี้ฐานจะแสดงข้อผิดพลาดของวิธีการของพวกเขาโดยไม่ตั้งใจ
underscore_d

1
@Giorgio มีจริงเคล็ดลับหนึ่งสามารถใช้และหลีกเลี่ยงการโทรเสมือนกับ destructor ผูกผ่าน const const Base& = make_Derived();อ้างอิงวัตถุที่ได้มาไปยังฐานเช่น ในกรณีนี้ destructor ของDerivedprvalue จะถูกเรียกแม้ว่ามันจะไม่เสมือนจริงก็ตามดังนั้นจึงประหยัดค่าใช้จ่ายที่แนะนำโดย vtables / vpointers แน่นอนขอบเขตค่อนข้าง จำกัด อังเดร Alexandrescu กล่าวถึงนี้ในหนังสือของเขาโมเดิร์น c ++ ออกแบบ
vsoftco

46

นอกจากนี้จะต้องตระหนักว่าการลบตัวชี้ชั้นฐานเมื่อไม่มี destructor เสมือนจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด สิ่งที่ฉันเรียนรู้เมื่อเร็ว ๆ นี้:

ควรลบล้างการลบใน C ++ อย่างไร

ฉันใช้ C ++ มาหลายปีแล้วและฉันก็ยังคงแขวนคอตัวเองอยู่


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

นั่นเป็นสิ่งเดียวกัน ตัวสร้างเริ่มต้นไม่ใช่เสมือน
BigSandwich

41

ทำให้ destructor เสมือนจริงเมื่อใดก็ตามที่คลาสของคุณเป็น polymorphic


13

การเรียก destructor ผ่านตัวชี้ไปยังคลาสพื้นฐาน

struct Base {
  virtual void f() {}
  virtual ~Base() {}
};

struct Derived : Base {
  void f() override {}
  ~Derived() override {}
};

Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived

การเรียก destructor เสมือนไม่แตกต่างจากการเรียกฟังก์ชันเสมือนอื่นใด ๆ

สำหรับbase->f()การโทรจะถูกส่งไปDerived::f()และมันก็เหมือนกันสำหรับbase->~Base()- ฟังก์ชั่นการเอาชนะ - Derived::~Derived()จะถูกเรียก

เดียวกันที่เกิดขึ้นเมื่อ destructor delete base;จะถูกเรียกว่าทางอ้อมเช่น deleteโทรงบประสงค์ซึ่งจะถูกส่งไปยังbase->~Base()Derived::~Derived()

คลาสนามธรรมพร้อมตัวทำลายที่ไม่เสมือน

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

// library.hpp

struct Base {
  virtual void f() = 0;

protected:
  ~Base() = default;
};

void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.

//-------------------
// application.cpp

struct Derived : Base {
  void f() override { ... }
};

int main() {
  Derived derived;
  CallsF(derived);
  // No need for virtual destructor here as well.
}

จำเป็นหรือไม่ที่จะต้องประกาศอย่างชัดเจน~Derived()ในคลาสที่ได้รับทั้งหมดแม้ว่าจะเป็นเพียง~Derived() = default? หรือว่าเป็นนัยโดยภาษา (ทำให้ปลอดภัยที่จะละเว้น)?
Ponkadoodle

@ Wallacoloo ไม่เพียง แต่ประกาศเมื่อจำเป็นเท่านั้น เช่นใส่ในส่วนหรือเพื่อให้มั่นใจว่ามันเป็นเสมือนโดยใช้protected override
Abyx

9

ฉันชอบคิดเกี่ยวกับอินเตอร์เฟสและการนำไปใช้งานของอินเตอร์เฟส ใน C ++ speak interface เป็นคลาสเสมือนจริง Destructor เป็นส่วนหนึ่งของอินเทอร์เฟซและคาดว่าจะนำไปใช้ ดังนั้น destructor ควรเป็น virtual virtual คอนสตรัคเตอร์เป็นอย่างไร? คอนสตรัคเตอร์ไม่ได้เป็นส่วนหนึ่งของส่วนต่อประสานเนื่องจากวัตถุนั้นถูกสร้างอินสแตนซ์เสมออย่างชัดเจน


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

2
+1 สำหรับระบุความคล้ายคลึงกันของแนวคิด OO ของอินเตอร์เฟซและ C ++ ระดับเสมือนบริสุทธิ์ เกี่ยวกับdestructor ที่คาดว่าจะดำเนินการ : ซึ่งมักไม่จำเป็น เว้นแต่คลาสจะจัดการทรัพยากรเช่นหน่วยความจำที่จัดสรรแบบไดนามิก (เช่นไม่ใช่ตัวชี้สมาร์ท), การจัดการไฟล์หรือจัดการฐานข้อมูลโดยใช้ destructor เริ่มต้นที่สร้างโดยคอมไพเลอร์จะดีในคลาสที่ได้รับ และโปรดทราบว่าหากมีการประกาศ destructor (หรือฟังก์ชั่นใด ๆ ) virtualในคลาสพื้นฐานมันจะอยู่virtualในคลาสที่ได้รับโดยอัตโนมัติแม้ว่าจะไม่ได้ประกาศก็ตาม
DavidRR

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

8

คำสำคัญเสมือนจริงสำหรับ destructor เป็นสิ่งจำเป็นเมื่อคุณต้องการ destructors ที่แตกต่างกันควรทำตามลำดับที่เหมาะสมในขณะที่วัตถุจะถูกลบผ่านตัวชี้ระดับฐาน ตัวอย่างเช่น:

Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ; 

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


7

เพื่อให้ง่าย Virtual destructor คือการทำลายทรัพยากรในลำดับที่ถูกต้องเมื่อคุณลบตัวชี้คลาสพื้นฐานที่ชี้ไปยังวัตถุคลาสที่ได้รับ

 #include<iostream>
 using namespace std;
 class B{
    public:
       B(){
          cout<<"B()\n";
       }
       virtual ~B(){ 
          cout<<"~B()\n";
       }
 };
 class D: public B{
    public:
       D(){
          cout<<"D()\n";
       }
       ~D(){
          cout<<"~D()\n";
       }
 };
 int main(){
    B *b = new D();
    delete b;
    return 0;
 }

OUTPUT:
B()
D()
~D()
~B()

==============
If you don't give ~B()  as virtual. then output would be 
B()
D()
~B()
where destruction of ~D() is not done which leads to leak


ไม่มี destructor เสมือนฐานและการเรียกdeleteบนตัวชี้พื้นฐานนำไปสู่พฤติกรรมที่ไม่ได้กำหนด
James Adkison

@JamesAdkison ทำไมมันนำไปสู่พฤติกรรมที่ไม่ได้กำหนด ???
rimalonfire

@rimiro มันเป็นสิ่งที่มาตรฐานกล่าวว่า ฉันไม่มีสำเนา แต่ลิงค์จะนำคุณไปยังความคิดเห็นที่มีคนอ้างอิงถึงตำแหน่งภายในมาตรฐาน
James Adkison

@rimiro "หากการลบดังนั้นสามารถดำเนินการ polymorphically ผ่านอินเตอร์เฟสคลาสพื้นฐานดังนั้นมันจะต้องทำงานเสมือนจริงและต้องเป็นเสมือนจริงแน่นอนภาษานั้นต้องการมัน - ถ้าคุณลบ polymorphically โดยไม่มี destructor เสมือนคุณเรียกปีศาจที่น่ากลัวของ "พฤติกรรมที่ไม่ได้กำหนด" สางที่ฉันมักจะไม่พบเจอในซอยที่มีแสงสว่างพอสมควรขอบคุณมาก " ( gotw.ca/publications/mill18.htm ) - Herb Sutter
James Adkison

4

ตัวทำลายคลาสพื้นฐานเสมือนเป็น "วิธีปฏิบัติที่ดีที่สุด" - คุณควรใช้มันเพื่อหลีกเลี่ยงการรั่วไหลของหน่วยความจำ (ยากต่อการตรวจจับ) เมื่อใช้พวกมันคุณสามารถมั่นใจได้ว่า destructors ทั้งหมดในห่วงโซ่การสืบทอดของคลาสของคุณนั้นถูกเรียกว่า beeing (ตามลำดับที่เหมาะสม) การสืบทอดจากคลาสพื้นฐานโดยใช้ destructor เสมือนทำให้ destructor ของคลาสที่สืบทอดมาเป็นเสมือนโดยอัตโนมัติเช่นกัน (ดังนั้นคุณไม่จำเป็นต้องพิมพ์ 'virtual' อีกครั้งในการประกาศคลาส destructor ที่สืบทอด)


4

ถ้าคุณใช้shared_ptr(shared_ptr เท่านั้นไม่ใช่ unique_ptr) คุณไม่จำเป็นต้องมีคลาส destructor เสมือน:

#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){ // not virtual
        cout << "Base Destructor called\n";
    }
};

class Derived: public Base
{
public:
    Derived(){
        cout << "Derived constructor called\n";
    }
    ~Derived(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    shared_ptr<Base> b(new Derived());
}

เอาท์พุท:

Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

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

3

destructor เสมือนจริงคืออะไรหรือวิธีการใช้ destructor เสมือน

ตัวทำลายคลาสคือฟังก์ชันที่มีชื่อเดียวกันของคลาสที่อยู่ก่อนหน้าด้วย ~ ที่จะจัดสรรหน่วยความจำที่จัดสรรโดยคลาส ทำไมเราต้องมี destructor เสมือน

ดูตัวอย่างต่อไปนี้ด้วยฟังก์ชั่นเสมือนจริงบางอย่าง

ตัวอย่างนี้ยังบอกด้วยว่าคุณสามารถแปลงตัวอักษรเป็นอักษรตัวสูงหรือต่ำได้อย่างไร

#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
  //void convertch(){};
  virtual char* convertChar() = 0;
  ~convertch(){};
};

class MakeLower :public convertch
{
public:
  MakeLower(char *passLetter)
  {
    tolower = true;
    Letter = new char[30];
    strcpy(Letter, passLetter);
  }

  virtual ~MakeLower()
  {
    cout<< "called ~MakeLower()"<<"\n";
    delete[] Letter;
  }

  char* convertChar()
  {
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] + 32;
    return Letter;
  }

private:
  char *Letter;
  bool tolower;
};

class MakeUpper : public convertch
{
public:
  MakeUpper(char *passLetter)
  {
    Letter = new char[30];
    toupper = true;
    strcpy(Letter, passLetter);
  }

  char* convertChar()
  {   
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] - 32;
    return Letter;
  }

  virtual ~MakeUpper()
  {
    cout<< "called ~MakeUpper()"<<"\n";
    delete Letter;
  }

private:
  char *Letter;
  bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{
  convertch *makeupper = new MakeUpper("hai"); 
  cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";     
  delete makeupper;
  convertch *makelower = new MakeLower("HAI");;
  cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" "; 
  delete makelower;
  return 0;
}

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

ดูตัวอย่างถัดไปด้วยตัวทำลายเสมือน

#include "stdafx.h"
#include<iostream>

using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor

};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
      delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;

}

return Letter;
}

private:
char *Letter;
bool tolower;

};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{

size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
      cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{

convertch *makeupper = new MakeUpper("hai");

cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";

delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";


delete makelower;
return 0;
}

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

หรือเยี่ยมชมลิงค์

https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138


2

เมื่อคุณต้องการเรียก class destructor ที่ได้รับจากคลาสฐาน คุณต้องประกาศ destructor คลาสฐานเสมือนในคลาสฐาน


2

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

class A
{
public:
    A() {}
    virtual void foo()
    {
        cout << "This is A." << endl;
    }
};

class B : public A
{
public:
    B() {}
    void foo()
    {
        cout << "This is B." << endl;
    }
};

int main(int argc, char* argv[])
{
    A *a = new B();
    a->foo();
    if(a != NULL)
    delete a;
    return 0;
}

จะพิมพ์ออกมา:

This is B.

ไม่virtualว่าจะพิมพ์ออกมา:

This is A.

และตอนนี้คุณควรเข้าใจว่าจะใช้ destructors เสมือนเมื่อใด


ไม่สิ่งนี้จะเป็นการอธิบายพื้นฐานขั้นสุดท้ายของฟังก์ชั่นเสมือนทั้งหมดโดยไม่สนใจถึงความแตกต่างของเวลา / เหตุใด destructor ควรเป็นหนึ่ง - ซึ่งไม่เข้าใจง่ายดังนั้นทำไม OP จึงถามคำถาม (นอกจากว่าทำไมการจัดสรรแบบไดนามิกที่ไม่จำเป็นที่นี่เพียงแค่ทำB b{}; A& a{b}; a.foo();ตรวจสอบ. NULL- ที่ควรจะเป็นnullptr- ก่อนที่deleteไอเอ็นจี - มี indendation ถูกต้อง - ไม่จำเป็นต้อง: delete nullptr;. ถูกกำหนดให้เป็นไม่มี-op หากสิ่งที่คุณควรจะมีการตรวจสอบนี้ก่อนที่จะเรียก->foo(), เป็นพฤติกรรมที่ไม่ได้กำหนดเป็นอย่างอื่นสามารถเกิดขึ้นได้หากnewอย่างใดล้มเหลว)
underscore_d

2
มันมีความปลอดภัยที่จะเรียกdeleteในNULLตัวชี้ (เช่นคุณไม่จำเป็นต้องif (a != NULL)Guard)
James Adkison

@SaileshD ใช่ฉันรู้ นั่นคือสิ่งที่ฉันพูดในความคิดเห็นของฉัน
James Adkison

1

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

#include <iostream>
using namespace std;

struct a
{
    ~a() {}

    unsigned long long i;
};

struct b : a
{
    ~b() {}

    unsigned long long j;
};

struct c : b
{
    ~c() {}

    virtual void m3() {}

    unsigned long long k;
};

struct d : c
{
    ~d() {}

    virtual void m4() {}

    unsigned long long l;
};

int main()
{
    cout << "sizeof(a): " << sizeof(a) << endl;
    cout << "sizeof(b): " << sizeof(b) << endl;
    cout << "sizeof(c): " << sizeof(c) << endl;
    cout << "sizeof(d): " << sizeof(d) << endl;

    // No issue.

    a* a1 = new a();
    cout << "a1: " << a1 << endl;
    delete a1;

    // No issue.

    b* b1 = new b();
    cout << "b1: " << b1 << endl;
    cout << "(a*) b1: " << (a*) b1 << endl;
    delete b1;

    // No issue.

    c* c1 = new c();
    cout << "c1: " << c1 << endl;
    cout << "(b*) c1: " << (b*) c1 << endl;
    cout << "(a*) c1: " << (a*) c1 << endl;
    delete c1;

    // No issue.

    d* d1 = new d();
    cout << "d1: " << d1 << endl;
    cout << "(c*) d1: " << (c*) d1 << endl;
    cout << "(b*) d1: " << (b*) d1 << endl;
    cout << "(a*) d1: " << (a*) d1 << endl;
    delete d1;

    // Doesn't crash, but may not produce the results you want.

    c1 = (c*) new d();
    delete c1;

    // Crashes due to passing an invalid address to the method which
    // frees the memory.

    d1 = new d();
    b1 = (b*) d1;
    cout << "d1: " << d1 << endl;
    cout << "b1: " << b1 << endl;
    delete b1;  

/*

    // This is similar to what's happening above in the "crash" case.

    char* buf = new char[32];
    cout << "buf: " << (void*) buf << endl;
    buf += 8;
    cout << "buf after adding 8: " << (void*) buf << endl;
    delete buf;
*/
}

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

หากคุณเรียกใช้รหัสข้างต้นคุณจะเห็นอย่างชัดเจนเมื่อเกิดปัญหา เมื่อตัวชี้ของคลาสพื้นฐาน (/ struct) นี้แตกต่างจากตัวชี้ของคลาสที่ได้รับ (/ struct) คุณจะพบปัญหานี้ ในตัวอย่างด้านบน struct a และ b ไม่มี vtables structs c และ d มี vtables ดังนั้นตัวชี้ a หรือ b ไปยังอินสแตนซ์ของวัตถุ ac หรือ d จะได้รับการแก้ไขเพื่อรองรับ vtable ถ้าคุณผ่านตัวชี้ a หรือ b นี้เพื่อลบมันจะผิดพลาดเนื่องจากที่อยู่ไม่ถูกต้องกับรูทีนว่างของฮีป

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


0

นิยามพื้นฐานเกี่ยวกับvirtualมันกำหนดว่าฟังก์ชันสมาชิกของคลาสสามารถ over-ridden ในคลาสที่ได้รับ

D-tor ของคลาสนั้นเรียกว่าโดยทั่วไปที่ส่วนท้ายของขอบเขต แต่มีปัญหาตัวอย่างเช่นเมื่อเรากำหนดอินสแตนซ์บน Heap (การจัดสรรแบบไดนามิก) เราควรลบด้วยตนเอง

ทันทีที่คำสั่งถูกเรียกใช้งานคลาสตัวทำลายฐานจะถูกเรียกใช้ แต่ไม่ใช่สำหรับคำสั่งที่ได้รับ

ตัวอย่าง Pratical คือเมื่อคุณต้องจัดการเอฟเฟ็กต์แอคทูเอเตอร์

ในตอนท้ายของขอบเขตหากไม่มีการเรียก destructor ของหนึ่งในองค์ประกอบพลังงาน (Actuator) จะมีผลร้ายแรง

#include <iostream>

class Mother{

public:

    Mother(){

          std::cout<<"Mother Ctor"<<std::endl;
    }

    virtual~Mother(){

        std::cout<<"Mother D-tor"<<std::endl;
    }


};

class Child: public Mother{

    public:

    Child(){

        std::cout<<"Child C-tor"<<std::endl;
    }

    ~Child(){

         std::cout<<"Child D-tor"<<std::endl;
    }
};

int main()
{

    Mother *c = new Child();
    delete c;

    return 0;
}

-1

คลาสใดก็ตามที่สืบทอดต่อสาธารณะ polymorphic หรือไม่ควรมี destructor เสมือน หากต้องการชี้อีกวิธีหนึ่งถ้ามันสามารถชี้โดยตัวชี้คลาสพื้นฐานคลาสพื้นฐานของมันควรมี destructor เสมือน

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


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