2008-11-19 23 views

Trả lời

64

Không có nhu cầu sử dụng một destructor ảo khi bất kỳ dưới đây là đúng:

  • Không có ý định để lấy được các lớp học từ nó
  • Không instantiation trên heap
  • Không có ý định để lưu trữ trong con trỏ của một siêu lớp

Không có lý do cụ thể nào để tránh trừ khi bạn thực sự bị ép vào bộ nhớ.

+0

tổng quan tốt. tôi đã bỏ câu trả lời của tôi ủng hộ điều này. +1 :) –

+15

Đây không phải là câu trả lời hay. "Không cần" khác với "không nên", và "không có ý định" khác với "không thể thực hiện được". –

+5

Ngoài ra thêm: không có ý định xóa một thể hiện thông qua một con trỏ lớp cơ sở. –

23

Tôi khai báo trình phá hủy ảo nếu và chỉ khi tôi có phương pháp ảo. Một khi tôi có các phương thức ảo, tôi không tin tưởng bản thân mình để tránh instantiating nó trên heap hoặc lưu trữ một con trỏ đến lớp cơ sở. Cả hai đều là các hoạt động cực kỳ phổ biến và thường sẽ rò rỉ các tài nguyên âm thầm nếu destructor không được khai báo ảo.

+3

Và, trên thực tế, có một tùy chọn cảnh báo trên gcc cảnh báo chính xác rằng trường hợp (phương pháp ảo nhưng không có dtor ảo). – CesarB

+3

Bạn không phải sau đó chạy các nguy cơ rò rỉ bộ nhớ nếu bạn lấy được từ các lớp học, bất kể bạn có chức năng ảo khác? –

+0

Tôi đồng ý với mag. Việc sử dụng một trình phá hủy ảo và phương thức ảo là các yêu cầu riêng biệt. Ảo destructor cung cấp khả năng cho một lớp để thực hiện dọn dẹp (ví dụ như xóa bộ nhớ, đóng tập tin, vv ...) VÀ cũng đảm bảo các nhà thầu của tất cả các thành viên của nó được gọi. – user48956

6

Cần có trình phá hủy ảo bất cứ khi nào có bất kỳ cơ hội nào delete có thể được gọi trên con trỏ tới đối tượng của lớp con với loại lớp của bạn. Điều này đảm bảo rằng destructor đúng được gọi vào thời gian chạy mà không có trình biên dịch phải biết lớp của một đối tượng trên heap tại thời gian biên dịch. Ví dụ, giả sử B là một lớp con của A:

A *x = new B; 
delete x;  // ~B() called, even though x has type A* 

Nếu mã của bạn không phải là hiệu suất quan trọng, nó sẽ là hợp lý để thêm một destructor ảo để mỗi lớp cơ sở bạn viết, chỉ cho an toàn.

Tuy nhiên, nếu bạn thấy mình delete nhập nhiều đối tượng trong một vòng lặp chặt chẽ, chi phí hoạt động của việc gọi hàm ảo (ngay cả một chức năng trống) có thể đáng chú ý. Trình biên dịch không thể thường xuyên nội tuyến các cuộc gọi này, và bộ vi xử lý có thể có một thời gian khó dự đoán nơi để đi. Nó không chắc điều này sẽ có tác động đáng kể đến hiệu suất, nhưng nó đáng nói đến.

-7

Câu trả lời hiệu suất là câu trả lời hiệu suất duy nhất mà tôi biết là viết tắt của cơ hội là sự thật. Nếu bạn đã đo lường và thấy rằng việc khử ảo hóa các destructors của bạn thực sự làm tăng tốc độ, thì bạn có thể có những thứ khác trong lớp cần tăng tốc, nhưng vào thời điểm này có những cân nhắc quan trọng hơn. Một ngày nào đó ai đó sẽ khám phá ra rằng mã của bạn sẽ cung cấp một lớp cơ sở tốt đẹp cho họ và tiết kiệm cho họ một tuần làm việc. Bạn nên chắc chắn rằng họ làm công việc của tuần đó, sao chép và dán mã của bạn, thay vì sử dụng mã của bạn làm cơ sở. Bạn nên chắc chắn rằng bạn thực hiện một số phương pháp quan trọng của bạn riêng tư để không ai có thể kế thừa từ bạn.

