Trong 2 bài trước chúng ta đã cùng 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, ta sẽ tiếp tục tìm hiểu thêm 1 tính chất cũng quan trọng không kém đó là tính đa hình trong lập trình hướng đối tượng nhé.
Tính đa hình là gì?
Từ đa hình có nghĩa là có nhiều dạng. Nói một cách đơn giản, chúng ta có thể định nghĩa đ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ình lấy một ví dụ thực thế nhé:
Một người cùng một lúc có thể có đặc điểm khác nhau. 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 sở hữu những hành vi khác nhau trong các tình huống khác nhau. Điều này được gọi 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ử.
Các 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 <bits/stdc++.h> 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, 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, ta chỉ dùng một hàm duy nhất có tên là func
nhưng có thể 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 <bits/stdc++.h> 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, 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, 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 ở cách nạp chồng toán tử trong kế thừa.
#include <bits/stdc++.h> 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 ta có kết quả
print derived class show base class
Tại sao lại có sự khác biệt ấy? Tại sao cùng là nạp chồng toán tử trong lớp kế thừa nhưng 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ấtderived
Mục đích của hàm ảo là gì?
Các hàm ảo sẽ 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ình lấy một ví dụ cụ thể nhé:
Ta sẽ bắt đầu với một phần mềm quản lý nhân viên.
Đầu tiên, ta sẽ xây dựng một lớp Nhanvien
sau đó xây dựng các hàm ảo tangluong()
, chuyenphong()
, …
Từ lớp Nhanvien
này ta sẽ cho kế thừa tới các lớp Baove
, NhanvienphongA
, NhanvienphongB
, … 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ở Nhanvien
.
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ới
vtable
và được duy trì cho mỗi một đối tượng.
Bạn có thể xem một ví dụ đơn giản: Tại đây
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à đăt vptr
trỏ đến vtable
của lớp.
Code với lệnh gọi hàm ảo. Tại bất cứ 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ở. Khi vptr
được nạp, vptable
của lớp dẫn xuất có thể được truy cập. Sử dụng vtable
, đị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ế thì 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 <bits/stdc++.h> using namespace std; class base { public: virtual void print(); // 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 hề 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
Để lại một bình luận