Hàm ảo và VTable
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 52 trên 104.
Từ khóa virtual giải quyết vấn đề mà chúng ta đã gặp trong bài học trước. Khi bạn khai báo một phương thức (method) là virtual, C++ sẽ xác định phiên bản nào cần gọi dựa trên kiểu thực tế của đối tượng tại thời điểm chạy, chứ không phải kiểu con trỏ.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Đầu ra: Woof!Giờ thì phương thức đúng đã được gọi! Từ khóa override là tùy chọn nhưng được khuyến khích dùng. Từ khóa này cho trình biên dịch biết bạn định ghi đè một hàm ảo, giúp phát hiện lỗi nếu các chữ ký hàm không khớp.
Cách hoạt động như thế nào? Khi một lớp có các hàm ảo, trình biên dịch tạo một bảng ảo (vtable). Đây là một bảng tra cứu ẩn chứa các con trỏ đến các hàm ảo của lớp. Mỗi đối tượng có các hàm ảo đều lưu một con trỏ ẩn (vptr) trỏ đến vtable của lớp đó.
Khi bạn gọi một hàm ảo thông qua con trỏ, chương trình sẽ tra cứu địa chỉ hàm phù hợp trong vtable khi chạy. Cách này gọi là điều phối động. Nó làm tăng một chút chi phí so với các lần gọi hàm thông thường, nhưng cho phép hành vi đa hình mạnh mẽ.
Một quy tắc quan trọng: nếu một lớp có các hàm ảo và sẽ được dùng làm lớp cơ sở, hàm hủy của lớp đó cũng nên là hàm ảo. Điều này giúp dọn dẹp đúng cách khi xóa các đối tượng lớp dẫn xuất thông qua con trỏ lớp cơ sở:
class Animal {
public:
virtual ~Animal() {} // Hàm hủy ảo
virtual void speak() {}
};Thử thách
DễHãy xây dựng một hệ thống trình phát media để minh họa sức mạnh của hàm ảo (virtual function) và đa hình lúc chạy (runtime polymorphism). Bạn sẽ tạo một hệ thống phân cấp các loại media, trong đó mỗi loại phát theo cách khác nhau, đồng thời tìm hiểu cách hàm ảo giúp chương trình thực hiện đúng hành vi ngay cả khi truy cập qua con trỏ lớp cơ sở.
Bạn sẽ sắp xếp code trong ba tệp:
Media.h: Định nghĩa lớp cơ sởMedia, đại diện cho mọi loại media có thể phát:- Một thành viên
std::string titlecó phạm vi truy cập protected - Một hàm khởi tạo nhận tiêu đề và lưu tiêu đề đó
- Một phương thức virtual
play()để in ra:Playing media: <title> - Một hàm hủy ảo để in ra:
Media [<title>] destroyed
- Một thành viên
AudioTrack.h: Định nghĩa lớpAudioTrack, kế thừa công khai từMedia:- Một thành viên riêng tư
std::string artist - Một hàm khởi tạo nhận tiêu đề và tên nghệ sĩ, truyền tiêu đề cho lớp cơ sở và lưu tên nghệ sĩ
- Ghi đè
play()bằng từ khóaoverrideđể in ra:Playing audio: <title> by <artist> - Một hàm hủy để in ra:
AudioTrack [<title>] destroyed
- Một thành viên riêng tư
main.cpp: Đọc ba đầu vào, mỗi đầu vào trên một dòng riêng:- Tiêu đề bản nhạc (chuỗi)
- Tên nghệ sĩ (chuỗi)
- Tiêu đề video (chuỗi)
Định nghĩa lớp
VideoCliptrực tiếp trong main.cpp. Lớp này kế thừa công khai từMedia:- Một thành viên riêng tư
int duration(tính bằng giây) - Một hàm khởi tạo nhận tiêu đề và thời lượng (thời lượng mặc định là 120)
- Ghi đè
play()để in ra:Playing video: <title> (<duration>s) - Một hàm hủy để in ra:
VideoClip [<title>] destroyed
Tạo một mảng gồm ba con trỏ
Media*bên trong một phạm vi khối lệnh. Cấp phát động:- Một đối tượng
Mediacơ sở có tiêu đề "Generic Media" - Một đối tượng
AudioTrackcó tiêu đề và tên nghệ sĩ từ đầu vào - Một đối tượng
VideoClipcó tiêu đề video từ đầu vào
Duyệt qua mảng và gọi
play()trên từng con trỏ. Sau đó, xóa tất cả đối tượng theo thứ tự ngược lại. Sau khối lệnh, in ra:Playback complete!
Ví dụ, với các đầu vào Bohemian Rhapsody, Queen và Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Hãy chú ý cách gọi play() thông qua các con trỏ Media* sẽ gọi phương thức của lớp dẫn xuất phù hợp nhờ hàm ảo. Đồng thời, hãy quan sát cách hàm hủy ảo đảm bảo việc dọn dẹp đúng cách: cả hàm hủy của lớp dẫn xuất lẫn lớp cơ sở đều chạy khi xóa thông qua con trỏ lớp cơ sở.
Tự làm thử
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Định nghĩa lớp VideoClip tại đây, kế thừa công khai từ Media
// Thành viên int duration ở chế độ private (tính bằng giây)
// Hàm khởi tạo nhận title và duration (duration mặc định là 120)
// Ghi đè play() để in ra: Playing video: <title> (<duration>s)
// Hàm hủy in ra: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Triển khai hàm khởi tạo
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Lưu duration
}
// TODO: Ghi đè phương thức play()
void play() override {
// TODO: Triển khai phương thức này
}
// TODO: Triển khai hàm hủy
~VideoClip() {
// TODO: Triển khai hàm hủy này
}
};
int main() {
// Đọc các giá trị đầu vào
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Tạo phạm vi khối lệnh bằng dấu ngoặc nhọn
// Trong khối lệnh:
// 1. Tạo mảng gồm ba con trỏ Media*
// 2. Cấp phát động:
// - Một đối tượng Media cơ sở có tiêu đề "Generic Media"
// - Một đối tượng AudioTrack với tiêu đề và nghệ sĩ từ đầu vào
// - Một đối tượng VideoClip với tiêu đề video từ đầu vào
// 3. Lặp qua mảng và gọi play() trên từng con trỏ
// 4. Xóa tất cả đối tượng theo thứ tự ngược lại
// TODO: Sau khối lệnh, in ra: Playback complete!
return 0;
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online