+0

Đa hình chắc chắn sẽ làm chậm mọi thứ. So sánh nó với một tình huống mà chúng ta cần đa hình và chọn không, nó sẽ thậm chí còn chậm hơn. Ví dụ: chúng tôi thực hiện tất cả các logic tại destructor lớp cơ sở, sử dụng RTTI và một câu lệnh switch để làm sạch tài nguyên. – sep

+1

Trong C++, bạn không phải chịu trách nhiệm ngăn tôi kế thừa từ các lớp học mà bạn đã ghi lại không phù hợp để sử dụng làm lớp cơ sở. Đó là trách nhiệm của tôi khi sử dụng thừa kế một cách thận trọng. Trừ khi hướng dẫn phong cách nhà nói khác, tất nhiên. –

+1

... chỉ cần làm cho destructor ảo không có nghĩa là lớp sẽ nhất thiết phải hoạt động chính xác như một lớp cơ sở. Vì vậy, đánh dấu nó ảo "chỉ vì", thay vì thực hiện đánh giá đó, là viết một kiểm tra mã của tôi không thể tiền mặt. –

1

Tôi thường khai báo destructor ảo, nhưng nếu bạn có mã quan trọng hiệu suất được sử dụng trong vòng lặp bên trong, bạn có thể muốn tránh tra cứu bảng ảo. Điều đó có thể quan trọng trong một số trường hợp, như kiểm tra va chạm. Nhưng hãy cẩn thận về cách bạn phá hủy các đối tượng đó nếu bạn sử dụng thừa kế, hoặc bạn sẽ tiêu diệt chỉ một nửa của đối tượng.

Lưu ý rằng tra cứu bảng ảo xảy ra cho một đối tượng nếu bất kỳ phương pháp nào trên đối tượng đó là ảo. Vì vậy, không có điểm trong việc loại bỏ các đặc tả ảo trên một destructor nếu bạn có các phương pháp ảo khác trong lớp.

57

Để trả lời câu hỏi một cách rõ ràng, tức làkhi nào bạn nên không khai báo trình phá hủy ảo.

C++ '98/'03

Thêm một destructor ảo có thể thay đổi lớp của bạn không bị POD (plain old data) * hoặc tổng hợp để phi POD. Điều này có thể ngăn không cho dự án của bạn biên dịch nếu loại lớp của bạn là tổng hợp được khởi tạo ở đâu đó.

struct A { 
    // virtual ~A(); 
    int i; 
    int j; 
}; 
void foo() { 
    A a = { 0, 1 }; // Will fail if virtual dtor declared 
} 

Thay đổi như vậy cũng có thể gây ra hành vi không xác định khi lớp đang được sử dụng theo cách yêu cầu POD, ví dụ: chuyển nó qua tham số dấu ba chấm, hoặc sử dụng nó với memcpy.

void bar (...); 
void foo (A & a) { 
    bar (a); // Undefined behavior if virtual dtor declared 
} 

[* Loại POD là loại có bảo đảm cụ thể về bố cục bộ nhớ của nó. Tiêu chuẩn thực sự chỉ nói rằng nếu bạn đã sao chép từ một đối tượng có loại POD thành một mảng ký tự (hoặc ký tự chưa ký) và ngược lại, thì kết quả sẽ giống với đối tượng gốc.]

Hiện đại C++

Trong các phiên bản gần đây của C++, khái niệm POD được phân chia giữa bố cục lớp và cách xây dựng, sao chép và hủy.

