Trong bài này chúng ta sẽ luyện 10 bài tập OOP C++ có lời giải, đi qua toàn bộ chương lập trình hướng đối tượng của serie: class và đối tượng, đóng gói, constructor và destructor, kế thừa, đa hình, lớp trừu tượng và nạp chồng toán tử. Đây cũng là lời giải cho phần bài tập cuối mỗi bài học trong chương.
Bài 23 trong Serie Lập trình C++ từ A tới Z
Cách làm bài tập OOP C++
Với bài tập lập trình hướng đối tượng, phần khó nhất không phải viết code mà là thiết kế class. Trước khi gõ, các bạn tự trả lời 3 câu hỏi:
- Class này cần lưu những dữ liệu gì? Dữ liệu nào phải luôn thỏa ràng buộc (không âm, không đổi sau khi tạo)?
- Bên ngoài cần làm những hành động gì với nó? Đặt tên phương thức theo hành động đó.
- Các class có quan hệ “là một” (kế thừa) hay “có một” (thành phần)?
Toàn bộ lời giải biên dịch bằng g++ -std=c++17 -Wall -Wextra không có cảnh báo nào.
Bài tập so sánh OOP và lập trình hướng cấu trúc
Kiến thức dùng trong phần này nằm ở bài so sánh OOP và lập trình hướng cấu trúc.
Bài 1: Tài khoản không cho rút quá số dư
Đề bài: Viết class TaiKhoan có số dư private và phương thức rutTien không cho rút quá số dư. Thử sửa số dư trực tiếp từ main.
Gợi ý: Số dư để private nên dòng tk.soDu_ = -100 trong main là lỗi biên dịch, mình để ở dạng comment. Cách duy nhất để thay đổi số dư là rutTien, mà hàm này luôn kiểm tra trước.
Lời giải:
#include <iostream>
class TaiKhoan
{
public:
TaiKhoan(long long soDu) : soDu_(soDu > 0 ? soDu : 0) {}
bool rutTien(long long soTien)
{
if (soTien <= 0 || soTien > soDu_)
return false;
soDu_ -= soTien;
return true;
}
long long soDu() const { return soDu_; }
private:
long long soDu_;
};
int main()
{
TaiKhoan tk(500000);
std::cout << "Rut 200000: " << (tk.rutTien(200000) ? "thanh cong" : "that bai") << '\n';
std::cout << "Rut 400000: " << (tk.rutTien(400000) ? "thanh cong" : "that bai") << '\n';
std::cout << "So du con: " << tk.soDu() << '\n';
// tk.soDu_ = -100; // loi bien dich: soDu_ la private
return 0;
}
Kết quả:
Bài tập lớp và đối tượng trong C++
Kiến thức dùng trong phần này nằm ở bài lớp và đối tượng trong C++.
Bài 2: Class hình chữ nhật
Đề bài: Viết class HinhChuNhat có chiều dài, chiều rộng và 2 phương thức tính chu vi, diện tích. Tạo 2 đối tượng và in kết quả.
Gợi ý: 2 phương thức chỉ đọc thuộc tính nên có const. Mỗi đối tượng có bộ thuộc tính riêng nên kết quả của a và b độc lập.
Lời giải:
#include <iostream>
class HinhChuNhat
{
public:
HinhChuNhat(double dai, double rong) : dai_(dai), rong_(rong) {}
double chuVi() const { return (dai_ + rong_) * 2; }
double dienTich() const { return dai_ * rong_; }
private:
double dai_, rong_;
};
int main()
{
HinhChuNhat a(5, 3), b(2.5, 4);
std::cout << "a: chu vi " << a.chuVi() << ", dien tich " << a.dienTich() << '\n';
std::cout << "b: chu vi " << b.chuVi() << ", dien tich " << b.dienTich() << '\n';
return 0;
}
Kết quả:
Bài 3: Độc giả mượn tối đa 5 cuốn
Đề bài: Viết class DocGia gồm tên, mã thẻ, số sách đang mượn. Phương thức muonThem() không cho mượn quá 5 cuốn.
Gợi ý: Giới hạn 5 cuốn khai báo là static constexpr trong class: hằng số dùng chung cho mọi độc giả, không tốn bộ nhớ trong từng đối tượng.
Lời giải:
#include <iostream>
#include <string>
class DocGia
{
public:
DocGia(const std::string& ten, int maThe) : ten_(ten), maThe_(maThe) {}
bool muonThem()
{
if (soSachDangMuon_ >= SO_SACH_TOI_DA)
return false;
soSachDangMuon_++;
return true;
}
void inThongTin() const
{
std::cout << ten_ << " (the " << maThe_ << "): dang muon " << soSachDangMuon_ << " cuon\n";
}
private:
static constexpr int SO_SACH_TOI_DA = 5;
std::string ten_;
int maThe_;
int soSachDangMuon_ = 0;
};
int main()
{
DocGia an("Nguyen Van An", 1024);
for (int lan = 1; lan <= 6; lan++)
std::cout << "Lan " << lan << ": " << (an.muonThem() ? "duoc muon" : "vuot qua 5 cuon") << '\n';
an.inThongTin();
return 0;
}
Kết quả:
Bài tập constructor và destructor
Kiến thức dùng trong phần này nằm ở bài constructor và destructor của serie.
Bài 4: Constructor ủy quyền và thứ tự hủy
Đề bài: Viết class DocGia có constructor nhận tên và mã thẻ (mã âm đổi thành 0), constructor ủy quyền chỉ nhận tên, và destructor in ra màn hình. Đoán trước thứ tự in.
Gợi ý: b và c nằm trong khối riêng nên bị hủy ngay khi ra khỏi khối, c tạo sau nên hủy trước. a và d sống tới cuối main, d hủy trước a.
Lời giải:
#include <iostream>
#include <string>
class DocGia
{
public:
DocGia(const std::string& ten, int maThe) : ten_(ten), maThe_(maThe >= 0 ? maThe : 0)
{
std::cout << " Tao doc gia " << ten_ << " (the " << maThe_ << ")\n";
}
DocGia(const std::string& ten) : DocGia(ten, 0) {}
~DocGia() { std::cout << " Huy doc gia " << ten_ << '\n'; }
private:
std::string ten_;
int maThe_;
};
int main()
{
DocGia a("An", 1024);
{
DocGia b("Binh");
DocGia c("Chi", -5);
}
DocGia d("Dung", 2048);
std::cout << "Ket thuc main\n";
return 0;
}
Kết quả:
Bài tập tính đóng gói trong C++
Kiến thức dùng trong phần này nằm ở bài tính đóng gói trong C++.
Bài 5: Tài khoản thư viện có tiền phạt
Đề bài: Viết class TaiKhoanThuVien có mã thẻ không đổi sau khi tạo và tiền phạt đang nợ. Thiết kế ghiPhat và thanhToan sao cho tiền nợ không bao giờ âm.
Gợi ý: Mã thẻ là const int và chỉ có getter, không có setter. Không có datTienNo, chỉ có 2 hành động thật là ghi phạt và thanh toán, mỗi hành động tự kiểm tra.
Lời giải:
#include <iostream>
class TaiKhoanThuVien
{
public:
explicit TaiKhoanThuVien(int maThe) : maThe_(maThe) {}
int maThe() const { return maThe_; }
int tienNo() const { return tienNo_; }
void ghiPhat(int soTien)
{
if (soTien > 0)
tienNo_ += soTien;
}
bool thanhToan(int soTien)
{
if (soTien <= 0 || soTien > tienNo_)
return false;
tienNo_ -= soTien;
return true;
}
private:
const int maThe_; // khong doi sau khi tao
int tienNo_ = 0;
};
int main()
{
TaiKhoanThuVien tk(1024);
tk.ghiPhat(6000);
std::cout << "No: " << tk.tienNo() << '\n';
std::cout << "Tra 10000: " << (tk.thanhToan(10000) ? "ok" : "bi tu choi") << '\n';
std::cout << "Tra 4000: " << (tk.thanhToan(4000) ? "ok" : "bi tu choi") << '\n';
std::cout << "The " << tk.maThe() << " con no: " << tk.tienNo() << '\n';
return 0;
}
Kết quả:
Bài tập tính kế thừa trong C++
Kiến thức dùng trong phần này nằm ở bài tính kế thừa trong C++.
Bài 6: Người dùng, độc giả và thủ thư
Đề bài: Viết lớp cha NguoiDung (họ tên, số điện thoại) và 2 lớp con DocGia (số sách đang mượn), ThuThu (ca làm việc).
Gợi ý: Phần họ tên và số điện thoại chỉ viết một lần ở lớp cha. Lớp con gọi constructor lớp cha trong danh sách khởi tạo, và dùng lại inChung() khi in.
Lời giải:
#include <iostream>
#include <string>
class NguoiDung
{
public:
NguoiDung(const std::string& ten, const std::string& sdt) : ten_(ten), sdt_(sdt) {}
void inChung() const { std::cout << ten_ << " - " << sdt_; }
private:
std::string ten_, sdt_;
};
class DocGia : public NguoiDung
{
public:
DocGia(const std::string& ten, const std::string& sdt, int soSach)
: NguoiDung(ten, sdt), soSach_(soSach) {}
void inThongTin() const { inChung(); std::cout << " | doc gia, dang muon " << soSach_ << " cuon\n"; }
private:
int soSach_;
};
class ThuThu : public NguoiDung
{
public:
ThuThu(const std::string& ten, const std::string& sdt, const std::string& ca)
: NguoiDung(ten, sdt), ca_(ca) {}
void inThongTin() const { inChung(); std::cout << " | thu thu, ca " << ca_ << '\n'; }
private:
std::string ca_;
};
int main()
{
DocGia an("Nguyen Van An", "0901234567", 2);
ThuThu lan("Tran Thi Lan", "0912345678", "sang");
an.inThongTin();
lan.inThongTin();
return 0;
}
Kết quả:
Bài tập tính đa hình trong C++
Kiến thức dùng trong phần này nằm ở bài tính đa hình trong C++.
Bài 7: Tổng diện tích các hình
Đề bài: Viết lớp cha HinhHoc có hàm dienTich() và 3 lớp con hình tròn, hình vuông, tam giác. Lưu chung trong một danh sách và tính tổng diện tích.
Gợi ý: Vòng lặp tính tổng chỉ biết HinhHoc, mỗi hình tự tính diện tích của mình qua hàm ảo. Thêm hình mới không phải sửa vòng lặp.
Lời giải:
#include <iostream>
#include <memory>
#include <vector>
class HinhHoc
{
public:
virtual ~HinhHoc() = default;
virtual double dienTich() const = 0;
};
class HinhTron : public HinhHoc
{
public:
explicit HinhTron(double r) : r_(r) {}
double dienTich() const override { return 3.14159265358979 * r_ * r_; }
private:
double r_;
};
class HinhVuong : public HinhHoc
{
public:
explicit HinhVuong(double canh) : canh_(canh) {}
double dienTich() const override { return canh_ * canh_; }
private:
double canh_;
};
class HinhTamGiac : public HinhHoc
{
public:
HinhTamGiac(double day, double cao) : day_(day), cao_(cao) {}
double dienTich() const override { return day_ * cao_ / 2; }
private:
double day_, cao_;
};
int main()
{
std::vector<std::unique_ptr<HinhHoc>> ds;
ds.push_back(std::make_unique<HinhTron>(1));
ds.push_back(std::make_unique<HinhVuong>(2));
ds.push_back(std::make_unique<HinhTamGiac>(3, 4));
double tong = 0;
for (const auto& h : ds)
tong += h->dienTich();
std::cout << "Tong dien tich: " << tong << '\n';
return 0;
}
Kết quả:
Bài tập tính trừu tượng trong C++
Kiến thức dùng trong phần này nằm ở bài tính trừu tượng trong C++.
Bài 8: Lớp trừu tượng nhân viên
Đề bài: Viết lớp trừu tượng NhanVien với hàm thuần ảo tinhLuong(), 2 lớp con lương cố định và lương theo giờ. Tính tổng quỹ lương.
Gợi ý: tinhLuong() = 0 buộc mọi loại nhân viên phải tự viết cách tính lương, và không ai tạo được một “nhân viên chung chung”. Lương dùng long long vì có thể vượt giới hạn của int.
Lời giải:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class NhanVien
{
public:
explicit NhanVien(const std::string& ten) : ten_(ten) {}
virtual ~NhanVien() = default;
virtual long long tinhLuong() const = 0;
const std::string& ten() const { return ten_; }
private:
std::string ten_;
};
class NhanVienToanThoiGian : public NhanVien
{
public:
NhanVienToanThoiGian(const std::string& ten, long long luong) : NhanVien(ten), luong_(luong) {}
long long tinhLuong() const override { return luong_; }
private:
long long luong_;
};
class NhanVienTheoGio : public NhanVien
{
public:
NhanVienTheoGio(const std::string& ten, int soGio, long long donGia)
: NhanVien(ten), soGio_(soGio), donGia_(donGia) {}
long long tinhLuong() const override { return soGio_ * donGia_; }
private:
int soGio_;
long long donGia_;
};
int main()
{
std::vector<std::unique_ptr<NhanVien>> ds;
ds.push_back(std::make_unique<NhanVienToanThoiGian>("Lan", 9000000));
ds.push_back(std::make_unique<NhanVienTheoGio>("Minh", 80, 30000));
long long tong = 0;
for (const auto& nv : ds)
{
std::cout << nv->ten() << ": " << nv->tinhLuong() << " dong\n";
tong += nv->tinhLuong();
}
std::cout << "Tong quy luong: " << tong << " dong\n";
return 0;
}
Kết quả:
Bài tập nạp chồng toán tử
Kiến thức dùng trong phần này nằm ở bài nạp chồng toán tử của serie.
Bài 9: Phân số đủ phép toán
Đề bài: Bổ sung cho class PhanSo các toán tử -, *, /, !=, và viết + thành hàm tự do để 1 + a biên dịch được.
Gợi ý: Tất cả toán tử viết thành hàm tự do kiểu friend ngay trong class. Nhờ vậy cả 2 vế đều được chuyển kiểu ngầm, 1 + a hiểu là 1/1 + a. != viết lại dựa trên == để 2 toán tử không bao giờ mâu thuẫn nhau.
Lời giải:
#include <iostream>
#include <numeric>
class PhanSo
{
public:
PhanSo(int tu = 0, int mau = 1) : tu_(tu), mau_(mau) { rutGon(); }
friend PhanSo operator+(const PhanSo& a, const PhanSo& b) { return PhanSo(a.tu_ * b.mau_ + b.tu_ * a.mau_, a.mau_ * b.mau_); }
friend PhanSo operator-(const PhanSo& a, const PhanSo& b) { return PhanSo(a.tu_ * b.mau_ - b.tu_ * a.mau_, a.mau_ * b.mau_); }
friend PhanSo operator*(const PhanSo& a, const PhanSo& b) { return PhanSo(a.tu_ * b.tu_, a.mau_ * b.mau_); }
friend PhanSo operator/(const PhanSo& a, const PhanSo& b) { return PhanSo(a.tu_ * b.mau_, a.mau_ * b.tu_); }
friend bool operator==(const PhanSo& a, const PhanSo& b) { return a.tu_ == b.tu_ && a.mau_ == b.mau_; }
friend bool operator!=(const PhanSo& a, const PhanSo& b) { return !(a == b); }
friend std::ostream& operator<<(std::ostream& os, const PhanSo& p) { return os << p.tu_ << '/' << p.mau_; }
private:
void rutGon()
{
int g = std::gcd(tu_, mau_);
tu_ /= g;
mau_ /= g;
if (mau_ < 0) { tu_ = -tu_; mau_ = -mau_; }
}
int tu_, mau_;
};
int main()
{
PhanSo a(3, 4), b(1, 6);
std::cout << a << " + " << b << " = " << a + b << '\n';
std::cout << a << " - " << b << " = " << a - b << '\n';
std::cout << a << " * " << b << " = " << a * b << '\n';
std::cout << a << " / " << b << " = " << a / b << '\n';
std::cout << "1 + " << a << " = " << 1 + a << '\n';
std::cout << a << " != " << b << " ? " << (a != b) << '\n';
return 0;
}
Kết quả:
Bài 10: Class thời gian và sắp xếp
Đề bài: Viết class ThoiGian (giờ, phút) có operator+, operator<, operator<<. Sắp xếp một vector các ThoiGian bằng std::sort.
Gợi ý: Lưu tổng số phút thay vì giờ và phút riêng: cộng và so sánh trở nên đơn giản, phút quá 60 tự quy đổi khi in ra.
Lời giải:
#include <algorithm>
#include <iostream>
#include <vector>
class ThoiGian
{
public:
ThoiGian(int gio, int phut) : tongPhut_(gio * 60 + phut) {}
friend ThoiGian operator+(const ThoiGian& a, const ThoiGian& b)
{
return ThoiGian(0, a.tongPhut_ + b.tongPhut_);
}
friend bool operator<(const ThoiGian& a, const ThoiGian& b) { return a.tongPhut_ < b.tongPhut_; }
friend std::ostream& operator<<(std::ostream& os, const ThoiGian& t)
{
return os << t.tongPhut_ / 60 << "h" << t.tongPhut_ % 60 << "p";
}
private:
int tongPhut_;
};
int main()
{
ThoiGian a(1, 45), b(0, 50);
std::cout << a << " + " << b << " = " << a + b << '\n';
std::vector<ThoiGian> ds = {{2, 10}, {0, 35}, {1, 5}};
std::sort(ds.begin(), ds.end());
std::cout << "Sap xep:";
for (const ThoiGian& t : ds)
std::cout << ' ' << t;
std::cout << '\n';
return 0;
}
Kết quả:
Kết
10 bài tập OOP C++ ở trên cho thấy một điều: phần lớn công sức nằm ở việc chọn dữ liệu nào để private, hành động nào để public, class nào kế thừa class nào. Code bên trong mỗi phương thức thường chỉ vài dòng.
Muốn tra cứu chi tiết về class, kế thừa và hàm ảo, các bạn xem thêm tài liệu chuẩn trên cppreference.
Lưu ý: Khi so lời giải, đừng chỉ so kết quả in ra. Hãy so cách thiết kế: các bạn có để thuộc tính public không, có viết setter tùy ý không, có quên const hay hàm hủy ảo không. Đó là những chỗ mà chương trình nhỏ vẫn chạy đúng, nhưng chương trình lớn sẽ gặp lỗi.
Tới đây là hết chương lập trình hướng đối tượng. Chương tiếp theo chúng ta sẽ học template, thư viện STL, xử lý ngoại lệ và đọc ghi file. Có cách thiết kế hay hơn thì các bạn để lại bình luận cho mọi người cùng tham khảo nhé.











