Menu
CoddyTech

Đa hình lúc biên dịch và khi chạy

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 56 trên 104.

Tính đa hình có nghĩa là “nhiều hình thức” và là một khái niệm cốt lõi trong lập trình hướng đối tượng, cho phép xử lý các đối tượng theo cách thống nhất trong khi chúng hoạt động khác nhau. C++ hỗ trợ hai kiểu đa hình riêng biệt, mỗi kiểu được xác định ở một giai đoạn khác nhau khi chương trình chạy.

Đa hình lúc biên dịch (compile-time polymorphism), còn gọi là đa hình tĩnh (static polymorphism), được trình biên dịch xử lý trước khi chương trình chạy. Trình biên dịch xác định chính xác hàm (function) nào cần gọi dựa trên chữ ký hàm. Cách này bao gồm nạp chồng hàm và template:

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // Trình biên dịch chọn print(int)
print(3.14);   // Trình biên dịch chọn print(double)

Đa hình động (còn gọi là đa hình thời gian chạy) được xác định khi chương trình đang chạy. Việc quyết định gọi hàm nào phụ thuộc vào kiểu của đối tượng thực tế, không phụ thuộc vào kiểu của con trỏ hoặc tham chiếu. Ta thực hiện điều này thông qua các hàm ảo:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing circle" << std::endl; }
};

Shape* s = new Circle();
s->draw();  // Xác định khi chạy: "Drawing circle"

Sự đánh đổi chính: đa hình lúc biên dịch (compile-time polymorphism) không gây thêm chi phí khi chạy vì các quyết định được đưa ra trong quá trình biên dịch, còn đa hình lúc chạy (runtime polymorphism) tốn thêm một khoản nhỏ do phải tra cứu vtable, nhưng linh hoạt hơn khi làm việc với các đối tượng có kiểu chưa được xác định cho đến khi chương trình chạy.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống máy tính để minh họa song song cả hai kiểu đa hình (polymorphism). Bạn sẽ tạo một hệ thống trong đó đa hình tại thời điểm biên dịch xử lý các kiểu đầu vào khác nhau thông qua nạp chồng hàm, còn đa hình tại thời điểm chạy cho phép hoán đổi linh hoạt các chiến lược tính toán.

Bạn sẽ sắp xếp code trong ba tệp:

  • Calculator.h: Định nghĩa lớp cơ sở Calculator, đại diện cho mọi chiến lược tính toán:
    • Một phương thức ảo calculate(int a, int b) trả về int và in ra: Base calculation: <a> ? <b> (trả về 0)
    • Một hàm hủy ảo
  • Operations.h: Định nghĩa hai lớp máy tính dẫn xuất ghi đè hành vi tính toán:
    • Adder: Ghi đè calculate() để in ra Adding: <a> + <b> và trả về tổng
    • Multiplier: Ghi đè calculate() để in ra Multiplying: <a> * <b> và trả về tích
    Cả hai lớp đều phải dùng từ khóa override.
  • main.cpp: Tạo một hệ thống minh họa cả hai kiểu đa hình. Đọc hai số nguyên đầu vào, mỗi số trên một dòng riêng.

    Trước tiên, minh họa đa hình tại thời điểm biên dịch bằng cách tạo ba hàm display() được nạp chồng:

    • display(int x) in ra: Integer value: <x>
    • display(double x) in ra: Double value: <x>
    • display(const std::string& x) in ra: String value: <x>

    Sau đó, minh họa đa hình tại thời điểm chạy bằng cách tạo một mảng con trỏ Calculator* chứa một Calculator cơ sở, một Adder và một Multiplier. Duyệt qua mảng và gọi calculate() trên từng đối tượng bằng các giá trị đầu vào, rồi in kết quả sau mỗi phép tính.

    Sắp xếp đầu ra như sau:

    === Compile-Time Polymorphism ===
    <display outputs for int, double, string>
    
    === Runtime Polymorphism ===
    <calculate outputs with results>

    Trong phần đa hình tại thời điểm biên dịch, gọi display() với đầu vào thứ nhất dưới dạng số nguyên, sau đó dưới dạng số thực (cùng giá trị nhưng cộng thêm .5), rồi với chuỗi "Result". Khi hoàn tất, hãy giải phóng các đối tượng máy tính đã cấp phát động.

Ví dụ, với đầu vào 10 và 3:

=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result

=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30

Hãy chú ý cách trình biên dịch chọn hàm nạp chồng display() phù hợp dựa trên kiểu đối số (quyết định tại thời điểm biên dịch), trong khi phương thức calculate() phù hợp được xác định dựa trên kiểu thực tế của đối tượng tại thời điểm chạy thông qua cơ chế vtable.

Tự làm thử

#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"

// TODO: Tạo ba hàm (function) display() nạp chồng:
// 1. display(int x): in ra "Integer value: <x>"
// 2. display(double x): in ra "Double value: <x>"
// 3. display(const std::string& x): in ra "String value: <x>"



int main() {
    // Đọc hai giá trị đầu vào là số nguyên
    int a, b;
    std::cin >> a;
    std::cin >> b;
    
    // === Compile-Time Polymorphism ===
    std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
    // TODO: Gọi display() với:
    // a là số nguyên
    // a là số thực kiểu double (cộng thêm 0.5)
    // chuỗi "Result"
    
    
    std::cout << std::endl;
    
    // === Runtime Polymorphism ===
    std::cout << "=== Runtime Polymorphism ===" << std::endl;
    // TODO: Tạo mảng gồm 3 con trỏ Calculator*:
    // một Calculator cơ sở
    // một Adder
    // - một Multiplier
    
    // TODO: Duyệt qua mảng (array), gọi calculate(a, b) cho từng phần tử,
    // rồi in "Result: <return_value>" sau mỗi phép tính
    
    
    // TODO: Giải phóng bộ nhớ đã cấp phát động
    
    
    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