Đối với trường hợp tỉnh lược, nó không còn undefined behavior nó bây giờ là conditionally-supported với implementation-defined semantics (N3937 - ~ C++ '14 - 5.2.2/7):

... Đi qua một cuộc tranh luận có khả năng đánh giá lại của loại lớp (Điều 9) có một hàm tạo bản sao không tầm thường, một hàm tạo di chuyển không tầm thường hoặc một trình phá hủy tầm thường, không có tham số tương ứng, được hỗ trợ có điều kiện với các ngữ nghĩa được thực hiện.

Tuyên bố một destructor khác hơn =default sẽ có nghĩa là nó không phải là tầm thường (12,4/5)

... Một destructor là tầm thường nếu nó không phải là người dùng cung cấp ...

Các thay đổi khác đối với Modern C++ làm giảm tác động của vấn đề khởi tạo tổng hợp như một hàm tạo có thể được thêm vào:

struct A { 
    A(int i, int j); 
    virtual ~A(); 
    int i; 

    int j; 
}; 
void foo() { 
    A a = { 0, 1 }; // OK 
} 
+1

Bạn nói đúng, và tôi đã sai, hiệu suất không phải là lý do duy nhất. Nhưng điều này cho thấy tôi đã đúng về phần còn lại của nó: các lập trình viên của lớp đã tốt hơn bao gồm mã để ngăn chặn các lớp từ bao giờ được thừa hưởng bởi bất cứ ai khác. –

+1

Hmm ... Tôi đồng ý với điểm này. – sep

+0

Richard thân yêu, bạn có thể vui lòng bình luận thêm một chút về những gì bạn đã viết. Tôi không hiểu điểm của bạn, nhưng có vẻ như điểm duy nhất có giá trị mà tôi đã tìm thấy bằng cách googling) Hoặc có thể bạn có thể đưa ra một liên kết đến một lời giải thích chi tiết hơn? –

5

Chức năng ảo có nghĩa là mọi đối tượng được phân bổ sẽ tăng chi phí bộ nhớ bằng con trỏ bảng chức năng ảo. Vì vậy, nếu chương trình của bạn liên quan đến việc phân bổ một số lượng rất lớn của một số đối tượng, nó sẽ là giá trị tránh tất cả các chức năng ảo để tiết kiệm 32 bit bổ sung cho mỗi đối tượng.

Trong tất cả các trường hợp khác, bạn sẽ tự cứu mình gỡ lỗi để làm cho dtor ảo.

5

Không phải tất cả các lớp C++ đều phù hợp để sử dụng làm lớp cơ sở có tính đa hình động.

Nếu bạn muốn lớp học của bạn phù hợp với đa hình động, thì hàm hủy của nó phải là ảo. Ngoài ra, bất kỳ phương thức nào mà một lớp con có thể tưởng tượng muốn ghi đè (có thể có nghĩa là tất cả các phương thức công cộng, cộng với một số phương thức được bảo vệ được sử dụng bên trong) phải là ảo.

Nếu lớp học của bạn không phù hợp với đa hình động, thì trình phá hủy không được đánh dấu ảo, vì làm như vậy là gây hiểu lầm. Nó chỉ khuyến khích mọi người sử dụng lớp của bạn không chính xác.

Dưới đây là một ví dụ về một lớp học đó sẽ không phù hợp với đa hình năng động, ngay cả khi destructor của nó là ảo:

class MutexLock { 
    mutex *mtx_; 
public: 
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); } 
    ~MutexLock() { mtx_->unlock(); } 
private: 
    MutexLock(const MutexLock &rhs); 
    MutexLock &operator=(const MutexLock &rhs); 
}; 

Toàn bộ điểm của lớp này là ngồi trên stack cho RAII. Nếu bạn đi qua các con trỏ đến các đối tượng của lớp này, hãy để một mình các lớp con của nó, thì bạn đang làm nó sai.

+1

Sử dụng đa hình không ngụ ý xóa đa hình. Có rất nhiều trường hợp sử dụng cho một lớp để có các phương thức ảo chưa có destructor ảo. Hãy xem xét một hộp thoại được định nghĩa tĩnh điển hình, trong bất kỳ bộ công cụ GUI nào. Cửa sổ cha sẽ phá hủy các đối tượng con, và nó biết chính xác kiểu của mỗi đối tượng, nhưng tất cả các cửa sổ con cũng sẽ được sử dụng đa hình ở bất kỳ số địa điểm nào, chẳng hạn như kiểm tra hit, bản vẽ, API trợ năng tìm nạp văn bản cho văn bản- công cụ chuyển văn bản, v.v. –

