Tại sao tôi nên khai báo một hàm hủy ảo cho một lớp trừu tượng trong C ++?


165

Tôi biết rằng đó là một cách thực hành tốt để khai báo các hàm hủy ảo cho các lớp cơ sở trong C ++, nhưng nó có luôn luôn quan trọng để khai báo các hàm virtualhủy ngay cả đối với các lớp trừu tượng có chức năng như giao diện không? Vui lòng cung cấp một số lý do và ví dụ tại sao.

Câu trả lời:


196

Nó thậm chí còn quan trọng hơn đối với một giao diện. Bất kỳ người dùng nào trong lớp của bạn có thể sẽ giữ một con trỏ tới giao diện, không phải là một con trỏ để triển khai cụ thể. Khi họ đến để xóa nó, nếu hàm hủy không phải là ảo, họ sẽ gọi hàm hủy của giao diện (hoặc mặc định do trình biên dịch cung cấp, nếu bạn không chỉ định một), không phải là hàm hủy của lớp dẫn xuất. Rò rỉ bộ nhớ tức thì.

Ví dụ

class Interface
{
   virtual void doSomething() = 0;
};

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

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete pgọi hành vi không xác định. Nó không được đảm bảo để gọi Interface::~Interface.
Mankude

@Mankude: bạn có thể giải thích nguyên nhân khiến nó không được xác định? Nếu Derogen không thực hiện hàm hủy của chính nó, nó vẫn sẽ là hành vi không xác định?
Ponkadoodle

14
@Wallacoloo: Không xác định được vì [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Nó vẫn sẽ không được xác định nếu Derogen sử dụng một hàm hủy được tạo ngầm.
Mankude

37

Câu trả lời cho câu hỏi của bạn là thường xuyên, nhưng không phải lúc nào cũng vậy. Nếu lớp trừu tượng của bạn cấm khách hàng gọi xóa trên một con trỏ tới nó (hoặc nếu nó nói như vậy trong tài liệu của nó), bạn có quyền không khai báo một hàm hủy ảo.

Bạn có thể cấm khách hàng gọi xóa trên một con trỏ tới nó bằng cách làm cho hàm hủy của nó được bảo vệ. Làm việc như thế này, hoàn toàn an toàn và hợp lý khi bỏ qua một hàm hủy ảo.

Cuối cùng, bạn sẽ không có bảng phương thức ảo và cuối cùng báo hiệu cho khách hàng của bạn ý định làm cho nó không bị xóa thông qua một con trỏ tới nó, vì vậy bạn thực sự có lý do để không khai báo ảo trong những trường hợp đó.

[Xem mục 4 trong bài viết này: http://www.gotw.ca/publications/mill18.htm ]


Chìa khóa để làm cho câu trả lời của bạn hoạt động là "trên đó xóa không được gọi." Thông thường nếu bạn có một lớp cơ sở trừu tượng được thiết kế để trở thành một giao diện, xóa sẽ được gọi trên lớp giao diện.
John Dibling

Như John ở trên đã chỉ ra, những gì bạn đề xuất là khá nguy hiểm. Bạn đang dựa vào giả định rằng các máy khách của giao diện của bạn sẽ không bao giờ phá hủy một đối tượng chỉ biết loại cơ sở. Cách duy nhất bạn có thể đảm bảo rằng nếu nó không ảo là làm cho dtor của lớp trừu tượng được bảo vệ.
Michel

Michel, tôi đã nói như vậy :) "Nếu bạn làm điều đó, bạn sẽ bảo vệ hàm hủy của mình. Nếu bạn làm như vậy, khách hàng sẽ không thể xóa bằng cách sử dụng một con trỏ tới giao diện đó." và thực sự nó không phụ thuộc vào khách hàng, nhưng nó phải thực thi nó nói với khách hàng rằng "bạn không thể làm ...". Tôi không thấy bất kỳ nguy hiểm nào
Johannes Schaub - litb

tôi đã sửa chữa từ ngữ kém trong câu trả lời của tôi bây giờ. bây giờ tuyên bố rõ ràng rằng nó không phụ thuộc vào khách hàng. thực ra tôi nghĩ rằng rõ ràng việc dựa vào khách hàng đang làm gì đó là điều không nên. cảm ơn :)
Julian Schaub - litb

