Khi nào nên sử dụng công cụ hủy ảo?


1486

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

Tôi nghĩ rằng hàm hủy luôn được gọi bất kể là gì và cho mọi đối tượng trong chuỗi.

Khi nào bạn có ý định biến chúng thành ảo và tại sao?


6
Xem điều này: Kẻ hủy diệt ảo
Naveen

146
Mỗi destructor xuống được gọi là không có vấn đề gì. virtualđảm bảo nó bắt đầu từ đầu thay vì ở giữa.
Vịt Mooing


@MooingDuck nhận xét hơi sai lệch.
Euri Pinhollow

1
@FranklinYu thật tốt khi bạn hỏi vì bây giờ tôi không thể thấy bất kỳ vấn đề nào với nhận xét đó (ngoại trừ cố gắng đưa ra câu trả lời trong các nhận xét).
Euri Pinhollow

Câu trả lời:


1572

Các hàm hủy ảo rất hữu ích khi bạn có khả năng xóa một thể hiện của 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àm hủy của Base virtual. Bây giờ, hãy xem đoạn trích sau:

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

Vì hàm hủy của Base không phải virtualblà một Base*trỏ đến một Derivedđối tượng, nên delete bhà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 loại động của nó, loại tĩnh sẽ là lớp cơ sở của loại động của đối tượng cần xóa và loại tĩnh sẽ có hàm hủy ả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 đến hàm hủy sẽ được giải quyết giống như bất kỳ mã không ả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à một trong các lớp dẫn xuất, dẫn đến rò rỉ tài nguyên.

Tóm lại, luôn luôn tạo ra các hàm hủy của các lớp cơ sở virtualkhi chúng có nghĩa là bị thao túng đ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 hàm hủy của lớp cơ sở được bảo vệ và không ảo; bằng cách đó, trình biên dịch sẽ không cho phép bạn gọidelete con trỏ lớp cơ sở.

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


174
Điều này sẽ giải thích tại sao tôi bị rò rỉ lớn khi sử dụng một nhà máy tôi đã làm trước đây. Tất cả có ý nghĩa bây giờ. Cảm ơn
Tạm biệt

8
Chà, đây là một ví dụ tồi vì không có thành viên dữ liệu. Điều gì nếu BaseDerivedtất cả các biến lưu trữ tự động? tức là không có mã "đặc biệt" hoặc mã tùy chỉnh bổ sung để thực thi trong hàm hủy. Có ổn không sau đó để lại viết bất kỳ kẻ hủy diệt nào? Hay lớp dẫn xuất vẫn bị rò rỉ bộ nhớ?
bobobobo


28
Từ bài viết của Herb Sutter: "Hướng dẫn số 4: Một hàm hủy của lớp cơ sở phải là công khai và ảo, hoặc được bảo vệ và không ảo."
Sundae

3
Cũng từ bài viết - 'nếu bạn xóa đa hình mà không có kẻ hủy ảo, bạn triệu tập bóng ma đáng sợ của "hành vi không xác định", một bóng ma mà cá nhân tôi không muốn gặp trong một con hẻm được chiếu sáng vừa phải, cảm ơn bạn rất nhiều.' lol
Bondolin

219

Một constructor ảo là không thể nhưng hủy diệt ả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;
}

Các 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 dẫn xuất tuân 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 ta đã thấy rằng chỉ có hàm hủy cơ sở được gọi. Nhưng đ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 hàm hủy cơ sở ảo. Bây giờ hãy xem những gì xảy ra 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, việc phá hủy con trỏ cơ sở (lấy phân bổ trên đối tượng dẫn xuất!) Tuân theo quy tắc hủy, tức là đầu tiên là Derogen, sau đó là Base. Mặt khác, không có gì giống như một nhà xây dựng ảo.


1
"Trình xây dựng ảo là không thể" có nghĩa là bạn không cần phải tự viết công cụ xây dựng ảo. Xây dựng đối tượng dẫn xuất phải tuân theo chuỗi xây dựng từ dẫn xuất đến cơ sở. Vì vậy, bạn không cần phải viết từ khóa ảo cho nhà xây dựng của bạn. Cảm ơn
Tunvir Rahman Taser

4
@Murkantilism, "các nhà xây dựng ảo không thể thực hiện được" thực sự đúng. Một constructor không thể được đánh dấu ảo.
cmeub

1
@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

@TunvirRahmanTizer bạn có thể giải thích lý do tại sao Base Destructor được gọi là ??
rimalonfire

@rimiro Nó tự động bởi c ++. bạn có thể theo liên kết stackoverflow.com/questions/677620/NH
Tunvir Rahman Tizer

195