+4

Đúng, nhưng người hỏi đang hỏi khi nào bạn nên tránh một trình phá hủy ảo. Đối với hộp thoại bạn mô tả, một destructor ảo là vô nghĩa, nhưng IMO không có hại. Tôi không chắc rằng tôi sẽ tự tin rằng tôi sẽ không bao giờ cần phải xóa một hộp thoại bằng cách sử dụng một con trỏ lớp cơ sở - ví dụ tôi có thể trong tương lai muốn cửa sổ cha mẹ của tôi để tạo các đối tượng con bằng cách sử dụng các nhà máy.Vì vậy, nó không phải là một câu hỏi của * tránh * destructor ảo, chỉ là bạn có thể không bận tâm có một. Một destructor ảo trên một lớp học không phù hợp cho derivation * là * có hại, mặc dù, bởi vì nó gây hiểu nhầm. –

3

Nếu bạn có một lớp học rất nhỏ với số lượng lớn các phiên bản, chi phí của một con trỏ vtable có thể tạo sự khác biệt trong việc sử dụng bộ nhớ của chương trình. Miễn là lớp của bạn không có bất kỳ phương pháp ảo nào khác, làm cho destructor không ảo sẽ tiết kiệm được chi phí đó.

0

Khi hoạt động sẽ được thực hiện trên lớp cơ sở và thao tác đó sẽ hoạt động hầu như, phải là ảo. Nếu xóa 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 hầu như và là ảo.

Trình phá hủy không cần phải ảo nếu bạn không định xuất phát từ lớp. Và ngay cả khi bạn làm vậy, một trình phá hủy không ảo được bảo vệ chỉ là tốt nếu việc xóa các con trỏ lớp cơ sở không được yêu cầu.

1

Nếu bạn hoàn toàn tích cực phải đảm bảo rằng lớp học của bạn không có vtable thì bạn cũng không được có destructor ảo.

Đây là trường hợp hiếm hoi, nhưng điều đó xảy ra.

Ví dụ quen thuộc nhất về mẫu làm việc này là các lớp DirectX D3DVECTOR và D3DMATRIX. Đây là các phương thức lớp thay vì các hàm cho đường cú pháp, nhưng các lớp cố ý không có vtable để tránh các chức năng trên vì các lớp này được sử dụng đặc biệt trong vòng lặp bên trong của nhiều ứng dụng hiệu suất cao.

4

Lý do chính đáng để không khai báo destructor là ảo khi điều này tiết kiệm lớp học của bạn khỏi việc thêm bảng chức năng ảo và bạn nên tránh điều đó bất cứ khi nào có thể.

Tôi biết rằng nhiều người thích chỉ luôn khai báo những người phá hoại là ảo, chỉ để ở bên an toàn. Nhưng nếu lớp của bạn không có bất kỳ chức năng ảo nào khác thì thực sự không có điểm nào trong việc có một destructor ảo. Thậm chí nếu bạn đưa lớp của bạn cho những người khác sau đó lấy được các lớp khác từ đó thì họ sẽ không có lý do gì để gọi xóa trên con trỏ đã được upcast cho lớp của bạn - và nếu họ làm vậy thì tôi sẽ coi đây là lỗi. Được rồi, có một ngoại lệ duy nhất, cụ thể là nếu lớp của bạn (mis-) được sử dụng để thực hiện xóa đa hình các đối tượng có nguồn gốc, nhưng sau đó bạn - hoặc những người khác - hy vọng biết rằng điều này đòi hỏi một destructor ảo.

Đặt một cách khác, nếu lớp của bạn có trình phá hủy không phải là ảo thì đây là một tuyên bố rất rõ ràng: "Không sử dụng tôi để xóa các đối tượng có nguồn gốc!"

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