2
+1 để đề cập đến các hàm hủy được bảo vệ, đó là "lối thoát" khác của vấn đề vô tình gọi sai hàm hủy khi xóa một con trỏ đến một lớp cơ sở.
j_random_hacker

23

Tôi quyết định thực hiện một số nghiên cứu và cố gắng tóm tắt câu trả lời của bạn. Các câu hỏi sau đây sẽ giúp bạn quyết định loại hủy nào bạn cần:

  1. Là lớp học của bạn dự định sẽ được sử dụng như một lớp cơ sở?
    • Không: Khai báo hàm hủy không ảo công khai để tránh con trỏ v trên mỗi đối tượng của lớp * .
    • Có: Đọc câu hỏi tiếp theo.
  2. Là lớp cơ sở của bạn trừu tượng? (tức là bất kỳ phương thức thuần ảo nào?)
    • Không: Cố gắng làm cho lớp cơ sở của bạn trừu tượng bằng cách thiết kế lại hệ thống phân cấp lớp của bạn
    • Có: Đọc câu hỏi tiếp theo.
  3. Bạn có muốn cho phép xóa đa hình thông qua một con trỏ cơ sở?
    • Không: Khai báo hàm hủy ảo được bảo vệ để ngăn chặn việc sử dụng không mong muốn.
    • Có: Khai báo hàm hủy ảo công khai (không có phí trong trường hợp này).

Tôi hi vọng cái này giúp được.

* Điều quan trọng cần lưu ý là không có cách nào trong C ++ để đánh dấu một lớp là cuối cùng (nghĩa là không thể phân lớp), vì vậy trong trường hợp bạn quyết định tuyên bố kẻ hủy diệt của mình không ảo và công khai, hãy nhớ cảnh báo rõ ràng với các lập trình viên của bạn chống lại xuất phát từ lớp học của bạn.

Người giới thiệu:


11
Câu trả lời này đã lỗi thời một phần, hiện đã có từ khóa cuối cùng trong C ++.
Étienne

10

Vâng, nó luôn luôn quan trọng. Các lớp dẫn xuất có thể phân bổ bộ nhớ hoặc giữ tham chiếu đến các tài nguyên khác sẽ cần được dọn sạch khi đối tượng bị phá hủy. Nếu bạn không cung cấp các hàm hủy ảo giao diện / lớp trừu tượng, thì mỗi khi bạn xóa một thể hiện của lớp dẫn xuất thông qua một lớp cơ sở thì hàm hủy của lớp dẫn xuất của bạn sẽ không được gọi.

Do đó, bạn đang mở ra khả năng rò rỉ bộ nhớ

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

Đúng, trong thực tế trong ví dụ đó, nó có thể không chỉ bị rò rỉ bộ nhớ, mà có thể bị sập: - /
Evan Teran

7

Nó không phải luôn luôn được yêu cầu, nhưng tôi thấy đó là một thực hành tốt. Những gì nó làm, là nó cho phép một đối tượng dẫn xuất được xóa an toàn thông qua một con trỏ của một loại cơ sở.

Ví dụ:

Base *p = new Derived;
// use p as you see fit
delete p;

không được định dạng nếu Basekhông có hàm hủy ảo, vì nó sẽ cố xóa đối tượng như thể nó là a Base *.


bạn không muốn sửa boost :: shared_pulum p (Derogen mới) để trông giống như boost :: shared_pulum <Base> p (new Derogen); ? có lẽ ppl sẽ hiểu câu trả lời của bạn sau đó và bỏ phiếu
Johannes Schaub - litb

EDIT: "Đã mã hóa" một vài phần để hiển thị dấu ngoặc góc, như litb đề xuất.
j_random_hacker

