Đa hình (Polymorphism)
Đa hình (polymorphism) nghĩa là “nhiều hình dạng” - cùng một lời gọi phương thức, nhưng hành vi thực thi khác nhau tùy vào kiểu thực sự của object, ngay cả khi được truy cập qua con trỏ/tham chiếu tới lớp cha. Đây là bước tiếp theo sau bài Kế thừa.
Vấn đề: ghi đè “tĩnh” không hoạt động qua con trỏ lớp cha
Phần tiêu đề “Vấn đề: ghi đè “tĩnh” không hoạt động qua con trỏ lớp cha”#include <iostream>
class Animal {public: void makeSound() { std::cout << "Dong vat phat ra am thanh" << std::endl; }};
class Cat : public Animal {public: void makeSound() { std::cout << "Meo: Meo meo!" << std::endl; }};
int main() { Cat c; Animal* ptr = &c; // Con trỏ kiểu Animal, trỏ tới object Cat ptr->makeSound(); // "Dong vat phat ra am thanh" - SAI Ý MUỐN!
return 0;}Dù ptr thực sự trỏ tới một Cat, chương trình vẫn gọi phiên bản makeSound() của Animal, vì trình biên dịch chọn hàm dựa vào kiểu khai báo của con trỏ (Animal*), không phải kiểu thực sự của object.
Giải pháp: từ khóa virtual
Phần tiêu đề “Giải pháp: từ khóa virtual”Đánh dấu phương thức ở lớp cha là virtual để C++ chọn đúng phiên bản dựa vào kiểu thực sự của object tại runtime (gọi là dynamic dispatch hay runtime polymorphism):
#include <iostream>
class Animal {public: virtual void makeSound() { // virtual! std::cout << "Dong vat phat ra am thanh" << std::endl; }};
class Cat : public Animal {public: void makeSound() override { // override: ghi đè đúng hàm virtual của cha std::cout << "Meo: Meo meo!" << std::endl; }};
class Dog : public Animal {public: void makeSound() override { std::cout << "Cho: Gau gau!" << std::endl; }};
int main() { Cat c; Dog d;
Animal* animals[2] = {&c, &d}; for (Animal* a : animals) { a->makeSound(); // Gọi đúng phiên bản của Cat và Dog } // Meo: Meo meo! // Cho: Gau gau!
return 0;}Từ khóa override (C++11) không bắt buộc nhưng rất nên dùng: nó nhờ trình biên dịch kiểm tra hàm này có thực sự ghi đè đúng một hàm virtual ở lớp cha hay không, tránh lỗi gõ sai tên/kiểu tham số.
Lớp trừu tượng (abstract class) và hàm thuần ảo (pure virtual function)
Phần tiêu đề “Lớp trừu tượng (abstract class) và hàm thuần ảo (pure virtual function)”Một hàm thuần ảo (= 0) là hàm virtual không có phần thân, bắt buộc lớp con phải tự định nghĩa. Class chứa ít nhất một hàm thuần ảo trở thành abstract class - không thể tạo object trực tiếp từ nó, chỉ dùng làm “khuôn mẫu” cho lớp con:
#include <iostream>
class Shape { // Abstract classpublic: virtual double area() = 0; // Hàm thuần ảo - không có thân};
class Circle : public Shape {public: double radius; Circle(double r) : radius(r) {}
double area() override { return 3.14159 * radius * radius; }};
class Square : public Shape {public: double side; Square(double s) : side(s) {}
double area() override { return side * side; }};
int main() { // Shape s; // LỖI! Không thể tạo object từ abstract class
Shape* shapes[2] = {new Circle(3), new Square(4)}; for (Shape* s : shapes) { std::cout << s->area() << std::endl; // 28.27, 16 }
delete shapes[0]; delete shapes[1];
return 0;}Abstract class rất hữu ích để định nghĩa một “giao diện” (interface) chung mà nhiều lớp con khác nhau cùng tuân theo, sau đó xử lý chúng đồng nhất qua con trỏ/tham chiếu tới lớp cha.
Virtual destructor
Phần tiêu đề “Virtual destructor”Khi delete một object thông qua con trỏ lớp cha, nếu destructor của lớp cha không là virtual, chỉ destructor của lớp cha được gọi - destructor của lớp con bị bỏ sót, gây rò rỉ tài nguyên. Vì vậy: nếu class có khả năng bị kế thừa và dùng đa hình, hãy khai báo destructor là virtual:
class Animal {public: virtual ~Animal() { // virtual destructor std::cout << "Huy Animal" << std::endl; }};
class Dog : public Animal {public: ~Dog() override { std::cout << "Huy Dog" << std::endl; }};
int main() { Animal* a = new Dog(); delete a; // Gọi ĐÚNG cả ~Dog() rồi mới đến ~Animal() // Huy Dog // Huy Animal
return 0;}Tổng kết
Phần tiêu đề “Tổng kết”- Đa hình cho phép chọn đúng phiên bản phương thức dựa vào kiểu thực sự của object, kể cả khi truy cập qua con trỏ/tham chiếu tới lớp cha
- Đánh dấu phương thức lớp cha là
virtualđể bật dynamic dispatch; dùngoverrideở lớp con để trình biên dịch kiểm tra tính đúng đắn - Hàm thuần ảo (
virtual ... () = 0;) biến class thành abstract class - không thể tạo object trực tiếp, chỉ dùng làm interface chung - Luôn khai báo destructor là
virtualnếu class có thể bị kế thừa và dùng qua con trỏ lớp cha, tránh rò rỉ tài nguyên