Bài tập OOP: mức cơ bản
Các bài luyện về lớp, đối tượng, đóng gói, hàm khởi tạo, this và thành viên tĩnh.
Tự làm trước, bí thì mở Gợi ý. Lời giải đầy đủ nằm trong từng bài học tương ứng.
Bài 1: lớp điểm số
Viết lớp DiemSo lưu điểm ba môn (toán, lý, hóa) và một phương thức trungBinh()
trả về điểm trung bình.
Gợi ý
Ba thuộc tính kiểu double, một phương thức trả về (toan + ly + hoa) / 3.0. Nhớ
chia cho 3.0 (số thực) để không bị cắt phần lẻ. Xem lại bài Lớp và đối tượng.
Bài 2: hình chữ nhật
Viết lớp HinhChuNhat với chiều dài và chiều rộng, có hai phương thức dienTich() và
chuVi().
Gợi ý
dienTich() trả về dai * rong, chuVi() trả về 2 * (dai + rong). Đây là ví dụ
điển hình của "dữ liệu + hành vi gói chung trong một lớp".
Bài 3: nhiệt kế có kiểm soát
Viết lớp NhietKe giữ nhiệt độ là private. Cung cấp dat(double t) chỉ chấp nhận
giá trị trong khoảng [-50, 50], và doc() trả về nhiệt độ hiện tại.
Gợi ý
Để nhietDo là private. Trong dat(), kiểm tra t >= -50 && t <= 50 trước khi
gán; nếu sai thì bỏ qua hoặc in cảnh báo. Đây chính là đóng gói: dữ liệu chỉ đổi qua
phương thức có kiểm tra. Xem lại bài Đóng gói.
Bài 4: khởi tạo sinh viên
Viết lớp SinhVien có tên và mã số. Dùng hàm khởi tạo để đối tượng vừa tạo là đã có
đủ thông tin, và một phương thức inThongTin().
Gợi ý
Dùng danh sách khởi tạo: SinhVien(string t, string m) : ten(t), mssv(m) {}. Cách này
bảo đảm không bao giờ có một SinhVien thiếu dữ liệu. Xem lại bài Khởi tạo và hủy.
Bài 5: đếm số đối tượng
Viết lớp CuaSo đếm xem chương trình đã tạo bao nhiêu đối tượng CuaSo, đọc được qua
CuaSo::soLuong().
Gợi ý
Một biến static int dem (chung cho cả lớp), tăng trong hàm khởi tạo, và đừng quên
dòng định nghĩa ngoài lớp int CuaSo::dem = 0;. Xem lại bài Thành viên tĩnh.
Bài 6: nối chuỗi lời gọi
Viết lớp ChuoiNoi có phương thức them(string) sao cho gọi được liên tiếp:
s.them("a").them("b").them("c").
Gợi ý
Cho them() trả về kiểu ChuoiNoi& và kết thúc bằng return *this;. Khi trả về chính
đối tượng gọi nó, ta có thể nối tiếp lời gọi. Xem lại bài Con trỏ this.
Bài 7: điểm trên mặt phẳng
Viết lớp Diem2D giữ x và y ở dạng private. Cung cấp getter layX(), layY()
và setter datX(double), datY(double), cùng phương thức inRa() in ra dạng (x, y).
Gợi ý
Hai thuộc tính private double x, y;. Getter là hàm const chỉ trả về giá trị, setter
nhận tham số rồi gán. Nhờ để dữ liệu ẩn và chỉ ra vào qua phương thức, sau này ta có thể
thêm kiểm tra mà không đổi cách dùng bên ngoài. Xem lại bài Đóng gói.
Bài 8: két tiền đơn giản
Viết lớp Ket giữ số dư private long soDu khởi đầu bằng 0. Thêm gui(long) cộng vào
số dư và rut(long) chỉ trừ khi số tiền rút không vượt quá số dư hiện có.
Gợi ý
Trong gui() nên bỏ qua số tiền âm. Trong rut(), kiểm tra so <= soDu trước khi trừ;
nếu không đủ thì không làm gì hoặc in cảnh báo. Đây là đóng gói kèm ràng buộc: số dư
không bao giờ được âm. Xem lại bài Đóng gói.
Bài 9: hình tròn với hàm khởi tạo mặc định
Viết lớp HinhTron có bán kính. Cung cấp một hàm khởi tạo nhận bán kính và một hàm khởi
tạo mặc định đặt bán kính bằng 1. Thêm phương thức dienTich().
Gợi ý
Hai hàm khởi tạo cùng tên khác tham số là nạp chồng hàm khởi tạo. Dùng danh sách khởi
tạo: HinhTron() : r(1) {} và HinhTron(double b) : r(b) {}. dienTich() trả về
3.14159 * r * r. Xem lại bài Khởi tạo và hủy.
Bài 10: phân số tự rút gọn
Viết lớp PhanSo với tử và mẫu. Ngay trong hàm khởi tạo, hãy rút gọn phân số bằng cách
chia cả tử lẫn mẫu cho ước chung lớn nhất, để mọi phân số lưu ra luôn ở dạng tối giản.
Gợi ý
Viết một hàm phụ tính ước chung lớn nhất (thuật toán Euclid), gọi nó trong hàm dựng rồi chia hai thành phần cho kết quả.
#include <iostream>
using namespace std;
int ucln(int a, int b) { // greatest common divisor
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
while (b != 0) { int t = a % b; a = b; b = t; }
return a == 0 ? 1 : a;
}
class PhanSo {
int tu, mau;
public:
PhanSo(int t, int m) : tu(t), mau(m) {
int g = ucln(tu, mau); // reduce inside the constructor
tu /= g;
mau /= g;
}
void in() const { cout << tu << "/" << mau << "\n"; }
};
int main() {
PhanSo p(6, 8); // stored as 3/4
p.in();
return 0;
}
Xem lại bài Khởi tạo và hủy.
Bài 11: ngăn xếp số nguyên đơn giản
Viết lớp NganXep lưu tối đa 100 số nguyên trong một mảng cố định, với push, pop,
top, rong() và day(). Đây là cấu trúc vào sau ra trước.
Gợi ý
Dùng một mảng int data[100] và một biến đếm số phần tử. push ghi vào vị trí cuối rồi
tăng đếm, pop giảm đếm, top trả phần tử ở đỉnh.
#include <iostream>
using namespace std;
class NganXep {
int data[100];
int n = 0; // number of elements
public:
bool day() const { return n == 100; }
bool rong() const { return n == 0; }
void push(int x) { if (!day()) data[n++] = x; }
void pop() { if (!rong()) --n; }
int top() const { return data[n - 1]; }
};
int main() {
NganXep s;
s.push(3);
s.push(7);
cout << s.top() << "\n"; // 7
s.pop();
cout << s.top() << "\n"; // 3
return 0;
}
Xem lại bài Lớp và đối tượng.
Bài 12: đếm số đối tượng đang sống
Viết lớp KetNoi đếm số đối tượng hiện đang tồn tại (chưa bị hủy). Khác với đếm tổng số
đã tạo, con số này giảm đi khi một đối tượng bị hủy.
Gợi ý
Một biến static int chung, tăng trong hàm dựng và giảm trong hàm hủy. Đừng quên định
nghĩa biến static một lần ngoài lớp.
#include <iostream>
using namespace std;
class KetNoi {
static int dangSong; // shared live counter
public:
KetNoi() { ++dangSong; }
~KetNoi() { --dangSong; }
static int soDangSong() { return dangSong; }
};
int KetNoi::dangSong = 0; // define the static member once
int main() {
KetNoi a;
{
KetNoi b, c;
cout << KetNoi::soDangSong() << "\n"; // 3
}
cout << KetNoi::soDangSong() << "\n"; // 1 after b and c destroyed
return 0;
}
Xem lại bài Thành viên tĩnh.
Bài 13: điểm thi có ràng buộc
Viết lớp DiemThi giữ một điểm private luôn nằm trong khoảng [0, 10]. Cung cấp getter
layDiem() và setter datDiem() từ chối mọi giá trị ngoài khoảng cho phép.
Gợi ý
Trong setter, kiểm tra điều kiện trước khi gán; nếu không hợp lệ thì bỏ qua hoặc in cảnh báo. Nhờ vậy đối tượng không bao giờ rơi vào trạng thái sai.
#include <iostream>
using namespace std;
class DiemThi {
double diem = 0; // must stay within [0, 10]
public:
double layDiem() const { return diem; }
void datDiem(double d) {
if (d >= 0 && d <= 10) diem = d; // reject invalid input
else cout << "gia tri khong hop le\n";
}
};
int main() {
DiemThi d;
d.datDiem(8.5);
cout << d.layDiem() << "\n"; // 8.5
d.datDiem(15); // rejected
cout << d.layDiem() << "\n"; // still 8.5
return 0;
}
Xem lại bài Đóng gói.
Bài 14: hàm dựng ủy quyền
Viết lớp SanPham có tên và giá. Cung cấp một hàm dựng đầy đủ và một hàm dựng chỉ nhận
tên, sao cho hàm dựng thứ hai gọi lại hàm dựng thứ nhất với giá mặc định bằng 0.
Gợi ý
Hàm dựng ủy quyền gọi một hàm dựng khác của cùng lớp ngay trong danh sách khởi tạo, giúp tránh lặp lại phần khởi tạo chung.
#include <iostream>
#include <string>
using namespace std;
class SanPham {
string ten;
double gia;
public:
SanPham(string t, double g) : ten(t), gia(g) {} // main constructor
SanPham(string t) : SanPham(t, 0.0) {} // delegates to the one above
void in() const { cout << ten << ": " << gia << "\n"; }
};
int main() {
SanPham a("But", 5000);
SanPham b("Tang kem"); // price defaults to 0 via delegation
a.in();
b.in();
return 0;
}
Xem lại bài Khởi tạo và hủy.
Câu hỏi tự kiểm
- 1Thành viên static của một lớp cần được định nghĩa ở đâu?
- 2Các thành viên của một đối tượng được khởi tạo theo thứ tự nào?
- 3Nếu ba điểm là kiểu int, biểu thức (a + b + c) / 3 sẽ cho kết quả thế nào?
- 4Vì sao nên để nhietDo là private và chỉ đổi qua phương thức dat()?