Bài 35.5: Bài tập thực hành lập trình hướng đối tượng trong C++

Bài 35.5: Bài tập thực hành lập trình hướng đối tượng trong C++
Chào mừng trở lại với chuỗi bài học C++ của FullhouseDev!
Sau khi đã cùng nhau tìm hiểu về các khái niệm cốt lõi trong Lập trình Hướng đối tượng (OOP) với C++, từ lớp (class), đối tượng (object), đóng gói (encapsulation), kế thừa (inheritance) đến đa hình (polymorphism)... giờ là lúc chúng ta xắn tay áo lên và thực hành! Lý thuyết rất quan trọng, nhưng chỉ thông qua việc viết code và giải quyết vấn đề thực tế, bạn mới có thể thực sự nắm vững và áp dụng OOP một cách hiệu quả.
Bài viết này, Bài 35.5, sẽ là nơi chúng ta cùng nhau thực hiện các bài tập thực hành để củng cố kiến thức về OOP trong C++. Không có cách nào tốt hơn để học ngoài việc làm!
Hãy cùng bắt đầu ngay với các bài tập nhé!
Bài tập 1: Xây dựng lớp SinhVien
đơn giản
Yêu cầu:
Tạo một lớp có tên là SinhVien
. Lớp này sẽ có các thuộc tính (dữ liệu thành viên) riêng tư (private) bao gồm:
maSV
(chuỗi - string)tenSV
(chuỗi - string)diemTrungBinh
(số thực - double)
Lớp cần có các phương thức công khai (public) để:
- Thiết lập giá trị cho các thuộc tính (setters).
- Lấy giá trị của các thuộc tính (getters).
- Hiển thị thông tin đầy đủ của sinh viên.
Giải thích:
Bài tập này giúp bạn ôn lại cách khai báo lớp, định nghĩa các thành viên dữ liệu (data members) và thành viên hàm (member functions), cũng như áp dụng nguyên tắc đóng gói (encapsulation) bằng cách giữ dữ liệu ở chế độ private
và cung cấp các hàm public
để tương tác với dữ liệu đó.
Code minh họa:
#include <iostream>
#include <string>
#include <iomanip> // Để định dạng output số thực
class SinhVien {
private:
string maSV;
string tenSV;
double diemTrungBinh;
public:
// Hàm tạo (Constructor) - Tùy chọn nhưng rất tiện lợi
// Khởi tạo đối tượng ngay khi được tạo
SinhVien(string ma, string ten, double diem) : maSV(ma), tenSV(ten), diemTrungBinh(diem) {
cout << "Da tao sinh vien: " << tenSV << endl;
}
// Hàm hủy (Destructor) - Tùy chọn
// Được gọi khi đối tượng bị hủy
~SinhVien() {
// cout << "Da huy sinh vien: " << tenSV << endl;
// Trong ví dụ đơn giản này, destructor không cần làm gì đặc biệt
}
// Setters (Thiết lập giá trị)
void setMaSV(const string& ma) {
maSV = ma;
}
void setTenSV(const string& ten) {
tenSV = ten;
}
void setDiemTrungBinh(double diem) {
if (diem >= 0 && diem <= 10) { // Thêm kiểm tra ràng buộc đơn giản
diemTrungBinh = diem;
} else {
cerr << "Loi: Diem khong hop le!" << endl;
}
}
// Getters (Lấy giá trị)
string getMaSV() const { // 'const' nghĩa là hàm này không thay đổi trạng thái của đối tượng
return maSV;
}
string getTenSV() const {
return tenSV;
}
double getDiemTrungBinh() const {
return diemTrungBinh;
}
// Phương thức hiển thị thông tin
void hienThongTin() const {
cout << "--- Thong tin Sinh vien ---" << endl;
cout << "Ma SV: " << maSV << endl;
cout << "Ten SV: " << tenSV << endl;
// Sử dụng fixed và setprecision để hiển thị 2 chữ số thập phân
cout << "Diem TB: " << fixed << setprecision(2) << diemTrungBinh << endl;
cout << "--------------------------" << endl;
}
}; // Kết thúc khai báo lớp bằng dấu chấm phẩy
int main() {
// Tạo một đối tượng SinhVien bằng constructor
SinhVien sv1("SV001", "Nguyen Van A", 8.75);
// Hiển thị thông tin ban đầu
sv1.hienThongTin();
// Sử dụng setters để cập nhật thông tin
sv1.setDiemTrungBinh(9.0);
// Sử dụng getters và phương thức hiển thị để kiểm tra
cout << "\nCap nhat diem cho " << sv1.getTenSV() << endl;
sv1.hienThongTin();
// Tạo thêm một đối tượng khác
SinhVien sv2("SV002", "Tran Thi B", 7.5);
sv2.hienThongTin();
return 0;
} // Khi ra khỏi phạm vi này, sv1 và sv2 sẽ bị hủy, destructor (nếu có nội dung) sẽ được gọi.
Giải thích code:
- Chúng ta khai báo lớp
SinhVien
với các thành viên dữ liệuprivate
(maSV
,tenSV
,diemTrungBinh
) để đóng gói dữ liệu, ngăn chặn truy cập trực tiếp từ bên ngoài lớp. - Các hàm
public
nhưsetMaSV
,getTenSV
,hienThongTin
là giao diện để thế giới bên ngoài tương tác với đối tượngSinhVien
. Đây chính là nguyên tắc đóng gói trong OOP. - Hàm tạo
SinhVien(string, string, double)
giúp khởi tạo các thuộc tính của đối tượng ngay khi nó được tạo ra, làm cho code ngắn gọn và an toàn hơn. - Hàm hủy
~SinhVien()
được gọi tự động khi đối tượng bị phá hủy (ví dụ: khi nó ra khỏi phạm vi), hữu ích cho việc giải phóng tài nguyên. - Trong
main()
, chúng ta tạo ra các đối tượng (instance) của lớpSinhVien
(sv1
,sv2
) và gọi các phương thức (methods) của chúng (hienThongTin
,setDiemTrungBinh
).
Bài tập 2: Tương tác giữa các đối tượng - Lớp Sach
và ThuVien
Yêu cầu:
- Tạo một lớp
Sach
đơn giản với các thuộc tính nhưtieuDe
(string) vàtacGia
(string), và một phương thức hiển thị thông tin sách. - Tạo một lớp
ThuVien
. Lớp này sẽ chứa một tập hợp các đối tượngSach
. Sử dụngvector
để lưu trữ danh sách sách. - Lớp
ThuVien
cần có các phương thức để:- Thêm một cuốn sách vào thư viện.
- Hiển thị thông tin của tất cả các cuốn sách trong thư viện.
Giải thích:
Bài tập này minh họa mối quan hệ thành phần (composition) trong OOP, nơi một đối tượng của lớp này (ThuVien
) có chứa (has-a) các đối tượng của lớp khác (Sach
). Bạn sẽ thấy cách các đối tượng tương tác với nhau thông qua việc gọi các phương thức của nhau.
Code minh họa:
#include <iostream>
#include <string>
#include <vector> // Để sử dụng vector
// Lớp Sach đơn giản
class Sach {
private:
string tieuDe;
string tacGia;
public:
// Hàm tạo
Sach(string td, string tg) : tieuDe(td), tacGia(tg) {}
// Getters (cần thiết để lớp ThuVien lấy thông tin hiển thị)
string getTieuDe() const { return tieuDe; }
string getTacGia() const { return tacGia; }
// Phương thức hiển thị thông tin Sach
void hienThongTin() const {
cout << "- \"" << tieuDe << "\" by " << tacGia << endl;
}
}; // Kết thúc khai báo lớp Sach
// Lớp ThuVien chứa các đối tượng Sach
class ThuVien {
private:
// Sử dụng vector để lưu trữ nhiều đối tượng Sach
vector<Sach> danhSachSach; // Đây là mối quan hệ composition (ThuVien "có" các đối tượng Sach)
public:
// Phương thức thêm sách mới vào thư viện
void themSach(const Sach& sachMoi) { // Truyền đối tượng Sach theo tham chiếu const để hiệu quả và không làm thay đổi đối tượng gốc
danhSachSach.push_back(sachMoi); // Thêm sách vào vector
cout << "Da them sach: \"" << sachMoi.getTieuDe() << "\"" << endl;
}
// Phương thức hiển thị tất cả sách trong thư viện
void hienThiTatCaSach() const {
cout << "\n--- Cac sach trong thu vien ---" << endl;
if (danhSachSach.empty()) {
cout << "Thu vien hien tai rong." << endl;
} else {
// Duyệt qua từng đối tượng Sach trong vector và gọi phương thức hienThongTin() của từng đối tượng đó
for (const auto& sach : danhSachSach) {
sach.hienThongTin();
}
}
cout << "------------------------------" << endl;
}
}; // Kết thúc khai báo lớp ThuVien
int main() {
// Tạo các đối tượng Sach
Sach sach1("Nha gia kim", "Paulo Coelho");
Sach sach2("De men phieu luu ky", "To Hoai");
Sach sach3("Hoang tu be", "Antoine de Saint-Exupery");
// Tạo một đối tượng ThuVien
ThuVien myThuVien;
// Thêm các đối tượng Sach vào đối tượng ThuVien
myThuVien.themSach(sach1);
myThuVien.themSach(sach2);
myThuVien.themSach(sach3);
// Hiển thị danh sách sách trong thư viện
myThuVien.hienThiTatCaSach();
return 0;
}
Giải thích code:
- Lớp
Sach
là một lớp đơn giản như bài tập 1. - Lớp
ThuVien
có một thành viên dữ liệu làvector<Sach> danhSachSach
. Đây là cách thể hiện mối quan hệ thành phần (composition): mộtThuVien
được tạo thành từ hoặc chứa nhiềuSach
. - Phương thức
themSach
củaThuVien
nhận một đối tượngSach
làm tham số và thêm nó vào vectordanhSachSach
sử dụngpush_back()
. - Phương thức
hienThiTatCaSach
lặp qua vectordanhSachSach
. Với mỗi đối tượngSach
trong vector, nó gọi phương thứchienThongTin()
của chính đối tượngSach
đó để hiển thị thông tin. Điều này cho thấy sự tương tác giữa các đối tượng thuộc các lớp khác nhau.
Bài tập 3: Áp dụng Kế thừa - Hệ thống Hình học cơ bản
Yêu cầu:
- Thiết kế một lớp cơ sở (base class) có tên là
Hinh
. Lớp này có thể có các thuộc tính chung nhưmauSac
(string) và một phương thức hiển thị màu sắc. - Thiết kế hai lớp kế thừa (derived classes) là
HinhTron
vàHinhChuNhat
, kế thừa từ lớpHinh
. - Lớp
HinhTron
cần có thêm thuộc tínhbanKinh
(double) và phương thức tính diện tích riêng. - Lớp
HinhChuNhat
cần có thêm thuộc tínhchieuDai
vàchieuRong
(double) và phương thức tính diện tích riêng. - Minh họa việc tạo đối tượng của các lớp kế thừa và truy cập các thuộc tính, phương thức từ cả lớp cơ sở và lớp kế thừa.
Giải thích:
Bài tập này giúp bạn hiểu và áp dụng nguyên tắc kế thừa (inheritance). Các lớp con (HinhTron
, HinhChuNhat
) kế thừa các đặc điểm chung (mauSac
, hienThiMau
) từ lớp cha (Hinh
), đồng thời bổ sung thêm các đặc điểm riêng của chúng (thuộc tính kích thước, phương thức tính diện tích). Kế thừa giúp tái sử dụng code và tạo ra cấu trúc phân cấp trong chương trình.
Code minh họa:
#include <iostream>
#include <string>
#include <cmath> // Để sử dụng M_PI cho hình tròn
#include <iomanip> // Để định dạng output
// Định nghĩa M_PI nếu chưa có (một số trình biên dịch cần define này)
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Lớp cơ sở (Base Class)
class Hinh {
protected: // Sử dụng protected để lớp con có thể truy cập thuộc tính này
string mauSac;
public:
// Hàm tạo lớp cơ sở
Hinh(string mau) : mauSac(mau) {
cout << "Constructor Hinh: Tao hinh mau " << mauSac << endl;
}
// Phương thức chung của lớp cơ sở
void hienThiMau() const {
cout << "Hinh nay co mau: " << mauSac << endl;
}
// Hàm hủy (nên để virtual nếu có kế thừa)
virtual ~Hinh() {
cout << "Destructor Hinh" << endl;
}
};
// Lớp kế thừa (Derived Class) HinhTron
// Kế thừa public từ lớp Hinh
class HinhTron : public Hinh {
private:
double banKinh;
public:
// Hàm tạo lớp HinhTron
// Phải gọi hàm tạo của lớp cơ sở (Hinh(mau))
HinhTron(string mau, double r) : Hinh(mau), banKinh(r) {
cout << "Constructor HinhTron: Ban kinh " << banKinh << endl;
}
// Phương thức riêng của HinhTron
double tinhDienTich() const {
return M_PI * banKinh * banKinh;
}
// Override hàm hủy nếu cần thiết (không bắt buộc trong ví dụ này nhưng là practice tốt)
~HinhTron() override {
cout << "Destructor HinhTron" << endl;
}
};
// Lớp kế thừa (Derived Class) HinhChuNhat
// Kế thừa public từ lớp Hinh
class HinhChuNhat : public Hinh {
private:
double chieuDai;
double chieuRong;
public:
// Hàm tạo lớp HinhChuNhat
// Phải gọi hàm tạo của lớp cơ sở (Hinh(mau))
HinhChuNhat(string mau, double cd, double cr) : Hinh(mau), chieuDai(cd), chieuRong(cr) {
cout << "Constructor HinhChuNhat: Dai " << chieuDai << ", Rong " << chieuRong << endl;
}
// Phương thức riêng của HinhChuNhat
double tinhDienTich() const {
return chieuDai * chieuRong;
}
~HinhChuNhat() override {
cout << "Destructor HinhChuNhat" << endl;
}
};
int main() {
cout << fixed << setprecision(2); // Định dạng output số thực
// Tạo đối tượng của lớp HinhTron
HinhTron hinhTr("Do", 5.0);
// Tạo đối tượng của lớp HinhChuNhat
HinhChuNhat hinhCN("Xanh Duong", 4.0, 6.0);
cout << endl; // Xuống dòng cho dễ nhìn
// Gọi phương thức kế thừa từ lớp cơ sở
hinhTr.hienThiMau();
hinhCN.hienThiMau();
cout << endl; // Xuống dòng
// Gọi phương thức riêng của từng lớp kế thừa
cout << "Dien tich Hinh Tron (" << hinhTr.getMauSac() << "): " << hinhTr.tinhDienTich() << endl;
cout << "Dien tich Hinh Chu Nhat (" << hinhCN.getMauSac() << "): " << hinhCN.tinhDienTich() << endl;
cout << endl; // Xuống dòng
cout << "Ket thuc main..." << endl;
// Các đối tượng hinhTr và hinhCN sẽ bị hủy khi ra khỏi scope
return 0;
} // Destructor sẽ được gọi tự động theo thứ tự ngược lại của constructor (derived trước, base sau)
Giải thích code:
- Lớp
Hinh
là lớp cơ sở với thuộc tínhprotected mauSac
.protected
cho phép các lớp kế thừa truy cập trực tiếp thuộc tính này, khác vớiprivate
. - Lớp
HinhTron
vàHinhChuNhat
kế thừa từHinh
bằng cú pháp: public Hinh
.public
inheritance nghĩa là các thành viênpublic
củaHinh
sẽ trở thànhpublic
của lớp con,protected
sẽ vẫn làprotected
. - Trong hàm tạo của lớp con (
HinhTron
,HinhChuNhat
), chúng ta sử dụng cú pháp: Hinh(mau)
để gọi hàm tạo của lớp cơ sở. Việc này đảm bảo phần thuộc tính của lớp cha trong đối tượng lớp con được khởi tạo đúng cách. - Các lớp con bổ sung các thuộc tính và phương thức riêng của chúng (
banKinh
,chieuDai
,chieuRong
,tinhDienTich
). - Trong
main()
, chúng ta tạo đối tượng của các lớp con. Ta có thể gọi các phương thức của lớp cơ sở (hienThiMau()
) trên đối tượng của lớp con, chứng tỏ chúng đã kế thừa phương thức đó. Đồng thời, ta gọi các phương thức riêng của từng lớp con (tinhDienTich()
). - Việc sử dụng
virtual
cho destructor của lớp cơ sở là practice tốt trong C++ khi làm việc với kế thừa, để đảm bảo destructor của lớp con được gọi đúng khi làm việc với con trỏ lớp cha.
Bài tập ví dụ: C++ Bài 24.B2: Quản lý doanh thu bán hàng (OOP)
Quản lý doanh thu bán hàng (OOP)
Đề bài
Công ty FullHouse Dev muốn quản lý doanh thu bán hàng của các nhân viên. Quy tắc đánh giá như sau:
- Doanh thu = Số lượng sản phẩm bán × Giá mỗi sản phẩm
Hãy nhập thông tin doanh thu của các nhân viên và tìm ra nhân viên có doanh thu cao nhất.
Input Format
- Dòng đầu ghi số nhân viên (không quá 100 nhân viên)
- Mỗi nhân viên ghi trên 4 dòng:
- Tên nhân viên
- Số lượng sản phẩm bán
- Giá mỗi sản phẩm
- Mã nhân viên
Output Format
Ghi ra thông tin của nhân viên có doanh thu cao nhất gồm các thông tin:
- Mã nhân viên
- Tên nhân viên
- Số lượng sản phẩm bán
- Giá mỗi sản phẩm
- Doanh thu
Ví dụ
Dữ liệu vào:
3
Nguyen Van A
50
200000
NV001
Tran Thi B
70
150000
NV002
Le Van C
30
100000
NV003
Dữ liệu ra:
NV002 Tran Thi B 70 150000 10500000
Giải thích ví dụ mẫu:
- Đầu vào: Nguyễn Văn A 50 200000 NV001
- Đầu ra: NV002 Trần Thị B 70 150000 10500000
Dòng này hiển thị thông tin chi tiết về nhân viên NV002, bao gồm mã nhân viên, tên, số lượng sản phẩm bán, giá mỗi sản phẩm và doanh thu. Đây là nhân viên có doanh thu cao nhất.
Cách tính doanh thu cho Trần Thị B: Doanh thu = 70 × 150000 = 10500000 Tuyệt vời! Đây là hướng dẫn chi tiết để giải bài tập này bằng C++ sử dụng lập trình hướng đối tượng (OOP), tập trung vào các bước và ý tưởng cốt lõi mà không cung cấp code hoàn chỉnh:
Bước 1: Xác định đối tượng (Object)
Bài toán quản lý doanh thu cho các nhân viên. Rõ ràng, đối tượng trung tâm ở đây chính là "Nhân viên". Chúng ta cần tạo một lớp (class) để biểu diễn một nhân viên bán hàng.
Bước 2: Thiết kế lớp (Class) Employee
Lớp Employee
sẽ chứa các thuộc tính (attributes) cần thiết để lưu trữ thông tin của một nhân viên và các phương thức (methods) để thực hiện các hành động liên quan.
Thuộc tính (Private Members): Để đảm bảo tính đóng gói (encapsulation), các dữ liệu của nhân viên nên được khai báo là
private
.- Tên nhân viên: Kiểu dữ liệu
string
. - Số lượng sản phẩm bán: Kiểu dữ liệu số nguyên (ví dụ
int
). - Giá mỗi sản phẩm: Kiểu dữ liệu số nguyên (ví dụ
int
). - Mã nhân viên: Kiểu dữ liệu
string
.
- Tên nhân viên: Kiểu dữ liệu
Phương thức (Public Members): Cần có các phương thức để tương tác với đối tượng
Employee
.- Constructor: Một phương thức khởi tạo để tạo đối tượng
Employee
từ các thông tin đầu vào. Có thể tạo một constructor nhận tất cả các thông tin (tên, số lượng, giá, mã) làm tham số. - Phương thức tính doanh thu: Một phương thức để tính toán doanh thu dựa trên số lượng và giá. Phương thức này nên trả về kết quả. Kiểu dữ liệu trả về cho doanh thu nên là
long long
để tránh tràn số, vì số lượng và giá có thể lớn khiến tích vượt quá giới hạn củaint
. Phương thức này không làm thay đổi trạng thái của đối tượng, nên đánh dấu làconst
. - Phương thức lấy thông tin (Getters): Cần các phương thức
public
để truy cập các thuộc tínhprivate
khi cần in kết quả (ví dụ:getId()
,getName()
,getQuantity()
,getPrice()
,getRevenue()
). Phương thứcgetRevenue()
có thể gọi lại phương thức tính doanh thu bên trong. Đánh dấu các getter làconst
.
- Constructor: Một phương thức khởi tạo để tạo đối tượng
Bước 3: Đọc dữ liệu và lưu trữ
Trong hàm main()
, bạn sẽ thực hiện việc đọc dữ liệu và tạo các đối tượng Employee
.
- Đọc số lượng nhân viên
n
. - Sử dụng một container để lưu trữ các đối tượng
Employee
.vector<Employee>
là lựa chọn phù hợp và linh hoạt. - Lặp lại
n
lần để đọc thông tin cho từng nhân viên:- Đọc tên (
getline
vì tên có thể chứa khoảng trắng). - Đọc số lượng (
cin >>
). - Đọc giá (
cin >>
). - Quan trọng: Sau khi đọc số nguyên bằng
cin >>
, còn lại ký tự newline trong bộ đệm. Cần xử lý nó trước khi gọigetline
tiếp theo (ví dụ:cin.ignore(numeric_limits<streamsize>::max(), '\n');
hoặc đơn giản hơn làcin.ignore();
nếu chỉ cần bỏ qua 1 ký tự newline). - Đọc mã nhân viên (
getline
). - Tạo một đối tượng
Employee
mới sử dụng constructor với dữ liệu vừa đọc. - Thêm đối tượng này vào
vector
.
- Đọc tên (
Bước 4: Tìm nhân viên có doanh thu cao nhất
Sau khi đã có một vector
chứa tất cả các đối tượng Employee
:
- Khởi tạo biến lưu trữ doanh thu cao nhất tìm được (
max_revenue
, kiểulong long
) và chỉ số (hoặc con trỏ/iterator) của nhân viên tương ứng (max_index
). - Lặp qua
vector
:- Đối với mỗi nhân viên trong vector, gọi phương thức tính doanh thu của họ.
- So sánh doanh thu hiện tại với
max_revenue
. - Nếu doanh thu hiện tại lớn hơn
max_revenue
, cập nhậtmax_revenue
và lưu lại chỉ số (hoặc thông tin khác) của nhân viên này. Có thể bắt đầu với nhân viên đầu tiên làm giả định ban đầu chomax_revenue
vàmax_index
.
Bước 5: In kết quả
Sau khi vòng lặp kết thúc, bạn đã xác định được nhân viên có doanh thu cao nhất (thông qua max_index
).
- Truy cập đối tượng
Employee
tạimax_index
trongvector
. - Sử dụng các phương thức getter của đối tượng này để lấy ra Mã nhân viên, Tên nhân viên, Số lượng sản phẩm bán, Giá mỗi sản phẩm và Doanh thu.
- In các thông tin này ra màn hình theo đúng định dạng yêu cầu, cách nhau bởi dấu cách.
Gợi ý về việc sử dụng std
:
- Sử dụng
cin
,cout
cho nhập/xuất cơ bản. - Sử dụng
string
cho tên và mã. - Sử dụng
vector
để lưu trữ danh sách nhân viên. - Sử dụng
getline
để đọc chuỗi có khoảng trắng. - Sử dụng
cin.ignore()
để xử lý ký tự newline sau khi đọc số. - Sử dụng
long long
cho kiểu dữ liệu doanh thu. - Sử dụng
const
cho các phương thức getter và phương thức tính doanh thu vì chúng không làm thay đổi trạng thái của đối tượng.
Comments