Câu hỏi Khi nào sử dụng các destructor ảo?


Tôi có một sự hiểu biết vững chắc về hầu hết các lý thuyết OO nhưng một điều làm tôi bối rối là rất nhiều destructors ảo.

Tôi nghĩ rằng destructor luôn luôn được gọi là không có vấn đề gì và cho mọi đối tượng trong chuỗi.

Khi nào bạn định làm cho chúng ảo và tại sao?


1208
2018-01-20 12:58


gốc


Xem này: Virtual Destructor - Naveen
Mọi destructor xuống được gọi là không có vấn đề gì. virtual đảm bảo nó bắt đầu ở đầu thay vì giữa. - Mooing Duck
câu hỏi liên quan: Khi nào bạn không nên sử dụng các trình phá hủy ảo? - Eitan T
Nhiều, Nhiều năm trước, tôi là một lập trình viên C ++ và đây là câu hỏi phỏng vấn phổ biến nhất. - Ian Ringrose
@ FranklinYu nó tốt mà bạn hỏi vì bây giờ tôi không thể nhìn thấy bất kỳ vấn đề với bình luận đó (ngoại trừ cố gắng để đưa ra câu trả lời trong ý kiến). - Euri Pinhollow


Các câu trả lời:


Các trình phá hủy ảo rất hữu ích khi bạn có thể xóa một cá thể của một lớp dẫn xuất thông qua một con trỏ tới lớp cơ sở:

class Base 
{
    // some virtual methods
};

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

Ở đây, bạn sẽ nhận thấy rằng tôi đã không tuyên bố hủy diệt của Base là virtual. Bây giờ, chúng ta hãy xem đoạn mã sau:

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

Vì destructor của Base không phải là virtual và b là một Base* chỉ vào một Derived vật, delete b có hành vi không xác định:

[Trong delete b], nếu loại tĩnh của   đối tượng cần xóa khác với kiểu động, tĩnh   loại phải là một lớp cơ sở của loại động của đối tượng sẽ là   đã xóa và loại tĩnh sẽ có một destructor ảo hoặc   hành vi không xác định.

Trong hầu hết các triển khai, lệnh gọi hàm hủy sẽ được giải quyết giống như bất kỳ mã không phải ảo nào, có nghĩa là hàm hủy của lớp cơ sở sẽ được gọi nhưng không phải là lớp dẫn xuất, dẫn đến rò rỉ tài nguyên.

Tóm lại, luôn luôn làm cho destructors của lớp cơ sở virtual khi chúng được dùng để thao tác đa hình.

Nếu bạn muốn ngăn chặn việc xóa một cá thể thông qua một con trỏ lớp cơ sở, bạn có thể làm cho lớp cơ sở destructor được bảo vệ và không ảo; bằng cách làm như vậy, trình biên dịch sẽ không cho phép bạn gọi delete trên một con trỏ lớp cơ sở.

Bạn có thể tìm hiểu thêm về ảo hóa và destructor lớp cơ sở ảo trong bài viết này từ Herb Sutter.


1315
2018-01-20 13:04



Điều này sẽ giải thích lý do tại sao tôi đã có rò rỉ lớn bằng cách sử dụng một nhà máy tôi đã thực hiện trước đây. Tất cả đều có ý nghĩa bây giờ. Cảm ơn - Lodle
Điều này cũng sẽ làm việc nếu con trỏ là một void *? - Lodle
Không, nó sẽ không. Con trỏ không có dấu hiệu không biết về destructors. - Leon Timmermans
Không, nó sẽ không hoạt động với khoảng trống *. Trình biên dịch không biết gì về điểm trống *. Tất cả những gì nó biết là nó là một vị trí bộ nhớ. Bạn cần phải đưa con trỏ đến một loại để báo cho trình biên dịch biết có gì. - Rob K
Từ bài viết của Herb Sutter: "Hướng dẫn # 4: Một lớp hủy cơ sở phải là công khai và ảo, hoặc được bảo vệ và không ảo." - Sundae


Khai báo các destructors ảo trong các lớp cơ sở đa hình. Đây là Hạng 7 trong Scott Meyers ' Hiệu quả C ++. Meyers tiếp tục tóm tắt rằng nếu một lớp học có bất kì chức năng ảo, nó phải có một destructor ảo, và các lớp đó không được thiết kế để là các lớp cơ sở hoặc không được thiết kế để được sử dụng đa hình nên không phải tuyên bố destructors ảo.


165
2018-01-20 13:11



