Trong 2 bài trước, chúng ta đã tìm hiểu về tính kế thừa và thực hành làm bài tập về nó. Trong bài này, chúng ta sẽ tiếp tục khám phá một tính chất quan trọng khác trong lập trình hướng đối tượng, đó là tính đa hình.
Tính đa hình là gì?
Đa hình có nghĩa là có nhiều dạng. Đơn giản, đa hình là khả năng của một thông điệp được hiển thị dưới nhiều dạng.
Một ví dụ thực tế để hiểu rõ hơn: Một người có thể có đặc điểm khác nhau cùng một lúc. Giống như một người đàn ông đồng thời là một người cha, một người chồng, một nhân viên. Vì vậy, cùng một người có thể có những hành vi khác nhau trong các tình huống khác nhau. Đây chính là đa hình.
Đa hình được coi là một trong những tính năng quan trọng của lập trình hướng đối tượng.
Phân loại đa hình
Trong ngôn ngữ C++, tính đa hình chủ yếu được chia thành hai loại:
- Compile time Polymorphism.
- Runtime Polymorphism.
Compile time Polymorphism
Tính đa hình này được sử dụng bằng cách nạp chồng hàm hoặc nạp chồng toán tử.
Bạn có thể xem lại về nạp chồng hàm và nạp chồng toán tử tại đây.
Nạp chồng hàm
#include
using namespace std;
class OOP {
public:
// Hàm có một tham số
void func(int x) {
cout << "value of x is " << x << endl;
}
// Hàm cùng tên có một tham số nhưng khác kiểu
void func(double x) {
cout << "value of x is " << x << endl;
}
// Hàm cùng tên nhưng có 2 tham số
void func(int x, int y) {
cout << "value of x and y is " << x << ", " << y << endl;
}
};
int main() {
OOP obj;
obj.func(7);
obj.func(9.132);
obj.func(85, 64);
return 0;
}
Sau khi biên dịch và chạy chương trình, chúng ta nhận được kết quả:
value of x is 7
value of x is 9.132
value of x and y is 85, 64
Trong ví dụ trên, chúng ta chỉ dùng một hàm duy nhất có tên là func
nhưng có thể sử dụng được cho 3 tình huống khác nhau. Đây là một thể hiện của tính đa hình.
Nạp chồng toán tử
#include
using namespace std;
class SoPhuc {
private:
int thuc, ao;
public:
SoPhuc(int thuc = 0, int ao = 0) {
this->thuc = thuc;
this->ao = ao;
}
~SoPhuc() {
this->thuc = 0;
this->ao = 0;
}
SoPhuc operator+(SoPhuc const &obj) {
SoPhuc res;
res.thuc = thuc + obj.thuc;
res.ao = ao + obj.ao;
return res;
}
void print() {
cout << this->thuc << " + " << this->ao << "i" << endl;
}
};
int main() {
SoPhuc c1(10, 5), c2(2, 4);
SoPhuc c3 = c1 + c2;
c3.print();
return 0;
}
Trong ví dụ trên, chúng ta đã nạp chồng lại toán tử cộng.
Định nghĩa của toán tử cộng chỉ dùng cho số nguyên int, nhưng sau khi nạp chồng lại, chúng ta có thể sử dụng chúng cho số phức.
Đây cũng là một thể hiện của tính đa hình.
Runtime Polymorphism
Tính đa hình được thể hiện trong cách nạp chồng toán tử trong kế thừa.
#include
using namespace std;
class base {
public:
virtual void print() {
cout << "print base class" << endl;
}
void show() {
cout << "show base class" << endl;
}
};
class derived : public base {
public:
void print() {
cout << "print derived class" << endl;
}
void show() {
cout << "show derived class" << endl;
}
};
int main() {
base *bptr;
derived d;
bptr = &d;
bptr->print();
bptr->show();
return 0;
}
Sau khi biên dịch và chạy chương trình, chúng ta có kết quả:
print derived class
show base class
Tại sao lại có sự khác biệt như vậy? Tại sao cùng là nạp chồng toán tử trong lớp kế thừa mà kết quả lại khác nhau?
Trong ví dụ trên, mình đã thêm từ khóa virtual
vào hàm print()
trong lớp cơ sở base
. Từ khóa virtual
này dùng để khai báo một hàm là hàm ảo.
Khi khai báo hàm ảo với từ khóa virtual
, nghĩa là hàm này sẽ được gọi theo loại đối tượng được trỏ (hoặc tham chiếu), chứ không phải theo loại của con trỏ (hoặc tham chiếu). Và điều này dẫn đến kết quả khác nhau:
- Nếu không khai báo hàm ảo virtual, trình biên dịch sẽ gọi hàm tại lớp cơ sở
base
. - Nếu dùng hàm ảo virtual, trình biên dịch sẽ gọi hàm tại lớp dẫn xuất
derived
.
Mục đích của hàm ảo là gì?
Các hàm ảo cho phép chúng ta tạo một danh sách các con trỏ lớp cơ sở và các phương thức của bất kỳ lớp dẫn xuất nào mà không cần biết loại đối tượng của lớp dẫn xuất.
Một ví dụ cụ thể:
Chúng ta sẽ bắt đầu với một phần mềm quản lý nhân viên.
Đầu tiên, ta xây dựng một lớp "Nhân viên" sau đó xây dựng các hàm ảo "tăng lương", "chuyển phòng", ... Từ lớp "Nhân viên" này, ta cho kế thừa tới các lớp "Bảo vệ", "Nhân viên phòng A", "Nhân viên phòng B", ... Và tất nhiên các lớp này có thể triển khai riêng biệt các hàm ảo có tại lớp cơ sở "Nhân viên".
Trình biên dịch sẽ thực hiện Runtime Polymorphism như thế nào?
Trình biên dịch sẽ duy trì:
vtable
: Đây là một bảng các con trỏ hàm được duy trì cho mỗi lớp.vptr
: Đây là một con trỏ tớivtable
và được duy trì cho mỗi một đối tượng.
Trình biên dịch sẽ thêm code bổ sung tại 2 chỗ là:
- Code trong mỗi hàm khởi tạo. Nó sẽ khởi tạo
vptr
của đối tượng được tạo và đặtvptr
trỏ đếnvtable
của lớp. - Code với lệnh gọi hàm ảo. Tại bất kỳ chỗ nào tính đa hình được thực hiện, trình biên dịch sẽ chèn code để tìm
vptr
trước bằng cách sử dụng con trỏ hoặc tham chiếu lớp cơ sở. Khivptr
được nạp,vtable
của lớp dẫn xuất có thể được truy cập. Sử dụngvtable
, địa chỉ của hàm ảo tại lớp dẫn xuất sẽ được truy cập và gọi.
Vậy thì đây có phải là một cách thực hiện Runtime Polymorphism chuẩn hay không?
Trên thực tế, C++ không bắt buộc một Runtime Polymorphism chạy chính xác như thế này. Nhưng trình biên dịch thường sử dụng các mô hình với biến thể nhỏ, dựa trên mô hình cơ bản bên trên.
Hàm Pure Virtual trong C++
Với Pure Virtual, nghĩa là bạn chỉ dùng hàm ảo tại lớp cơ sở để khai báo, chứ không có bất kỳ câu lệnh nào bên trong hàm đó.
#include
using namespace std;
class base {
public:
virtual void print() = 0; // Pure Virtual
void show() {
cout << "show base class" << endl;
}
};
class derived : public base {
public:
void print() {
cout << "print derived class" << endl;
}
void show() {
cout << "show derived class" << endl;
}
};
int main() {
base *bptr;
derived d;
bptr = &d;
bptr->print();
bptr->show();
return 0;
}
Trong đoạn code trên, mình đã sửa hàm print()
thành một Pure Virtual và tất nhiên kết quả vẫn không thay đổi.
Bài viết của mình đến đây là hết rồi, mình rất mong nhận được những ý kiến của các bạn để bài viết của mình ngày một tốt hơn. Vì thế đừng ngần ngại comment bất kỳ thắc mắc, hay đóng góp nào tại phần bình luận ngay phía dưới nhé. Cảm ơn mọi người rất nhiều. Hẹn gặp lại các bạn trong bài viết tiếp theo.
Tài liệu tham khảo:
- Geeksforgeeks