Đ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.
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ềintvà in ra:Base calculation: <a> ? <b>(trả về 0) - Một hàm hủy ảo
- Một phương thức ả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 raAdding: <a> + <b>và trả về tổngMultiplier: Ghi đècalculate()để in raMultiplying: <a> * <b>và trả về tích
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ộtCalculatorcơ sở, mộtAddervà mộtMultiplier. Duyệt qua mảng và gọicalculate()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: 30Hã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;
}
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