Tại sao lớp cơ sở cần phải có một hàm hủy ảo ở đây nếu lớp dẫn xuất phân bổ không có bộ nhớ động thô?


12

Đoạn mã sau gây rò rỉ bộ nhớ:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

Nó không có ý nghĩa nhiều đối với tôi, vì lớp dẫn xuất phân bổ không có bộ nhớ động thô và unique_ptr tự xử lý. Tôi nhận được hàm hủy của hàm cơ sở lớp đó đang được gọi thay vì dẫn xuất, nhưng tôi không hiểu tại sao đó là vấn đề ở đây. Nếu tôi viết một hàm hủy rõ ràng cho dẫn xuất, tôi sẽ không viết bất cứ điều gì cho vec.


4
Bạn đang giả sử một hàm hủy chỉ tồn tại nếu được viết bằng tay; giả định này bị lỗi: ngôn ngữ cung cấp một ~derived()đại biểu cho hàm hủy của vec. Ngoài ra, bạn đang giả định rằng unique_ptr<base> ptsẽ biết hàm hủy có nguồn gốc. Nếu không có một phương thức ảo, đây không thể là trường hợp. Mặc dù unique_ptr có thể được cung cấp chức năng xóa là tham số mẫu mà không có bất kỳ biểu diễn thời gian chạy nào và tính năng đó không được sử dụng cho mã này.
amon

Chúng ta có thể đặt dấu ngoặc trên cùng một dòng để làm cho mã ngắn hơn không? Bây giờ tôi phải cuộn.
laike9m

Câu trả lời:


14

Khi trình biên dịch thực thi ẩn delete _ptr;bên trong hàm unique_ptrhủy của (nơi _ptrcon trỏ được lưu trữ trong unique_ptr), nó biết chính xác hai điều:

  1. Địa chỉ của đối tượng cần xóa.
  2. Các loại con trỏ đó _ptrlà. Vì con trỏ ở trong unique_ptr<base>, nên có nghĩa _ptrlà thuộc loại base*.

Đây là tất cả các trình biên dịch biết. Vì vậy, cho rằng nó đang xóa một đối tượng kiểu base, nó sẽ gọi ~base().

Vậy ... đâu là nơi mà nó phá hủy derviedvật thể mà nó thực sự chỉ đến? Bởi vì nếu trình biên dịch không biết rằng nó đang phá hủy a derived, thì nó hoàn toàn không biết derived::vec tồn tại , chứ đừng nói rằng nó sẽ bị hủy. Vì vậy, bạn đã phá vỡ đối tượng bằng cách để một nửa của nó không bị phá hủy.

Trình biên dịch không thể giả định rằng bất kỳ base*bị phá hủy thực sự là một derived*; Rốt cuộc, có thể có bất kỳ số lượng các lớp xuất phát từ base. Làm thế nào nó biết loại đặc biệt này base*thực sự trỏ đến?

Những gì trình biên dịch phải làm là tìm ra hàm hủy chính xác để gọi (vâng, derivedcó một hàm hủy. Trừ khi bạn = deletelà một hàm hủy, mỗi lớp có một hàm hủy, cho dù bạn có viết một hay không). Để làm điều này, nó sẽ phải sử dụng một số thông tin được lưu trữ baseđể lấy đúng địa chỉ của mã hủy để gọi, thông tin được thiết lập bởi hàm tạo của lớp thực tế. Sau đó, nó phải sử dụng thông tin này để chuyển đổi base*một con trỏ thành địa chỉ của derivedlớp tương ứng (có thể có hoặc không ở một địa chỉ khác. Vâng, thực sự). Và sau đó nó có thể gọi hàm hủy đó.

Cơ chế đó tôi vừa mô tả? Nó thường được gọi là "công văn ảo": aka, điều đó xảy ra bất cứ khi nào bạn gọi một hàm được đánh dấu virtualkhi bạn có một con trỏ / tham chiếu đến một lớp cơ sở.

Nếu bạn muốn gọi một hàm lớp dẫn xuất khi tất cả những gì bạn có là một con trỏ / tham chiếu lớp cơ sở, thì hàm đó phải được khai báo virtual. Phá hủy về cơ bản là không khác nhau về vấn đề này.


0

Di sản

Toàn bộ điểm kế thừa là chia sẻ một giao diện và giao thức chung giữa nhiều triển khai khác nhau sao cho một thể hiện của lớp dẫn xuất có thể được xử lý giống hệt với bất kỳ trường hợp nào khác từ bất kỳ loại dẫn xuất nào khác.

