Bài 38.3: Bài tập thực hành đa hình trong C++

Bài 38.3: Bài tập thực hành đa hình trong C++
Chào mừng các bạn quay trở lại với chuỗi bài học C++ chuyên sâu của FullhouseDev!
Sau khi đã cùng nhau khám phá về đa hình (polymorphism) - một trong những trụ cột quan trọng nhất của lập trình hướng đối tượng (OOP) trong C++, hôm nay chúng ta sẽ đi sâu vào thực hành để củng cố kiến thức này. Lý thuyết là cần thiết, nhưng chỉ có thông qua các bài tập và ví dụ thực tế, chúng ta mới có thể nắm vững và áp dụng hiệu quả khái niệm đa hình vào các dự án của mình.
Đa hình, nói một cách đơn giản, là khả năng các đối tượng thuộc các lớp khác nhau nhưng có quan hệ kế thừa có thể được xử lý thông qua một giao diện chung, thường là con trỏ hoặc tham chiếu tới lớp cơ sở. Điều kỳ diệu xảy ra khi chúng ta gọi một phương thức virtual thông qua con trỏ/tham chiếu đó - C++ sẽ tự động xác định và thực thi phiên bản phương thức phù hợp với loại đối tượng thực tế, chứ không phải loại của con trỏ/tham chiếu. Đây chính là đa hình lúc chạy (runtime polymorphism).
Hãy cùng bắt tay vào các bài tập thực hành để thấy rõ sức mạnh và sự linh hoạt mà đa hình mang lại!
Bài tập 1: Hình học đơn giản với Đa hình
Một ví dụ kinh điển để bắt đầu. Chúng ta sẽ tạo một lớp cơ sở Shape
và các lớp dẫn xuất như Circle
và Square
. Mục tiêu là có thể vẽ (hoặc mô phỏng việc vẽ) các hình này một cách thống nhất.
Bước 1: Định nghĩa Lớp Cơ sở và các Lớp Dẫn xuất
Chúng ta cần một lớp cơ sở Shape
với một phương thức draw()
là virtual, và các lớp dẫn xuất Circle
, Square
ghi đè (override) phương thức này.
#include <iostream>
#include <vector>
using namespace std;
class Shape {
public:
virtual void draw() const {
cout << "Ve hinh chung." << endl;
}
virtual ~Shape() {
cout << "Huy Shape." << endl;
}
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Ve Hinh Tron." << endl;
}
~Circle() override {
cout << "Huy Circle." << endl;
}
};
class Square : public Shape {
public:
void draw() const override {
cout << "Ve Hinh Vuong." << endl;
}
~Square() override {
cout << "Huy Square." << endl;
}
};
Giải thích:
- Lớp
Shape
là lớp cơ sở. Phương thứcdraw()
được đánh dấu làvirtual
. Điều này báo hiệu cho trình biên dịch rằng các lớp dẫn xuất có thể cung cấp phiên bản riêng của phương thức này, và khi gọi thông qua con trỏ/tham chiếuShape
, hành vi sẽ được xác định lúc chạy. - Các lớp
Circle
vàSquare
kế thừa từShape
(public Shape
). - Chúng ghi đè phương thức
draw()
. Từ khóaoverride
(trong C++11 trở lên) là tùy chọn nhưng rất nên dùng vì nó giúp trình biên dịch kiểm tra xem bạn có thực sự ghi đè một phương thức virtual của lớp cơ sở hay không. - Điểm cực kỳ quan trọng: Destructor (
~Shape()
) cũng được đánh dấu làvirtual
. Chúng ta sẽ thảo luận chi tiết hơn về điều này sau, nhưng về cơ bản, nó đảm bảo rằng khi bạn xóa một đối tượng dẫn xuất thông qua con trỏ lớp cơ sở (delete ptr_to_base;
), destructor thực tế của lớp dẫn xuất sẽ được gọi, tránh rò rỉ bộ nhớ hoặc hành vi không mong muốn.
Bước 2: Thực hành Đa hình
Bây giờ, hãy xem làm thế nào chúng ta có thể sử dụng đa hình để xử lý các đối tượng Circle
và Square
một cách đồng nhất.
#include <iostream>
#include <vector>
using namespace std;
// (Các lớp Shape, Circle, Square được định nghĩa ở trên)
int main() {
vector<Shape*> ds;
ds.push_back(new Circle());
ds.push_back(new Square());
ds.push_back(new Circle());
cout << "Ve tat ca hinh:" << endl;
for (auto& h : ds) {
h->draw();
}
cout << "\nGiai phong bo nho:" << endl;
for (auto& h : ds) {
delete h;
}
return 0;
}
Output cho Bài tập 1:
Ve tat ca hinh:
Ve Hinh Tron.
Ve Hinh Vuong.
Ve Hinh Tron.
Giai phong bo nho:
Huy Circle.
Huy Shape.
Huy Square.
Huy Shape.
Huy Circle.
Huy Shape.
Bạn có thể thấy rõ ràng destructor của lớp dẫn xuất được gọi trước khi destructor của lớp cơ sở được gọi cho mỗi đối tượng.
Bài tập 2: Đa hình với Tham chiếu và Hàm xử lý chung
Thay vì dùng con trỏ, chúng ta cũng có thể sử dụng tham chiếu để thể hiện đa hình. Kỹ thuật này thường được dùng khi bạn truyền đối tượng vào một hàm để xử lý.
Chúng ta sẽ sử dụng lại các lớp Shape
, Circle
, Square
từ ví dụ trước.
Bước 1: Tạo một hàm xử lý sử dụng tham chiếu lớp cơ sở
#include <iostream>
using namespace std;
// (Các lớp Shape, Circle, Square được định nghĩa ở Bài tập 1)
void xuLyHinh(const Shape& h) {
cout << "Xu ly hinh qua tham chieu: ";
h.draw();
}
Giải thích:
- Hàm
processShape
nhận đối số là một tham chiếu hằng (const Shape&
) tớiShape
. - Bên trong hàm, chúng ta gọi
shape_ref.draw()
. Vìshape_ref
thực sự đang tham chiếu đến một đối tượngCircle
hoặcSquare
, vàdraw()
là phương thức virtual, C++ sẽ gọi phiên bảndraw()
phù hợp với đối tượng thực tế.
Bước 2: Sử dụng hàm với các đối tượng khác nhau
#include <iostream>
using namespace std;
// (Các lớp Shape, Circle, Square và hàm xuLyHinh được định nghĩa ở trên)
int main() {
Circle c;
Square v;
cout << "Dung xuLyHinh voi cac doi tuong khac nhau:" << endl;
xuLyHinh(c);
xuLyHinh(v);
return 0;
}
Output cho Bài tập 2:
Dung xuLyHinh voi cac doi tuong khac nhau:
Xu ly hinh qua tham chieu: Ve Hinh Tron.
Xu ly hinh qua tham chieu: Ve Hinh Vuong.
Đây là một cách thể hiện đa hình rất phổ biến, đặc biệt khi truyền đối tượng vào các hàm để xử lý chung.
Bài tập 3: Đa hình trong Hệ thống Nhân viên
Hãy xây dựng một hệ thống đơn giản quản lý các loại nhân viên khác nhau, ví dụ như nhân viên lương cứng và nhân viên lương theo giờ. Cả hai loại này đều có cách tính lương khác nhau, nhưng chúng ta muốn xử lý chúng một cách thống nhất để tính tổng lương cho toàn bộ công ty.
Bước 1: Định nghĩa Lớp Cơ sở và các Lớp Dẫn xuất
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Employee {
protected:
string ten;
public:
Employee(const string& t) : ten(t) {}
virtual double tinhLuong() const = 0;
virtual string layTen() const {
return ten;
}
virtual ~Employee() {
cout << "Huy Employee: " << ten << endl;
}
};
class SalariedEmployee : public Employee {
private:
double luongNam;
public:
SalariedEmployee(const string& t, double luong)
: Employee(t), luongNam(luong) {}
double tinhLuong() const override {
return luongNam / 12.0;
}
~SalariedEmployee() override {
cout << "Huy SalariedEmployee: " << ten << endl;
}
};
class HourlyEmployee : public Employee {
private:
double tienMoiGio;
int soGioLam;
public:
HourlyEmployee(const string& t, double tien, int gio)
: Employee(t), tienMoiGio(tien), soGioLam(gio) {}
double tinhLuong() const override {
return tienMoiGio * soGioLam;
}
~HourlyEmployee() override {
cout << "Huy HourlyEmployee: " << ten << endl;
}
};
Giải thích:
- Lớp
Employee
là lớp cơ sở. Nó có một thành viênname
và constructor để khởi tạo tên. - Phương thức
calculateSalary()
được khai báo làvirtual double calculateSalary() const = 0;
. Dấu= 0
ở cuối biến nó thành một phương thức ảo thuần túy (pure virtual function). Một lớp có ít nhất một phương thức ảo thuần túy được gọi là lớp trừu tượng (abstract class). Bạn không thể tạo đối tượng trực tiếp từ một lớp trừu tượng. Mục đích của nó là định nghĩa một giao diện mà các lớp dẫn xuất phải thực hiện. Bất kỳ lớp dẫn xuất nào không ghi đè tất cả các phương thức ảo thuần túy của lớp cơ sở cũng sẽ là lớp trừu tượng. - Phương thức
getName()
là virtual thông thường, các lớp dẫn xuất có thể ghi đè hoặc sử dụng phiên bản mặc định của lớp cơ sở. - Destructor
~Employee()
là virtual (cũng như các destructor của lớp dẫn xuất) vì chúng ta sẽ dùng con trỏ vàdelete
. SalariedEmployee
vàHourlyEmployee
kế thừa từEmployee
. Chúng cung cấp các thành viên và constructor riêng để lưu trữ thông tin lương.- Cả hai lớp dẫn xuất bắt buộc phải ghi đè phương thức
calculateSalary()
để cung cấp logic tính lương cụ thể của từng loại.
Bước 2: Sử dụng Đa hình để Tính Tổng Lương
Bây giờ, chúng ta có thể tạo một danh sách các nhân viên thuộc các loại khác nhau và tính tổng lương của họ một cách dễ dàng nhờ đa hình.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// (Các lớp Employee, SalariedEmployee, HourlyEmployee được định nghĩa ở trên)
int main() {
vector<Employee*> nv;
nv.push_back(new SalariedEmployee("Alice", 60000.0));
nv.push_back(new HourlyEmployee("Bob", 15.0, 160));
nv.push_back(new SalariedEmployee("Charlie", 72000.0));
nv.push_back(new HourlyEmployee("David", 18.0, 140));
double tongLuong = 0.0;
cout << "Tinh luong tat ca nhan vien:" << endl;
for (auto& p : nv) {
double luongThang = p->tinhLuong();
cout << p->layTen() << ": $" << luongThang << endl;
tongLuong += luongThang;
}
cout << "\nTong luong thang: $" << tongLuong << endl;
cout << "\nGiai phong doi tuong nhan vien:" << endl;
for (auto& p : nv) {
delete p;
}
return 0;
}
Output cho Bài tập 3:
Tinh luong tat ca nhan vien:
Alice: $5000
Bob: $2400
Charlie: $6000
David: $2520
Tong luong thang: $15920
Giai phong doi tuong nhan vien:
Huy SalariedEmployee: Alice
Huy Employee: Alice
Huy HourlyEmployee: Bob
Huy Employee: Bob
Huy SalariedEmployee: Charlie
Huy Employee: Charlie
Huy HourlyEmployee: David
Huy Employee: David
Các điểm quan trọng cần ghi nhớ khi thực hành Đa hình:
- Kế Thừa: Đa hình chỉ hoạt động giữa các lớp có quan hệ kế thừa.
- Phương thức
virtual
: Chỉ các phương thức được đánh dấu làvirtual
trong lớp cơ sở mới có thể được gọi đa hình thông qua con trỏ/tham chiếu lớp cơ sở. - Ghi đè (
override
): Các lớp dẫn xuất cung cấp phiên bản riêng của phương thức virtual bằng cách ghi đè nó. Sử dụng từ khóaoverride
giúp tránh lỗi chính tả hoặc sai chữ ký phương thức. - Con trỏ/Tham chiếu Lớp Cơ sở: Bạn cần sử dụng con trỏ hoặc tham chiếu tới lớp cơ sở để lưu trữ hoặc thao tác với các đối tượng dẫn xuất và gọi phương thức virtual một cách đa hình.
- Destructor
virtual
: Luôn luôn khai báo destructor của lớp cơ sở làvirtual
nếu lớp đó có bất kỳ phương thứcvirtual
nào và bạn dự định xóa các đối tượng dẫn xuất thông qua con trỏ/tham chiếu lớp cơ sở. Điều này là cực kỳ quan trọng để ngăn chặn rò rỉ bộ nhớ và đảm bảo giải phóng tài nguyên đúng cách. - Lớp Trừu tượng và Phương thức ảo thuần túy (
= 0
): Sử dụng phương thức ảo thuần túy để định nghĩa các hành vi mà bắt buộc các lớp dẫn xuất phải triển khai. Lớp chứa phương thức ảo thuần túy trở thành lớp trừu tượng và không thể tạo đối tượng trực tiếp.
Comments