2008-11-07 36 views

Trả lời

166

Thậm chí còn quan trọng hơn đối với 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, chứ không phải là một con trỏ tới việc triển khai thực hiện cụ thể. Khi họ đến để xóa nó, nếu destructor là không ảo, họ sẽ gọi destructor của giao diện (hoặc trình biên dịch cung cấp mặc định, nếu bạn không chỉ định một), không phải là destructor 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

'xóa p' gọi hành vi không xác định. Nó không được bảo đảm để gọi 'Giao diện :: ~ Giao diện'. – Mankarse

+0

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

+12

@Wallacoloo: Nó không được xác định vì '[expr.delete] /': '... nếu kiểu tĩnh của đối tượng cần xóa khác với kiểu động của nó, ... kiểu tĩnh sẽ có một destructor ảo hoặc hành vi không xác định. ... '. Nó vẫn sẽ không được xác định nếu Derived sử dụng một destructor ngầm được tạo ra. – Mankarse

6

Có nó luôn luôn là quan trọng. Các lớp có nguồn gốc có thể cấp phát bộ nhớ hoặc giữ tham chiếu tới các tài nguyên khác cần được dọn dẹp khi đối tượng bị hủy. Nếu bạn không cung cấp cho giao diện của bạn/lớp trừu tượng destructors ảo, sau đó mỗi khi bạn xóa một thể hiện lớp dẫn xuất thông qua một lớp cơ sở xử lý destructor 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 
+0

Đúng, trong thực tế trong ví dụ đó, nó có thể không chỉ rò rỉ bộ nhớ, nhưng có thể sụp đổ: -/ –

+1

Tại sao nó sẽ sụp đổ? –

5

Kevin, trước hết, xin đừng lấy nhảy của tôi trên bình luận của bạn trong cuộc thảo luận trước đó của chúng tôi cá nhân, tôi không có ý định để trở nên quá khắc nghiệt. Dù sao, câu trả lời chính cho câu hỏi.

Không phải là luôn yêu cầu nhưng tôi thấy đó là phương pháp hay. Những gì nó làm, là cho phép một đối tượng có nguồn gốc được xóa một cách an toàn thông qua một con trỏ của một loại cơ sở.

Vì vậy, ví dụ:

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

là vô hình thành nếu cơ sở không có một destructor ảo, bởi vì nó sẽ cố gắng để xóa các đối tượng như thể nó là một Base *.

+0

Không phạm tội. Tôi nghĩ rằng bạn đã thực hiện một số điểm tốt và nghĩ rằng một câu hỏi trực tiếp hơn sẽ cung cấp sự giúp đỡ tương tự cho người khác. Cảm ơn. – Kevin

+0

Không có vấn đề gì, đó là vẻ đẹp của tràn ngăn xếp, mọi người đều có cơ hội được cả sinh viên và giáo viên :) –

+0

bạn đã thực hiện một số điểm tốt :) –

5

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

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

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

Animal* pAnimal = GetAnimal(); 
delete pAnimal; 

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

Lý do khiến cho destructor ảo trong lớp cơ sở là nó đơn giản loại bỏ sự lựa chọn từ các lớp dẫn xuất. Destructor của họ trở thành ảo theo mặc định.

+2

Tôi * chủ yếu * đồng ý với bạn, vì * thường * khi xác định thứ bậc bạn muốn có thể tham chiếu đến đối tượng dẫn xuất bằng cách sử dụng con trỏ lớp cơ sở/tham chiếu. Nhưng đó không phải là * luôn luôn * trường hợp, và trong những trường hợp khác, nó có thể đủ để bảo vệ lớp cơ sở được bảo vệ thay thế. –

+0

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

+1

@JaredPar: Đúng vậy, nhưng ít nhất bạn có thể chịu trách nhiệm trong mã của riêng bạn - điều khó khăn là đảm bảo rằng * mã khách * không thể làm cho 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 đó.) –

34

Câu trả lời cho câu hỏi của bạn thường, nhưng không phải lúc nào. 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ỏ đến nó (hoặc nếu nó nói như vậy trong tài liệu của nó), bạn được tự do không khai báo một destructor ảo.

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

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

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

+0

Chìa khóa để làm cho câu trả lời của bạn hoạt động là "mà 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, việc xóa sẽ được gọi trên lớp giao diện. –

+0

Như John đã nêu ở trên, điều bạn cho là khá nguy hiểm.Bạn đang dựa vào giả định rằng khách hàng 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 phải là ảo hóa là làm cho lớp của lớp trừu tượng được bảo vệ. – Michel

+0

Michel, tôi đã nói như vậy :) "Nếu bạn làm điều đó, bạn làm cho destructor của bạn được bảo vệ. 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ỏ đến 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 "bạn không thể làm ...". Tôi không thấy bất kỳ mối nguy hiểm nào –

20

tôi quyết định làm 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 sẽ giúp bạn quyết định loại phá hủy nào bạn cần:

  1. Lớp học của bạn có được dùng làm lớp cơ sở không?
    • Không: Khai báo công khai phá hủy không ảo để tránh v-con trỏ trên từng đối tượng của lớp *.
    • Có: Đọc câu hỏi tiếp theo.
  2. Tóm tắt lớp cơ sở của bạn phải không? (Ví dụ: bất kỳ phương pháp tinh khiết ảo?)
    • Số: Cố gắng làm cho lớp cơ sở trừu tượng của bạn bằng cách thiết kế lại hệ thống phân cấp lớp học 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 con trỏ cơ sở không?
    • Không: Khai báo trình phá hủy ảo được bảo vệ để ngăn việc sử dụng không mong muốn.
    • Có: Khai báo hủy diệt ảo công khai (không có phí trong trường hợp này).

Tôi hy vọng điều này sẽ hữu ích. Đ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 (tức là không phân lớp), vì vậy trong trường hợp bạn quyết định khai báo destructor của bạn không ảo và công khai, hãy nhớ một cách rõ ràng cảnh báo các lập trình viên của bạn chống lại việc phát sinh từ lớp của bạn.

Tài liệu tham khảo:

+9

Câu trả lời này là một phần lỗi thời, bây giờ có một từ khóa cuối cùng trong C++. –

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 trên, nhưng nếu destructor lớp cơ sở là không ảo, destructor chỉ lớp cơ sở sẽ được gọi và tất cả các dữ liệu trong lớp có nguồn gốc sẽ vẫn phục hồi rồi.

Các vấn đề liên quan