Chuyển đến nội dung

Hàm ảo, vtables & Đa hình

Đi sâu vào tính đa hình của thời gian chạy: cách thức hoạt động cơ bản của công văn ảo, thời điểm sử dụng các lớp ghi đè và cuối cùng, lớp trừu tượng và RTTI.

Đa hình thời gian chạy

Đa hình có nghĩa là "nhiều dạng" — cùng một giao diện hoạt động khác nhau tùy thuộc vào loại đối tượng thực tế. Trong C++, tính đa hình thời gian chạy đạt được thông qua các hàm ảo. Khi bạn gọi một hàm ảo thông qua một con trỏ cơ sở hoặc một tham chiếu, lệnh gọi đó sẽ được gửi đến phần triển khai thực tế của lớp dẫn xuất trong thời gian chạy.

Đây là cơ chế làm cho OOP trở nên mạnh mẽ: bạn có thể viết mã hoạt động trên giao diện lớp cơ sở và nó tự động hoạt động chính xác với bất kỳ lớp dẫn xuất nào - ngay cả những lớp được viết sau mã của bạn.

How vtables Work

Khi một lớp có các hàm ảo, trình biên dịch sẽ tạo một vtable (bảng hàm ảo) — một mảng các con trỏ hàm. Mỗi đối tượng của lớp đó chứa một vptr ẩn (con trỏ tới vtable). Khi một hàm ảo được gọi, chương trình sẽ theo vptr đến vtable, tra cứu con trỏ hàm và gọi thông qua nó. Sự gián tiếp này là lý do tại sao các cuộc gọi ảo có chi phí nhỏ hơn so với các cuộc gọi trực tiếp.

polymorphism.cpp
#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;

    // Pure virtual — no default implementation
    virtual double area() const = 0;

    // Virtual with default implementation
    virtual std::string name() const { return "Shape"; }
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_{r} {}

    double area() const override {
        return 3.14159265 * radius_ * radius_;
    }

    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_{w}, h_{h} {}

    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
    std::cout << s.name() << ": area = " << s.area() << '\n';
}

int main() {
    Circle c{5.0};
    Rectangle r{3.0, 4.0};

    print_info(c);  // Circle: area = 78.5398
    print_info(r);  // Rectangle: area = 12

    // Polymorphic container
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(2.0));
    shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));

    for (const auto& s : shapes) {
        print_info(*s);  // virtual dispatch at runtime
    }
    return 0;
}

Công cụ hủy ảo là bắt buộc

Nếu một lớp có bất kỳ hàm ảo nào thì hàm hủy của nó phải là hàm ảo. Nếu không có nó, việc xóa một đối tượng dẫn xuất thông qua một con trỏ cơ sở sẽ gây ra hành vi không xác định — hàm hủy dẫn xuất không bao giờ chạy, làm rò rỉ tài nguyên.

virtual_destructor.cpp
#include <iostream>
#include <memory>

class Base {
public:
    // Without virtual destructor: UNDEFINED BEHAVIOR on delete
    // virtual ~Base() = default;  // ALWAYS do this!
    ~Base() { std::cout << "~Base\n"; }
};

class Derived : public Base {
    int* data_;
public:
    Derived() : data_{new int[1000]} {
        std::cout << "Derived: allocated\n";
    }
    ~Derived() {
        delete[] data_;
        std::cout << "~Derived: freed\n";
    }
};

int main() {
    Base* ptr = new Derived{};
    delete ptr;  // Only ~Base runs! ~Derived is NOT called!
    // Result: memory leak (data_ never freed)
    // Fix: make ~Base() virtual
    return 0;
}

override and final Keywords

The override keyword tells the compiler you intend to override a base class virtual function. If the signature doesn't match (a common typo), the compiler catches it. The final keyword prevents further overriding of a function or prevents inheritance from a class.

override_final.cpp
#include <iostream>

class Base {
public:
    virtual ~Base() = default;
    virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
    virtual void execute() { std::cout << "Base::execute\n"; }
};

class Middle : public Base {
public:
    // override: compiler checks signature matches Base::process
    void process(int x) const override {
        std::cout << "Middle: " << x * 2 << '\n';
    }

    // final: no further derived class can override execute()
    void execute() final { std::cout << "Middle::execute\n"; }
};

