Tính đa hình trong lập trình hướng đối tượng C++
1.5 Polymorphism - Tính đa hình
Tính đa hình đề cập đến khả năng tiếp thu từ nhiều hình thức. Trong bối cảnh của lập trình hướng đối tượng điều này có nghĩa là cùng một tên hàm biểu thị nhiều hàm thành viên của các đối tượng khác nhau. Xét ví dụ sau:
class Class1 {
public:
virtual void f() {
cout << "Function f() in Class1\n";
}
void g() {
cout << "Function g() in Class1\n";
}
};
class Class2 {
public:
virtual void f() {
cout << "Function f() in Class2\n";
}
void g() {
cout << "Function g() in Class2\n";
}
};
class Class3 {
public:
virtual void h() {
cout << "Function h() in Class3\n";
}
};
int main() {
Class1 object1, *p;
Class2 object2;
Class3 object3;
p = &object1;
p->f();
p->g();
p = (Class1*) &object2;
p->f();
p->g();
p = (Class1*) &object3;
p->f(); // possibly abnormal program termination;
p->g();
// p->h(); // h() is not a member of Class1;
return 0;
}
Đầu ra của chương trình này như sau: Function f() in Class1 Function g() in Class1 Function f() in Class2 Function g() in Class1 Function h() in Class3 Function g() in Class1
Chúng ta không nên ngạc nhiên khi p
được khai báo là một con trỏ tới object1
của loại lớp Class1
, sau đó hai hàm thành viên định nghĩa trong Class1
được kích hoạt. Nhưng sau đó p
trở thành con trỏ tới object1
của loại lớp Class2
, sau đó p->f()
kích hoạt một hàm được định nghĩa trong Class2
. Trong khi p->g()
kích hoạt một hàm trong Class1
. Tại sao lại như thế? Sự khác biệt nằm ở thời điểm đưa ra quyết định về hàm được gọi.
Trong trường hợp ràng buộc tĩnh
, quyết định liên quan đến một hàm nào được thực thi sẽ được xác định tại thời điểm trình biên dịch. Trong trường hợp ràng buộc động
, quyết định được trì hoãn cho đến thời gian chạy. Trong C++, ràng buộc động được thi hành bằng cách khai báo một hàm ảo. Trong cách này, nếu hàm ảo được gọi, thì hàm được chọn để thực thi không phụ thuộc vào loại con trỏ được xác định bởi khai báo, nhưng phụ thuộc về loại giá trị mà con trỏ hiện có. Trong ví dụ của chúng tôi, con trỏ p
đã được khai báo kiểu Class1*
. Do đó, nếu p
trỏ đến hàm g()
là không ảo, sau đó thì bất kì ở nơi nào trong chương trình xảy ra lệnh gọi p->g()
,đây luôn luôn được xem như là một lời gọi hàm g()
được định nghĩa trong Class1. Điều này do trình biên dịch đưa ra quyết định dựa trên khai báo kiểu của p
và thực tế là hàmg()
không phải là ảo. Đối với các hàm thành viên ảo sẽ có sự thay đổi trong quá trình chạy chương trình. Lần này, quyết định được đưa ra trong thời gian chạy: nếu một hàm là ảo, hệ thống xem xét loại giá trị của con trỏ hiện tại và gọi hàm thành viên thích hợp. Sau đó khai báo ban đầu của p
là kiểu Class1*
, hàm ảo f()
thuộc Class1 được gọi, trong khi chỉ định p
, địa chỉ của object2
thuộc kiểu Class2
đã được gọi.
Lưu ý răng sau khi p
được gán địa chỉ của object3
, nó vẫn gọi hàm g()
được định nghĩa trong Class1
. Điều này là do hàm g()
không được định nghĩa trong Class3
và hàm g()
được gọi từ Class1
. Nhưng nổ lực gọi p->f()
dẫn đến sự cố chương trình, hoặc đầu ra sai vì C++ chọn hàm ảo đầu tiên trong Class3
, bởi vì hàm f()
được khai báo là ảo trong Class1
để hệ thống cố gắng tìm, không thành công, trong Class3
hàm f() được định nghĩa. Ngoài ra, mặc dù thực tế là p
trỏ đến object3
, lệnh p->h()
dẫn đến lỗi biên dịch, bởi vì trình biên dịch không tìm thấy hàm h()
trong Class1
, trong đó Class1*
vẫn là loại con trỏ p
. Đối với trình biên dịch, nó không có về đề là di hàm h()
được định nghĩa trong Class3 (có thể là ảo hoặc không).
Tính đa hình là một công cụ mạnh mẽ trong lập trình hướng đối tượng, nó là dùng để gửi một tin nhắn tiêu chuẩn tới một vài đối tượng khác nhau nà không chỉ định cách thức xử lí như thế nào. Không cần biết loại của đối tượng là gì. Người nhận có trách nhiệm phiên dịch thông điệp và làm theo nó. Người gửi không phải sửa đổi tin nhắn tùy thuộc vào loại người nhận. Không cần đến mệnh đề switch hoặc if-else. Ngoài ra, các đơn vị mới có thể thêm vào một chương trình phức tạp mà không cần biên dịch lại toàn bộ chương trình.