Khai báo các hàm hủy ảo trong các lớp cơ sở đa hình. Đây là mục 7 trong C ++ hiệu quả của Scott Meyers . Meyers tiếp tục tóm tắt rằng nếu một lớp có bất kỳ chức năng ảo nào , thì nó nên có một hàm hủy ảo và các lớp không được thiết kế thành các lớp cơ sở hoặc không được thiết kế để được sử dụng đa hình không nên khai báo các hàm hủy ảo.


14
+ "Nếu một lớp có bất kỳ chức năng ảo nào, thì nó nên có một hàm hủy ảo và các lớp đó không được thiết kế thành các lớp cơ sở hoặc không được thiết kế để được sử dụng đa hình không nên khai báo các hàm hủy ảo.": Có trường hợp nào trong đó có ý nghĩa phá vỡ quy tắc này? Nếu không, nó có ý nghĩa để trình biên dịch kiểm tra điều kiện này và đưa ra một lỗi là nó không hài lòng?
Giorgio

@Giorgio Tôi không biết bất kỳ trường hợp ngoại lệ nào cho quy tắc. Nhưng tôi sẽ không đánh giá mình là một chuyên gia về C ++, vì vậy bạn có thể muốn đăng bài này dưới dạng 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 Lizard

10
Các lớp có thể được thiết kế để không bị xóa thông qua con trỏ của một loại nhất định, nhưng vẫn có các chức năng ảo - ví dụ điển hình là giao diện gọi lại. Người ta không xóa việc thực hiện của mình thông qua một con trỏ giao diện gọi lại vì nó chỉ dành cho đăng ký, nhưng nó có các chức năng ảo.
dascandy

3
@dascandy Chính xác - đó hoặc tất cả nhiều tình huống khác khi chúng tôi sử dụng hành vi đa hình nhưng không thực hiện quản lý lưu trữ thông qua con trỏ - ví dụ: duy trì các đối tượng tự động hoặc thời gian tĩnh, với con trỏ chỉ được sử dụng làm tuyến quan sát. Không cần / mục đích trong việc thực hiện một hàm hủy ảo trong bất kỳ trường hợp nào như vậy. Vì chúng tôi chỉ trích dẫn mọi người ở đây, tôi thích Sutter từ phía trên: "Hướng dẫn số 4: Một hàm hủy của lớp cơ sở nên là công khai và ảo, hoặc được bảo vệ và không ảo." Cái sau đảm bảo bất cứ ai vô tình cố gắng xóa qua một con trỏ cơ sở được hiển thị lỗi theo cách của họ
underscore_d

1
@Giorgio Thực sự có một mẹo người ta có thể sử dụng và tránh một cuộc gọi ảo đến một hàm hủy: 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ư thế nào const Base& = make_Derived();. Trong trường hợp này, hàm hủy của giá Derivedtrị sẽ được gọi, ngay cả khi nó không phải là ảo, do đó, người ta sẽ tiết kiệm chi phí được giới thiệu bởi vtables / vpointers. Tất nhiên phạm vi khá hạn chế. Andrei Alexandrescu đã đề cập đến điều này trong cuốn sách Modern C ++ Design .
vsoftco

46

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

Làm thế nào để 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 tự treo cổ.


Tôi đã xem xét câu hỏi đó của bạn và thấy rằng bạn đã khai báo hàm hủy cơ sở là ảo. Vì vậy, việc "xóa một con trỏ lớp cơ sở khi không có hàm hủy ảo sẽ dẫn đến hành vi không xác định" có hợp lệ đối với câu hỏi đó của bạn không? Vì trong câu hỏi đó, khi bạn gọi 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 phiên bản tương thích trước tiên. Vì nó tìm thấy một cái ở đó, nó được gọi. Vì vậy, bạn không nghĩ sẽ tốt hơn nếu nói "xóa một con trỏ lớp cơ sở khi không có hàm hủy sẽ dẫn đến hành vi không xác định"?
ubuntugod

Đó là khá nhiều điều tương tự. Các constructor mặc định không phải là ảo.
BigSandwich

41

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


13

Gọi hàm hủy qua con trỏ tới 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 hàm hủy ảo không khác với bất kỳ cuộc gọi chức năng ảo nào khác.

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

Điều tương tự xảy ra khi hàm hủy được gọi gián tiếp, vd delete base;. Các deletetuyên bố sẽ gọi base->~Base()mà sẽ được gửi đến Derived::~Derived().

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

Nếu bạn sẽ 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 hàm hủy ảo. Chỉ cần làm cho 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.
}

Có cần phải khai báo rõ ràng ~Derived()trong tất cả các lớp dẫn xuất, ngay cả khi nó chỉ ~Derived() = default? Hoặc điều đó được 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ụ: để đặt trong protectedphần hoặc để đảm bảo rằng nó ảo bằng cách sử dụng override.
Abyx

9

