Trong bài này chúng ta sẽ cùng tìm hiểu tính đa hình (polymorphism) trong C++: đa hình là gì, hai loại đa hình compile-time và run-time, hàm ảo và hàm thuần ảo. Đây là tính chất “ăn tiền” nhất của lập trình hướng đối tượng, và cũng là câu hỏi rất hay gặp khi đi phỏng vấn nhé.
Tổng quan về tính đa hình trong C++
Đa hình là khả năng cùng một lời gọi hàm nhưng mỗi đối tượng thực hiện theo cách riêng của nó.
Lấy ví dụ đời thường: bạn bấm nút “phát” trên điều khiển, nếu đang mở TV thì nó phát kênh truyền hình, đang mở loa thì phát nhạc, đang mở máy chiếu thì chiếu phim. Cùng một nút “phát”, nhưng mỗi thiết bị làm một việc khác nhau. Đó chính là đa hình.

Ví dụ:
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape(int w = 0, int h = 0) {
width = w;
height = h;
}
virtual ~Shape() {}
virtual int area() {
cout << "Parent class area: ";
return 0;
}
};
class Rectangle : public Shape {
public:
Rectangle(int w = 0, int h = 0) : Shape(w, h) {}
int area() override {
cout << "Rectangle class area: ";
return width * height;
}
};
class Triangle : public Shape {
public:
Triangle(int w = 0, int h = 0) : Shape(w, h) {}
int area() override {
cout << "Triangle class area: ";
return width * height / 2;
}
};
int main() {
Shape *shape;
Rectangle rec(10, 7);
Triangle tri(10, 5);
shape = &rec;
int a1 = shape->area(); // gọi Rectangle::area()
cout << a1 << endl;
shape = &tri;
int a2 = shape->area(); // gọi Triangle::area()
cout << a2 << endl;
return 0;
}
Kết quả:
Rectangle class area: 70 Triangle class area: 25
Chúng ta có ba lớp ‘Shape’, ‘Rectangle’ và ‘Triangle’. Lớp ‘Shape’ khai báo hàm ảo ‘area()’, còn ‘Rectangle’ và ‘Triangle’ kế thừa từ ‘Shape’ và tự định nghĩa lại hàm ‘area()’ cho mình.
Trong hàm ‘main()’, cùng một con trỏ ‘shape’ nhưng khi trỏ tới ‘rec’ thì gọi hàm của Rectangle, khi trỏ tới ‘tri’ thì gọi hàm của Triangle. Đó chính là tính đa hình đúng không nào.
Từ khóa ‘override’ (có từ C++11) giúp trình biên dịch kiểm tra hàm ở lớp con có thực sự ghi đè hàm ảo của lớp cha hay không. Nếu lỡ gõ sai tên hoặc sai tham số, trình biên dịch sẽ báo lỗi ngay thay vì âm thầm tạo ra một hàm mới.
Có bao nhiêu loại đa hình trong C++?
C++ có hai loại đa hình: đa hình compile-time (lúc biên dịch) và đa hình run-time (lúc chạy). Chúng ta cùng tìm hiểu từng loại nhé.

1. Đa hình compile-time
Loại này còn gọi là đa hình tĩnh. Việc chọn hàm nào để gọi được quyết định ngay khi biên dịch. Chúng ta có thể dùng nạp chồng hàm (function overloading) hoặc template để làm đa hình tĩnh.
Ví dụ:
#include <iostream>
using namespace std;
template <typename T>
T myMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int x = 5, y = 10;
float a = 1.2, b = 3.4;
cout << myMax(x, y) << endl; // trả về 10
cout << myMax(a, b) << endl; // trả về 3.4
return 0;
}
Hàm template ‘myMax()’ tìm giá trị lớn hơn giữa hai giá trị cùng kiểu. Chỉ viết một hàm, nhưng trình biên dịch tự sinh ra phiên bản cho kiểu int và kiểu float ngay lúc biên dịch.
Lưu ý: Không nên đặt tên hàm là ‘max’ khi đã có ‘using namespace std;’, vì thư viện chuẩn đã có sẵn hàm ‘std::max’. Trình biên dịch sẽ báo lỗi “call of overloaded max is ambiguous” do không biết nên gọi hàm nào.
2. Đa hình run-time
Đa hình run-time là loại đa hình mà việc chọn hàm nào để gọi được quyết định khi chương trình đang chạy. Chúng ta dùng kế thừa kết hợp với hàm ảo để làm đa hình động.
Ví dụ: Quay lại ví dụ ‘Shape’ ở đầu bài, con trỏ ‘shape’ có kiểu ‘Shape*’, nhưng hàm ‘area()’ được gọi lại tùy thuộc vào đối tượng thật mà nó đang trỏ tới:
Shape *shape; Rectangle rec(10, 7); Triangle tri(10, 5); shape = &rec; cout << shape->area() << endl; // Rectangle class area: 70 shape = &tri; cout << shape->area() << endl; // Triangle class area: 25
Trình biên dịch không biết trước ‘shape’ sẽ trỏ tới đối tượng nào. Chỉ khi chương trình chạy tới dòng đó, cơ chế hàm ảo mới tra xem đối tượng thật là Rectangle hay Triangle để gọi đúng hàm.
Những ưu điểm của tính đa hình trong C++
Tại sao chúng ta cần đa hình? Đây là những lợi ích mà nó mang lại:
-
Linh hoạt: cùng một đoạn code có thể làm việc với nhiều loại đối tượng khác nhau. Thêm loại đối tượng mới mà không phải sửa code cũ.
-
Tái sử dụng: phần code chung nằm ở lớp cơ sở, các lớp dẫn xuất chỉ cần viết lại phần khác biệt. Không phải lặp lại code nhiều lần.
-
Code gọn hơn: thay vì viết một chuỗi if-else dài để kiểm tra “đây là hình chữ nhật hay hình tam giác” rồi gọi hàm tương ứng, chúng ta chỉ cần gọi ‘shape->area()’ là xong.
-
Dễ bảo trì: mỗi lớp tự lo phần xử lý của riêng mình, sửa một lớp không ảnh hưởng tới các lớp khác.

