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.
#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.
#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.
#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.
#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 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.
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.
- 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 a
virtualdestructor - Always use
overridewhen 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?