+ "Nếu một lớp có bất kỳ hàm ảo nào, nó phải có một destructor ảo, và các lớp đó không được thiết kế là các lớp cơ sở hoặc không được thiết kế để sử dụng đa hình không nên khai báo các destructor ảo.": Có trường hợp nào có ý nghĩa phá vỡ quy tắc này? Nếu không, nó sẽ làm cho tinh thần để có trình biên dịch kiểm tra tình trạng này và phát hành một lỗi là nó không hài lòng? - Giorgio
@Giorgio Tôi không biết về bất kỳ ngoại lệ nào đối với quy tắc. Nhưng tôi sẽ không đánh giá bản thân mình như là một chuyên gia C ++, vì vậy bạn có thể muốn đăng bài này như là một câu hỏi riêng biệt. Một cảnh báo trình biên dịch (hoặc một cảnh báo từ một công cụ phân tích tĩnh) có ý nghĩa với tôi. - Bill the Lizard
Các lớp có thể được thiết kế không bị xóa thông qua một con trỏ kiểu nào đó, nhưng vẫn có các hàm ảo - ví dụ điển hình là một giao diện gọi lại. Một không xóa thực hiện của mình thông qua một con trỏ giao diện gọi lại vì đó là chỉ để đăng ký, nhưng nó có chức năng ảo. - dascandy
@dascandy Chính xác - đó hoặc tất cả nhiều các tình huống khác mà chúng tôi sử dụng hành vi đa hình nhưng không thực hiện quản lý bộ nhớ thông qua con trỏ - ví dụ: duy trì các đối tượng thời gian tự động hoặc tĩnh, với con trỏ chỉ được sử dụng như các tuyến quan sát. Không cần / mục đích trong việc thực hiện một destructor ảo trong bất kỳ trường hợp như vậy. Vì chúng tôi chỉ trích dẫn mọi người ở đây, tôi thích Sutter hơn ở trên: "Hướng dẫn # 4: Trình phá hủy lớp cơ sở phải công khai và ảo, hoặc được bảo vệ và không ảo." Sau này đảm bảo bất cứ ai vô tình cố gắng xóa thông qua một con trỏ cơ sở được hiển thị lỗi của cách của họ - underscore_d
@Giorgio Thực sự là một mẹo có thể sử dụng và tránh một cuộc gọi ảo đến một destructor: liên kết thông qua một tham chiếu const một đối tượng dẫn xuất đến một cơ sở, như const Base& = make_Derived();. Trong trường hợp này, destructor của Derived prvalue sẽ được gọi, ngay cả khi nó không phải là ảo, do đó, một tiết kiệm chi phí giới thiệu bởi vtables / vpointers. Tất nhiên phạm vi là khá hạn chế. Andrei Alexandrescu đã đề cập điều này trong cuốn sách của ông Thiết kế C ++ hiện đại. - vsoftco


Một constructor ảo là không thể nhưng destructor ảo là có thể. Hãy để chúng tôi thử nghiệm ....

#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;
}

Đoạn mã trên xuất ra như sau:

Base Constructor Called
Derived constructor called
Base Destructor called

Việc xây dựng đối tượng có nguồn gốc theo quy tắc xây dựng nhưng khi chúng ta xóa con trỏ "b" (con trỏ cơ sở), chúng tôi đã phát hiện ra rằng chỉ có trình phá hủy cơ sở là call.But điều này không được xảy ra. Để làm điều thích hợp, chúng ta phải làm cho cơ sở destructor ảo. Bây giờ hãy xem điều gì xảy ra trong những điều sau đây:

#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;
}

Đầu ra thay đổi như sau:

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

Vì vậy, sự phá hủy của con trỏ cơ sở (trong đó có một phân bổ trên đối tượng có nguồn gốc!) Thực hiện theo các quy tắc tiêu hủy i.e đầu tiên có nguồn gốc sau đó cơ sở. Mặt khác cho constructor không có gì giống như constructor ảo.


155
2018-04-09 13:39



"constructor ảo là không thể" có nghĩa là bạn không cần viết constructor ảo bởi chính bạn. Việc xây dựng đối tượng dẫn xuất phải tuân theo chuỗi xây dựng từ nguồn gốc đến cơ sở. Vì vậy, bạn không cần viết từ khóa ảo cho hàm tạo của bạn. Cảm ơn - Tunvir Rahman Tusher
@Murkantilism, "các nhà thầu ảo không thể được thực hiện" là đúng sự thật. Một hàm tạo không thể được đánh dấu là ảo. - cmeub
@ cmeub, Nhưng có một thành ngữ để đạt được những gì bạn muốn từ một nhà xây dựng ảo. Xem parashift.com/c++-faq-lite/virtual-ctors.html - cape1232
@TunvirRahmanTusher bạn có thể vui lòng giải thích tại sao Base Destructor được gọi là ?? - rimiro
@rimiro Tự động của nó bằng c ++, bạn có thể theo liên kết stackoverflow.com/questions/677620/… - Tunvir Rahman Tusher