Trong kế thừa C ++ cũng mang đến chi tiết triển khai, đánh dấu (hoặc không đánh dấu) hàm hủy là ảo là một chi tiết triển khai như vậy.

Chức năng đóng sách

Bây giờ khi một hàm, hoặc bất kỳ trường hợp đặc biệt nào của nó như hàm tạo hoặc hàm hủy được gọi, trình biên dịch phải chọn thực hiện hàm nào có nghĩa. Sau đó, nó phải tạo mã máy theo ý định này.

Cách đơn giản nhất để làm việc này là chọn hàm tại thời gian biên dịch và phát ra mã máy vừa đủ sao cho bất kể giá trị nào, khi đoạn mã đó thực thi, nó luôn chạy mã cho hàm. Điều này làm việc tuyệt vời ngoại trừ thừa kế.

Nếu chúng ta có một lớp cơ sở với một hàm (có thể là bất kỳ hàm nào, bao gồm hàm tạo hoặc hàm hủy) và mã của bạn gọi một hàm trên nó, điều này có nghĩa là gì?

Lấy từ ví dụ của bạn, nếu bạn gọi initialize_vector()trình biên dịch phải quyết định xem bạn thực sự có ý định gọi việc thực hiện được tìm thấy Basehay thực hiện được tìm thấy trong Derived. Có hai cách để quyết định điều này:

  1. Đầu tiên là quyết định rằng vì bạn đã gọi từ một Baseloại, bạn có nghĩa là việc thực hiện Base.
  2. Thứ hai là quyết định rằng vì loại thời gian chạy của giá trị được lưu trữ trong Basegiá trị được nhập có thể là Basehoặc Derivedquyết định về cuộc gọi sẽ thực hiện, phải được thực hiện trong thời gian chạy khi được gọi (mỗi lần nó được gọi).

Trình biên dịch tại thời điểm này bị lẫn lộn, cả hai tùy chọn đều có giá trị như nhau. Đây là khi virtualđi vào hỗn hợp. Khi có từ khóa này, trình biên dịch chọn tùy chọn 2 trì hoãn quyết định giữa tất cả các triển khai có thể cho đến khi mã đang chạy với một giá trị thực. Khi từ khóa này vắng mặt, trình biên dịch chọn tùy chọn 1 vì đó là hành vi bình thường khác.

Trình biên dịch vẫn có thể chọn tùy chọn 1 trong trường hợp gọi hàm ảo. Nhưng chỉ khi nó có thể chứng minh rằng đây luôn là trường hợp.

Nhà xây dựng và phá hủy

Vậy tại sao chúng ta không chỉ định một Nhà xây dựng ảo?

Trực giác hơn làm thế nào trình biên dịch sẽ chọn giữa các triển khai giống hệt của hàm tạo cho DerivedDerived2? Điều này khá đơn giản, nó không thể. Không có giá trị tồn tại từ đó trình biên dịch có thể tìm hiểu những gì thực sự được dự định. Không có giá trị trước vì đó là công việc của nhà xây dựng.

Vậy tại sao chúng ta cần chỉ định một hàm hủy ảo?

Trực giác hơn làm thế nào trình biên dịch sẽ chọn giữa các triển khai cho BaseDerived? Chúng chỉ là các cuộc gọi chức năng, vì vậy hành vi gọi chức năng xảy ra. Nếu không có hàm hủy ảo được khai báo, trình biên dịch sẽ quyết định liên kết trực tiếp với hàm Basehủy bất kể loại thời gian chạy giá trị.

Trong nhiều trình biên dịch, nếu dẫn xuất không khai báo bất kỳ thành viên dữ liệu nào, cũng không kế thừa từ các loại khác, hành vi trong ~Base()sẽ phù hợp, nhưng nó không được bảo đảm. Nó sẽ hoạt động hoàn toàn bằng cách tình cờ, giống như đứng trước súng phun lửa chưa được đốt cháy. Bạn ổn trong một thời gian.

Cách chính xác duy nhất để khai báo bất kỳ loại cơ sở hoặc giao diện nào trong C ++ là khai báo một hàm hủy ảo, để hàm hủy chính xác được gọi cho bất kỳ trường hợp cụ thể nào của hệ thống phân cấp loại đó. Điều này cho phép hàm có kiến ​​thức nhất về thể hiện để dọn sạch thể hiện đó một cách chính xác.

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.