class Bottom : public Middle {
public:
    void process(int x) const override {
        std::cout << "Bottom: " << x * 3 << '\n';
    }

    // ERROR if uncommented: execute() is final in Middle
    // void execute() override { }
};

// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
    void process(int x) const override {
        std::cout << "Leaf: " << x << '\n';
    }
};

// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};

int main() {
    Bottom b;
    Base& ref = b;
    ref.process(10);  // Bottom: 30 (virtual dispatch)
    ref.execute();    // Middle::execute (final version)
    return 0;
}

Lớp trừu tượng & Mẫu NVI

Một lớp có ít nhất một hàm thuần ảo (= 0) là trừu tượng — nó không thể được khởi tạo. Mẫu Giao diện phi ảo (NVI) cung cấp một điểm vào phi ảo công khai gọi một hàm ảo riêng tư, cho phép lớp cơ sở kiểm soát các điều kiện trước/sau.

nvi_pattern.cpp
#include <iostream>
#include <string>

// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
    virtual ~Validator() = default;

    // Public non-virtual interface — controls the workflow
    bool validate(const std::string& input) const {
        if (input.empty()) {
            std::cout << "[Validator] Input is empty\n";
            return false;
        }
        // Delegate to derived class implementation
        bool result = do_validate(input);
        std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
        return result;
    }

private:
    // Pure virtual — derived classes MUST implement
    virtual bool do_validate(const std::string& input) const = 0;
};

class EmailValidator : public Validator {
    bool do_validate(const std::string& input) const override {
        return input.find('@') != std::string::npos
            && input.find('.') != std::string::npos;
    }
};

class LengthValidator : public Validator {
    std::size_t min_len_;
    bool do_validate(const std::string& input) const override {
        return input.length() >= min_len_;
    }
public:
    explicit LengthValidator(std::size_t min) : min_len_{min} {}
};

int main() {
    EmailValidator email_v;
    LengthValidator len_v{5};

    email_v.validate("user@example.com");  // PASS
    email_v.validate("invalid");            // FAIL
    len_v.validate("hi");                   // FAIL
    len_v.validate("hello!");               // PASS
    return 0;
}

RTTI & dynamic_cast

RTTI (Thông tin loại thời gian chạy) cho phép bạn truy vấn loại thực tế của đối tượng đa hình trong thời gian chạy. dynamic_cast(base_ptr) truyền con trỏ cơ sở tới con trỏ dẫn xuất một cách an toàn, trả vềnullptrnếu quá trình truyền không thành công. Để tham khảo,dynamic_castkhông thành công sẽ némstd::bad_cast.

typeid(obj) trả về tham chiếustd::type_infocó thể so sánh để xác định loại thời gian chạy.

Sử dụng RTTI một cách tiết kiệm — việc sử dụngdynamic_castthường xuyên thường báo hiệu vấn đề về thiết kế. Ưu tiên các chức năng ảo cho hành vi dành riêng cho từng loại.

Lỗi thường gặp

Không bao giờ gọi các hàm ảo từ hàm tạo hoặc hàm hủy. Trong quá trình xây dựng, kiểu động của đối tượng là lớp hiện đang được xây dựng, không phải lớp dẫn xuất cuối cùng. Cuộc gọi ảo trongBase::Base()sẽ gọiBase::method()chứ không phảiDerived::method()- ngay cả khi đối tượng được tạo làDerived. Đây là một nguồn lỗi nổi tiếng có thể cực kỳ khó chẩn đoán.

Điểm chính
  • Các hàm ảo cho phép đa hình thời gian chạy thông qua vtables và vptrs
  • Any class with virtual functions must have avirtualdestructor
  • Always use override when overriding — it catches signature mismatches at compile time
  • Pure virtual functions (= 0) make a class abstract and force derived classes to implement them
  • The NVI pattern lets base classes enforce pre/post conditions around customization points
  • Avoid calling virtual functions in constructors/destructors — the derived class is not yet (or no longer) fully constructed

Câu đố — Kiểm tra kiến thức

(20 XP)

1. What happens if a polymorphic base class does NOT have a virtual destructor?

2. What does the`override`keyword do?

3. Why should you avoid calling virtual functions from a constructor?