Cũng lưu ý rằng việc xóa một con trỏ lớp cơ sở khi không có destructor ảo nào sẽ dẫn đến hành vi không xác định. Điều mà tôi vừa mới học được:

Làm thế nào nên ghi đè xóa trong C ++ hành xử?

Tôi đã sử dụng C ++ trong nhiều năm và tôi vẫn cố gắng treo cổ.


37
2018-01-21 01:09



Tôi đã xem xét câu hỏi đó của bạn và thấy rằng bạn đã tuyên bố destructor cơ sở là ảo. Vì vậy, "xóa một lớp con trỏ cơ sở khi không có destructor ảo sẽ dẫn đến hành vi không xác định" ở lại hợp lệ đối với câu hỏi đó của bạn? Vì, trong câu hỏi đó, khi bạn gọi là xóa, lớp dẫn xuất (được tạo bởi toán tử mới của nó) được kiểm tra cho một phiên bản tương thích trước tiên. Vì nó tìm thấy một ở đó, nó được gọi. Vì vậy, bạn không nghĩ rằng nó sẽ là tốt hơn để nói là "xóa một lớp con trỏ cơ sở khi không có destructor sẽ dẫn đến hành vi không xác định"? - ubuntugod
Thats khá nhiều điều tương tự. Hàm khởi tạo mặc định không phải là ảo. - BigSandwich


Làm cho destructor ảo bất cứ khi nào lớp của bạn là đa hình.


30
2018-01-20 13:01





Gọi destructor thông qua một con trỏ đến một lớp cơ sở

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

Cuộc gọi destructor ảo không khác với bất kỳ cuộc gọi hàm ảo nào khác.

Dành cho base->f(), cuộc gọi sẽ được gửi đến Derived::f()và nó cũng tương tự cho base->~Base() - chức năng ghi đè của nó - Derived::~Derived() sẽ được gọi.

Tương tự xảy ra khi destructor được gọi gián tiếp, ví dụ: delete base;. Các delete tuyên bố sẽ gọi base->~Base() sẽ được gửi đến Derived::~Derived().

Lớp trừu tượng với trình hủy không ảo

Nếu bạn không xóa đối tượng thông qua một con trỏ đến lớp cơ sở của nó - thì không cần phải có một destructor ảo. Chỉ cần làm nó protected để nó sẽ không được gọi là vô tình:

// 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.
}

10
2018-05-18 13:38



Có cần khai báo một cách rõ ràng không ~Derived() trong tất cả các lớp có nguồn gốc, ngay cả khi nó chỉ ~Derived() = default? Hoặc là ngụ ý bởi ngôn ngữ (làm cho nó an toàn để bỏ qua)? - Ponkadoodle
@ Wallacoloo không, chỉ khai báo khi cần thiết. Ví dụ. đưa vào protected hoặc đảm bảo rằng nó là ảo bằng cách sử dụng override. - Abyx


Tôi thích suy nghĩ về giao diện và triển khai giao diện. Trong giao diện nói C ++ là lớp ảo thuần túy. Destructor là một phần của giao diện và được mong đợi thực hiện. Do đó, destructor phải là virtual virtual. Làm thế nào về constructor? Constructor thực sự không phải là một phần của giao diện bởi vì đối tượng luôn được khởi tạo một cách rõ ràng.


7
2017-11-08 16:28



Làm cách nào để cải thiện câu trả lời đã được chấp nhận này? - cale_b
Đó là một quan điểm khác về cùng một câu hỏi. Nếu chúng ta nghĩ về mặt giao diện thay vì lớp cơ sở so với lớp dẫn xuất thì đó là kết luận tự nhiên: nếu nó là một phần của giao diện hơn là làm cho nó trở thành ảo. Nếu không. - Dragan Ostojic
1 để nêu rõ sự giống nhau của khái niệm OO về giao diện và một C ++ lớp ảo tinh khiết. Về destructor dự kiến ​​sẽ được thực hiện: điều đó thường không cần thiết. Trừ khi một lớp đang quản lý một tài nguyên như bộ nhớ được cấp phát động (ví dụ, không thông qua con trỏ thông minh), xử lý tệp hoặc xử lý cơ sở dữ liệu, sử dụng trình phá hủy mặc định do trình biên dịch tạo ra là tốt trong các lớp dẫn xuất. Và lưu ý rằng nếu một hàm hủy (hoặc bất kỳ hàm nào) được khai báo virtual trong một lớp cơ sở, nó sẽ tự động virtual trong một lớp dẫn xuất, ngay cả khi nó không được khai báo như vậy. - DavidRR
Điều này bỏ lỡ các chi tiết quan trọng mà destructor là không cần thiếtmột phần của giao diện. Người ta có thể dễ dàng lập trình các lớp có chức năng đa hình nhưng người gọi không quản lý / không được phép xóa. Sau đó, một destructor ảo không có mục đích. Tất nhiên, để đảm bảo điều này, không phải ảo - có thể là trình phá hủy mặc định nên không công khai. Nếu tôi phải đoán, tôi muốn nói rằng các lớp như vậy thường được sử dụng nội bộ hơn cho các dự án, nhưng điều đó không làm cho chúng ít liên quan hơn như một ví dụ / sắc thái trong tất cả điều này. - underscore_d


