Menu
CoddyTech

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() {}
};
challenge icon

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 title có 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
  • AudioTrack.h: Định nghĩa lớp AudioTrack, 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óa override để in ra: Playing audio: <title> by <artist>
    • Một hàm hủy để in ra: AudioTrack [<title>] destroyed
  • main.cpp: Đọc ba đầu vào, mỗi đầu vào trên một dòng riêng:
    1. Tiêu đề bản nhạc (chuỗi)
    2. Tên nghệ sĩ (chuỗi)
    3. Tiêu đề video (chuỗi)

    Định nghĩa lớp VideoClip trự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 Media cơ sở có tiêu đề "Generic Media"
    • Một đối tượng AudioTrack có tiêu đề và tên nghệ sĩ từ đầu vào
    • Một đối tượng VideoClip có 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;
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C++ online