Trong lập trình nhúng, đa hình cũng được dùng nhiều. Ví dụ một lớp cha ‘Sensor’ có hàm ảo ‘read()’, các lớp con DHT11, BME280, DS18B20 tự cài đặt cách đọc riêng. Chương trình chính chỉ cần gọi ‘sensor->read()’ mà không quan tâm đó là cảm biến gì nhé.
Phương thức ảo là gì?
Phương thức ảo (hàm ảo) là một hàm được khai báo trong lớp cơ sở với từ khóa “virtual”, cho phép những lớp dẫn xuất ghi đè (override) nó theo cách riêng của mình.
Khi gọi hàm ảo qua con trỏ hoặc tham chiếu kiểu lớp cơ sở, hàm của lớp dẫn xuất tương ứng với đối tượng thật sẽ được gọi.
Ví dụ:
#include <iostream>
using namespace std;
class Shape {
public:
virtual ~Shape() {}
virtual void draw() {
cout << "Drawing a Shape" << endl;
}
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a Circle" << endl;
}
};
class Square : public Shape {
public:
void draw() override {
cout << "Drawing a Square" << endl;
}
};
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Square();
shape1->draw();
shape2->draw();
delete shape1;
delete shape2;
return 0;
}
Lớp ‘Shape’ có hàm ảo ‘draw’, hai lớp dẫn xuất Circle và Square ghi đè hàm này để vẽ hình tròn và hình vuông. Trong hàm main, chúng ta tạo hai đối tượng Circle và Square rồi gọi ‘draw’ thông qua con trỏ kiểu ‘Shape*’.
Chạy chương trình, kết quả là:
Drawing a Circle Drawing a Square
Dù cả hai con trỏ đều có kiểu ‘Shape*’, hàm ‘draw’ được gọi vẫn đúng theo đối tượng thật là Circle hay Square. Các bạn thử bỏ từ khóa ‘virtual’ ở hàm ‘draw’ của lớp ‘Shape’ (và bỏ luôn ‘override’ ở hai lớp con, nếu không trình biên dịch sẽ báo lỗi) rồi chạy lại xem. Lúc này cả hai dòng đều in ra “Drawing a Shape”, vì không còn cơ chế hàm ảo nữa.
Lưu ý: Khi xóa đối tượng lớp con thông qua con trỏ lớp cha (như ‘delete shape1;’), lớp cha bắt buộc phải có destructor ảo ‘virtual ~Shape() {}’. Nếu thiếu, destructor của lớp con sẽ không được gọi, tài nguyên của lớp con bị rò rỉ.
Thế nào là phương thức thuần ảo trong C++?
Phương thức thuần ảo là hàm ảo không có phần định nghĩa, được khai báo bằng cách thêm “= 0” ở cuối. Lớp có ít nhất một hàm thuần ảo được gọi là lớp trừu tượng trong ngôn ngữ lập trình C++.
Ví dụ:
class Shape {
public:
virtual ~Shape() {}
virtual void draw() = 0; // hàm thuần ảo
};
Lớp ‘Shape’ bây giờ là lớp trừu tượng, ‘draw()’ là hàm thuần ảo. Chúng ta không thể tạo đối tượng trực tiếp từ ‘Shape’ (viết ‘Shape s;’ sẽ bị báo lỗi), nó chỉ dùng làm lớp cha cho các lớp khác.
Lớp con kế thừa từ ‘Shape’ bắt buộc phải cài đặt hàm ‘draw()’. Nếu không, lớp con đó cũng trở thành lớp trừu tượng và không tạo được đối tượng. Nhờ vậy hàm thuần ảo giống như một “hợp đồng”, ép mọi lớp con phải có đủ các hàm cần thiết.
Lời kết
Tính đa hình giúp code linh hoạt và dễ mở rộng hơn rất nhiều. Ba thứ cần nhớ là: dùng ‘virtual’ ở lớp cha, dùng ‘override’ ở lớp con, và luôn có destructor ảo khi xóa đối tượng qua con trỏ lớp cha.
Nếu có thắc mắc gì về tính đa hình, hãy để lại bình luận bên dưới nhé. Hãy cùng chuyển sang bài tiếp theo trong serie để tìm hiểu tính kế thừa trong C++!