Tôi thích nghĩ về giao diện và triển khai giao diện. Trong C ++, giao diện nói là lớp ảo thuần túy. Destructor là một phần của giao diện và dự kiến ​​sẽ thực hiện. Do đó, hàm hủy phải là thuần ảo. Làm thế nào về xây dựng? Trình xây dựng thực sự không phải là một phần của giao diện vì đối tượng luôn được khởi tạo rõ ràng.


2
Đó là một quan điểm khác nhau cho 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 đó là một phần của giao diện hơn là biến nó thành ảo. Nếu không thì không.
Dragan Ostojic

2
+1 để nêu sự tương đồng của khái niệm giao diện OO và lớp ảo thuần C ++ . Về hủy diệt 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ý 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 hàm hủy mặc định được tạo bởi trình biên dịch 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 virtualtrong một lớp cơ sở, thì nó sẽ tự động virtualtrong 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ỡ chi tiết quan trọng rằng bộ hủy không nhất thiết phải là mộ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 kẻ hủy diệt ảo không có mục đích. Tất nhiên, để đảm bảo điều này, hàm hủy không ảo - có thể mặc định - phải không công khai. Nếu tôi phải đoán, tôi sẽ 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.
gạch dưới

8

Từ khóa ảo cho hàm hủy là cần thiết khi bạn muốn các hàm hủy khác nhau phải tuân theo đúng thứ tự trong khi các đối tượng đang bị 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 hàm hủy lớp cơ sở của bạn là ảo thì các đối tượng sẽ bị hủy theo thứ tự (trước tiên là đối tượng dẫn xuất sau đó là cơ sở). Nếu hàm hủy của lớp cơ sở của bạn KHÔNG phải là ảo thì chỉ có đối tượng lớp cơ sở sẽ bị xóa (vì con trỏ là của lớp cơ sở "Base * myObj"). Vì vậy, sẽ có rò rỉ bộ nhớ cho đối tượng dẫn xuất.


7

Nói một cách đơn giản, Trình hủy ảo là hủy các tài nguyên theo một 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


Không có hàm hủy ảo cơ sở và gọi deletecon 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 ??
rimalonfire

@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 một nhận xét nơi ai đó tham chiếu vị trí trong tiêu chuẩn.
James Adkison

@rimiro "Do đó, 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ở, thì nó phải hoạt động gần 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ó hàm hủy ảo, bạn triệu tập bóng ma đáng sợ "hành vi không xác định", một bóng ma mà cá nhân tôi không muốn gặp trong một con hẻm được chiếu sáng vừa phải, cảm ơn bạn rất nhiều. " ( gotw.ca/publications/mill18.htm ) - Herb Sutter
James Adkison

4

Các hàm hủy lớp cơ sở ảo là "thực hành tốt nhất" - bạn nên luô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 hàm hủy trong chuỗi thừa kế của các lớp của bạn đang được gọi là (theo đúng thứ tự). Kế thừa từ một lớp cơ sở bằng cách sử dụng hàm hủy ảo làm cho hàm hủy của lớp kế thừa cũng tự động trở thành ảo (vì vậy bạn không phải gõ lại 'ảo' trong khai báo hàm hủy lớp kế thừa).


4

Nếu bạn sử dụng shared_ptr(chỉ shared_ptr, không phải unique_ptr), bạn không cần phải có hàm hủy hủy lớp cơ sở ảo:

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

đầu ra:

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

Mặc dù điều này là có thể, tôi sẽ không khuyến khích bất cứ ai sử dụng nó. Chi phí hoạt động của một công cụ hủy ảo là rất nhỏ và điều này chỉ khiến nó có thể gây rối, đặc biệt bởi một lập trình viên ít kinh nghiệm, người không biết điều này. virtualTừ khóa nhỏ đó có thể cứu bạn khỏi rất nhiều đau đớn.
Michal Štein

3

Công cụ hủy ảo là gì hoặc cách sử dụng công cụ hủy ảo

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

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

Mẫu cũng cho biết làm thế nào bạn có thể chuyển đổi một chữ cái thành trên hoặc dưới

#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ừ mẫu 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 hàm 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;
}

Hàm hủy ảo sẽ gọi rõ ràng là hàm hủy thời gian chạy dẫn xuất nhất của lớp để nó có thể xóa đối tượng một 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


2

khi bạn cần gọi hàm hủy lớp dẫn xuất từ ​​lớp cơ sở. bạn cần khai báo hàm hủy lớp cơ sở ảo trong lớp cơ sở.


2

Tôi nghĩ cốt lõi của câu hỏi này là về các phương thức ảo và đa hình, chứ không phải là hàm hủy cụ thể. Đây là một ví dụ rõ ràng hơn:

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

Sẽ in ra:

This is B.

Không có virtualnó sẽ in ra:

This is A.

Và bây giờ bạn nên hiểu khi nào nên sử dụng các hàm hủy ảo.


