Bạn đang đi vào những giả thuyết với những câu trả lời này, vì vậy tôi sẽ cố gắng đưa ra một lời giải thích đơn giản hơn, thực tế hơn cho rõ ràng.
Các mối quan hệ cơ bản của thiết kế hướng đối tượng là hai: IS-A và HAS-A. Tôi đã không làm cho những lên. Đó là những gì họ được gọi.
IS-A chỉ ra rằng một đối tượng cụ thể xác định là thuộc lớp nằm trên nó trong hệ thống phân cấp lớp. Một đối tượng chuối là một đối tượng trái cây nếu nó là một lớp con của lớp trái cây. Điều này có nghĩa là bất cứ nơi nào một lớp trái cây có thể được sử dụng, một quả chuối có thể được sử dụng. Nó không phải là phản xạ, mặc dù. Bạn không thể thay thế một lớp cơ sở cho một lớp cụ thể nếu lớp cụ thể đó được yêu cầu.
Has-a chỉ ra rằng một đối tượng là một phần của lớp tổng hợp và có mối quan hệ sở hữu. Điều đó có nghĩa là trong C ++, nó là một đối tượng thành viên và do đó, onus thuộc lớp sở hữu để loại bỏ nó hoặc trao quyền sở hữu trước khi phá hủy chính nó.
Hai khái niệm này dễ nhận ra hơn trong các ngôn ngữ thừa kế đơn hơn là trong một mô hình đa thừa kế như c ++, nhưng các quy tắc về cơ bản là giống nhau. Sự phức tạp xuất hiện khi danh tính lớp không rõ ràng, chẳng hạn như chuyển một con trỏ lớp Banana vào một hàm lấy con trỏ lớp Fruit.
Các chức năng ảo trước hết là một thứ thời gian. Nó là một phần của đa hình ở chỗ nó được sử dụng để quyết định chức năng nào sẽ chạy tại thời điểm nó được gọi trong chương trình đang chạy.
Từ khóa ảo là một chỉ thị của trình biên dịch để liên kết các hàm theo một thứ tự nhất định nếu có sự mơ hồ về danh tính lớp. Các hàm ảo luôn nằm trong các lớp cha (theo như tôi biết) và chỉ ra cho trình biên dịch rằng việc ràng buộc các hàm thành viên với tên của chúng sẽ diễn ra với hàm lớp con trước và hàm lớp cha sau.
Một lớp Fruit có thể có màu hàm ảo () trả về "KHÔNG" theo mặc định. Hàm màu của lớp Banana () trả về "VÀNG" hoặc "BROWN".
Nhưng nếu hàm lấy con trỏ Fruit gọi color () trên lớp Banana được gửi tới nó - hàm color () nào được gọi? Hàm thường gọi Fruit :: color () cho một đối tượng Fruit.
Đó sẽ là 99% thời gian không phải là những gì đã được dự định. Nhưng nếu Fruit :: color () được khai báo là ảo thì Banana: color () sẽ được gọi cho đối tượng vì hàm color () chính xác sẽ được liên kết với con trỏ Fruit tại thời điểm cuộc gọi. Thời gian chạy sẽ kiểm tra đối tượng mà con trỏ trỏ tới vì nó được đánh dấu ảo trong định nghĩa lớp Fruit.
Điều này khác với việc ghi đè một hàm trong một lớp con. Trong trường hợp đó, con trỏ Fruit sẽ gọi Fruit :: color () nếu tất cả những gì nó biết là con trỏ IS-A của Fruit.
Vì vậy, bây giờ ý tưởng về một "chức năng ảo thuần túy" xuất hiện. Đó là một cụm từ khá đáng tiếc vì độ tinh khiết không có gì để làm với nó. Điều đó có nghĩa là phương thức lớp cơ sở không bao giờ được gọi. Thật vậy, một hàm ảo thuần túy không thể được gọi. Nó vẫn phải được xác định, tuy nhiên. Một chữ ký chức năng phải tồn tại. Nhiều lập trình viên thực hiện một triển khai trống {} để hoàn thiện, nhưng trình biên dịch sẽ tạo ra một nội bộ nếu không. Trong trường hợp đó khi hàm được gọi ngay cả khi con trỏ là Fruit, Banana :: color () sẽ được gọi vì đây là cách thực hiện duy nhất của color ().
Bây giờ là mảnh ghép cuối cùng của câu đố: nhà xây dựng và kẻ hủy diệt.
Các nhà xây dựng ảo thuần túy là bất hợp pháp, hoàn toàn. Đó chỉ là ra ngoài.
Nhưng các hàm hủy ảo thuần hoạt động trong trường hợp bạn muốn cấm tạo một thể hiện của lớp cơ sở. Chỉ các lớp con có thể được khởi tạo nếu hàm hủy của lớp cơ sở là thuần ảo. quy ước là gán nó cho 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
Bạn phải tạo ra một thực hiện trong trường hợp này. Trình biên dịch biết đây là những gì bạn đang làm và đảm bảo rằng bạn làm đúng, hoặc nó phàn nàn rằng nó không thể liên kết với tất cả các chức năng mà nó cần để biên dịch. Các lỗi có thể gây nhầm lẫn nếu bạn không đi đúng hướng về cách bạn đang mô hình hóa hệ thống phân cấp lớp của mình.
Vì vậy, trong trường hợp này, bạn bị cấm tạo các phiên bản của Fruit, nhưng được phép tạo các phiên bản của Banana.
Một lệnh gọi để xóa con trỏ Fruit trỏ đến một thể hiện của Banana sẽ gọi Banana :: ~ Banana () trước và sau đó gọi Fuit :: ~ Fruit (), luôn luôn. Bởi vì không có vấn đề gì, khi bạn gọi một hàm hủy lớp con, hàm hủy của lớp cơ sở phải tuân theo.
Có phải là một mô hình xấu? Nó phức tạp hơn trong giai đoạn thiết kế, vâng, nhưng nó có thể đảm bảo rằng việc liên kết chính xác được thực hiện trong thời gian chạy và chức năng của lớp con được thực hiện khi có sự mơ hồ về chính xác lớp con nào đang được truy cập.
Nếu bạn viết C ++ để bạn chỉ chuyển xung quanh các con trỏ lớp chính xác mà không có con trỏ chung chung hoặc mơ hồ, thì các hàm ảo không thực sự cần thiết. Nhưng nếu bạn yêu cầu các loại linh hoạt trong thời gian chạy (như trong Apple Banana Orange ==> Fruit), các chức năng trở nên dễ dàng và linh hoạt hơn với mã dự phòng ít hơn. Bạn không còn phải viết một hàm cho từng loại trái cây và bạn biết rằng mỗi loại trái cây sẽ phản ứng với màu () với chức năng chính xác của nó.
Tôi hy vọng lời giải thích dài dòng này củng cố khái niệm này hơn là gây nhầm lẫn mọi thứ. Có rất nhiều ví dụ hay để xem xét, xem xét đủ và thực sự chạy chúng và gây rối với chúng và bạn sẽ nhận được nó.