Để đơn giản, Virtual destructor là hủy các tài nguyên theo thứ tự thích hợp, khi bạn xóa một con trỏ lớp cơ sở trỏ đến đối tượng lớp dẫn xuất.

 #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


6
2017-08-26 05:33



Không có trình phá hủy ảo cơ bản và gọi delete trên một con trỏ cơ sở dẫn đến hành vi không xác định. - James Adkison
@JamesAdkison tại sao nó dẫn đến hành vi không xác định ?? - rimiro
@rimiro Đó là những gì tiêu chuẩn nói. Tôi không có bản sao nhưng liên kết sẽ đưa bạn đến nhận xét nơi người nào đó tham chiếu vị trí trong tiêu chuẩn. - James Adkison
@rimiro "Nếu xóa, do đó, có thể được thực hiện đa hình thông qua giao diện lớp cơ sở, sau đó nó phải hành xử hầu như và phải ảo. Thật vậy, ngôn ngữ yêu cầu nó - nếu bạn xóa đa hình mà không có một destructor ảo, bạn triệu tập ma sợ hãi của "hành vi không xác định," một bóng ma cá nhân tôi không muốn gặp trong một con hẻm vừa đủ ánh sáng, cảm ơn bạn rất nhiều. " (gotw.ca/publications/mill18.htm) - Herb Sutter - James Adkison


Từ khóa ảo cho destructor là cần thiết khi bạn muốn destructors khác nhau nên theo thứ tự thích hợp trong khi đối tượng đang được xóa thông qua con trỏ lớp cơ sở. ví dụ:

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

Nếu destructor lớp dẫn xuất của bạn là ảo thì các đối tượng sẽ bị hủy bỏ theo thứ tự (đối tượng thứ nhất bắt nguồn sau đó là base). Nếu destructor lớp dẫn xuất của bạn là không ảo thì đối tượng lớp cơ sở chỉ sẽ bị xóa (vì con trỏ là lớp cơ sở "Base * myObj"). Vì vậy, sẽ có rò rỉ bộ nhớ cho đối tượng có nguồn gốc.


5
2018-01-29 07:11





Các trình phá hủy lớp cơ sở ảo là "thực hành tốt nhất" - bạn nên luôn sử dụng chúng để tránh rò rỉ bộ nhớ (khó phát hiện). Sử dụng chúng, bạn có thể chắc chắn rằng tất cả các destructors trong chuỗi thừa kế của các lớp của bạn được gọi là beeing (theo thứ tự thích hợp). Thừa kế từ một lớp cơ sở sử dụng destructor ảo làm cho destructor của lớp kế thừa tự động ảo, quá (vì vậy bạn không phải gõ lại 'virtual' trong khai báo lớp hủy thừa kế).


2
2018-01-24 16:22





Một destructor ảo là gì hoặc cách sử dụng destructor ảo

Một destructor lớp là một hàm có cùng tên của lớp trước với ~ sẽ tái phân bổ bộ nhớ được phân bổ bởi lớp đó. Tại sao chúng ta cần một destructor ảo

Xem mẫu sau với một số chức năng ảo

Mẫu cũng cho biết cách bạn có thể chuyển đổi một chữ cái thành chữ hoa hoặc chữ thường

#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;
}

Từ ví dụ trên bạn có thể thấy rằng hàm hủy cho cả lớp MakeUpper và MakeLower không được gọi.

Xem mẫu tiếp theo với trình phá hủy ảo

#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;
}

Trình phá hủy ảo sẽ gọi rõ ràng là hàm hủy thời gian chạy nhất của lớp để nó có thể xóa đối tượng theo cách thích hợp.

Hoặc truy cập liên kết

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


1
2017-07-17 14:03