Không, điều này chỉ đọc lại những điều cơ bản hoàn toàn của các chức năng ảo, hoàn toàn bỏ qua sắc thái của thời điểm / tại sao bộ hủy phải là một - không trực quan, do đó OP hỏi câu hỏi. ? (Ngoài ra, lý do tại sao việc phân bổ động không cần thiết ở đây Chỉ cần làm B b{}; A& a{b}; a.foo();kiểm tra cho. NULL- mà nên nullptr- trước deleteing - với indendation không chính xác - không yêu cầu: delete nullptr;. Được định nghĩa là một không-op Nếu bất cứ điều gì, bạn nên đã kiểm tra này trước khi gọi ->foo(), như hành vi không xác định khác có thể xảy ra nếu newthất bại bằng cách nào đó.)
underscore_d

2
Nó là an toàn để gọi deletevề một NULLcon trỏ (ví dụ, bạn không cần if (a != NULL)bảo vệ).
James Adkison

@SaileshD Vâng, tôi biết. Đó là những gì tôi đã nói trong bình luận của mình
James Adkison

1

Tôi nghĩ rằng sẽ có ích khi thảo luận về hành vi "không xác định" hoặc ít nhất là hành vi không xác định "sự cố" có thể xảy ra khi xóa qua lớp cơ sở (/ struct) mà không có hàm hủy ảo hoặc chính xác hơn là không có vtable. Mã dưới đây liệt kê một vài cấu trúc đơn giản (điều tương tự sẽ đúng với các lớp).

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

Tôi không gợi ý liệu bạn có cần các công cụ hủy diệt ảo hay không, mặc dù tôi nghĩ nói chung, đó là một thực hành tốt để có chúng. Tôi chỉ nêu ra lý do bạn có thể gặp sự cố nếu lớp cơ sở (/ struct) của bạn không có vtable và lớp dẫn xuất (/ struct) của bạn không và bạn xóa một đối tượng thông qua lớp cơ sở (/ struct) con trỏ. Trong trường hợp này, địa chỉ bạn chuyển đến thói quen miễn phí của heap là không hợp lệ và do đó là lý do cho sự cố.

Nếu bạn chạy đoạn mã trên, bạn sẽ thấy rõ khi sự cố xảy ra. Khi con trỏ này của lớp cơ sở (/ struct) khác với con trỏ của lớp dẫn xuất (/ struct), bạn sẽ gặp phải vấn đề này. Trong mẫu ở trên, struct a và b không có vtables. cấu trúc c và d có vtables. Do đó, một con trỏ a hoặc b đến đối tượng ac hoặc d sẽ được cố định để giải thích cho vtable. Nếu bạn vượt qua con trỏ a hoặc b này để xóa nó sẽ bị sập do địa chỉ không hợp lệ với thói quen miễn phí của heap.

Nếu bạn có kế hoạch xóa các thể hiện dẫn xuất có vtables khỏi các con trỏ lớp cơ sở, bạn cần đảm bảo lớp cơ sở có vtable. Một cách để làm điều đó là thêm một hàm hủy ảo, dù sao bạn cũng có thể muốn dọn sạch tài nguyên.


0

Một định nghĩa cơ bản về virtual là nó xác định xem một hàm thành viên của một lớp có thể bị quá tải trong các lớp dẫn xuất của nó hay không.

D-tor của một lớp về cơ bản được gọi là ở cuối phạm vi, nhưng có một vấn đề, ví dụ khi chúng ta định nghĩa một thể hiện trên Heap (phân bổ động), chúng ta nên xóa nó bằng tay.

Ngay khi lệnh được thực thi, hàm hủy của lớp cơ sở được gọi, nhưng không phải cho lệnh dẫn xuất.

Một ví dụ điển hình là khi, trong lĩnh vực điều khiển, bạn phải thao tác với tác nhân, bộ truyền động.

Ở cuối phạm vi, nếu kẻ hủy diệt của một trong các yếu tố sức mạnh (Thiết bị truyền động), không được gọi, sẽ có hậu quả nghiêm trọng.

#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

Bất kỳ lớp nào được kế thừa công khai, đa hình hay không, nên có một hàm hủy ảo. Nói cách khác, nếu nó có thể được trỏ bởi một con trỏ lớp cơ sở, thì lớp cơ sở của nó sẽ có một hàm hủy ảo.

Nếu ảo, hàm hủy của lớp dẫn xuất được gọi, thì hàm tạo của lớp cơ sở. Nếu không ảo, chỉ có hàm hủy lớp cơ sở được gọi.


Tôi muốn nói rằng điều này chỉ cần thiết "nếu nó có thể được trỏ đến bởi một con trỏ lớp cơ sở" có thể bị xóa công khai. Nhưng tôi đoán sẽ không hại gì khi có thói quen thêm các máy ảo trong trường hợp chúng có thể trở nên cần thiết sau này.
gạch dưới
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.