@EvanTeran: Tôi không chắc liệu điều này có thay đổi hay không vì câu trả lời ban đầu được đăng (tài liệu Boost tại boost.org/doc/libs/1_52_0/libs/smart_ptr/ Shared_ptr.htm cho thấy nó có thể có), nhưng điều đó không đúng những ngày shared_ptrnày sẽ cố gắng xóa đối tượng như thể nó là một Base *- nó nhớ loại vật mà bạn đã tạo ra nó. Xem liên kết được tham chiếu, cụ thể là bit có nội dung "Hàm hủy sẽ gọi xóa với cùng một con trỏ, hoàn thành với loại ban đầu của nó, ngay cả khi T không có hàm hủy ảo hoặc bị hủy."
Stuart Golodetz

@StuartGolodetz: Hmm, bạn có thể đúng, nhưng tôi thực sự không chắc chắn. Nó vẫn có thể bị hình thành trong bối cảnh này do thiếu bộ hủy ảo. Thật đáng để xem qua.
Evan Teran

@EvanTeran: Trong trường hợp nó hữu ích - stackoverflow.com/questions/3899790/spl-ptr-magic .
Stuart Golodetz

5

Đó không chỉ là thực hành tốt. Đó là quy tắc số 1 cho bất kỳ hệ thống phân cấp lớp nào.

  1. Lớp cơ sở nhất của hệ thống phân cấp trong C ++ phải có hàm hủy ảo

Bây giờ cho Tại sao. Lấy thứ bậc động vật điển hình. Các hàm hủy ảo đi qua công văn ảo giống như bất kỳ lệnh gọi phương thức nào khác. Lấy ví dụ sau.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Giả sử rằng Animal là một lớp trừu tượng. Cách duy nhất mà C ++ biết hàm hủy thích hợp để gọi là thông qua phương thức ảo. Nếu hàm hủy không phải là ảo thì nó sẽ chỉ gọi hàm hủy của Animal và không hủy bất kỳ đối tượng nào trong các lớp dẫn xuất.

Lý do làm cho hàm hủy ảo trong lớp cơ sở là vì nó đơn giản loại bỏ sự lựa chọn khỏi các lớp dẫn xuất. Kẻ hủy diệt của chúng trở thành ảo theo mặc định.


2
Tôi hầu như đồng ý với bạn, bởi vì thông thường khi xác định một hệ thống phân cấp bạn muốn có thể tham chiếu đến một đối tượng dẫn xuất bằng cách sử dụng một con trỏ / tham chiếu lớp cơ sở. Nhưng điều đó không phải lúc nào cũng đúng, và trong những trường hợp khác, nó có thể đủ để làm cho lớp cơ sở được bảo vệ thay thế.
j_random_hacker

@j_random_hacker làm cho nó được bảo vệ sẽ không bảo vệ bạn khỏi những lần xóa nội bộ không chính xác
JaredPar

1
@JaredPar: Điều đó đúng, nhưng ít nhất bạn có thể chịu trách nhiệm về mã của riêng mình - điều khó khăn là đảm bảo rằng mã máy khách không thể khiến mã của bạn phát nổ. (Tương tự, làm cho thành viên dữ liệu riêng tư không ngăn mã nội bộ làm điều gì đó ngu ngốc với thành viên đó.)
j_random_hacker

@j_random_hacker, rất tiếc khi trả lời bài đăng trên blog nhưng nó thực sự phù hợp với kịch bản này. blog.msdn.com/jaredpar/archive/2008/03/24/ Cách
JaredPar

@JaredPar: Bài đăng tuyệt vời, tôi đồng ý với bạn 100%, đặc biệt là về việc kiểm tra hợp đồng trong mã bán lẻ. Tôi chỉ có nghĩa là có những trường hợp khi bạn biết bạn không cần một dtor ảo. Ví dụ: các lớp thẻ cho công văn mẫu. Chúng có kích thước 0, bạn chỉ sử dụng tính kế thừa để chỉ ra các chuyên ngành.
j_random_hacker

3

Câu trả lời rất đơn giản, bạn cần nó là ảo nếu không lớp cơ sở sẽ không phải là một lớp đa hình hoàn chỉnh.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Bạn muốn xóa phần trên, nhưng nếu hàm hủy của lớp cơ sở không phải là ảo, chỉ có hàm hủy của lớp cơ sở sẽ được gọi và tất cả dữ liệu trong lớp dẫn xuất sẽ không bị xóa.

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.