[C++ OOP Thực Chiến] Bài 36: Đặc tính cơ bản của kế thừa đơn (Phần 2) - Cú lừa của private và sự xuất hiện của protected!
Chào anh em! Ở cuối Bài 35, chúng ta đã để hở một lỗ hổng bảo mật chết người: Để Lớp con (DevBackend) có thể xài được biến hoTen và luongCoBan của Lớp cha (NhanVien), chúng ta đã ngậm ngùi ném hai biến đó ra khu vực public.
Điều này đồng nghĩa với việc vứt bỏ hoàn toàn công sức xây dựng tính Đóng gói (Encapsulation) từ đầu series đến giờ. Ở ngoài hàm main, ai cũng có thể gọi dev.luongCoBan = 99999; để tự tăng lương cho mình.
Nếu chúng ta đưa chúng về lại private thì sao? Một rắc rối lớn hơn sẽ xuất hiện.
1. Sự khắc nghiệt của bức tường private
Nhiều người mới học C++ thường lầm tưởng: "Lớp con kế thừa Lớp cha thì đương nhiên nó được xài hết tài sản của cha, kể cả private!"
Đây là một cú lừa! Trong C++, private là một lãnh địa BẤT KHẢ XÂM PHẠM. Nó thuộc về quyền sở hữu độc quyền của Class định nghĩa ra nó.
Khi Lớp con kế thừa Lớp cha, nó VẪN NHẬN ĐƯỢC biến private đó (biến đó vẫn tồn tại trong RAM khi đúc Object con), nhưng Lớp con BỊ CẤM CHẠM VÀO.
Giống như việc cha bạn để lại cho bạn một cái két sắt (private). Két sắt đó nằm trong nhà bạn, nhưng cha bạn không cho mã PIN. Bạn không thể tự mở nó ra được!
Nếu DevBackend cố tình gọi hoTen (đang là private của NhanVien):
Trình biên dịch sẽ báo lỗi đỏ lòm: error: 'hoTen' is a private member of 'NhanVien'.
2. Vị cứu tinh mang tên protected
Để giải quyết bài toán "Tiến thoái lưỡng nan" này (Để public thì mất an toàn, để private thì con cái không dùng được), OOP sinh ra một Access Modifier thứ 3: protected (Được bảo vệ).
Hãy coi protected là "Bảo vật truyền gia":
- Đối với người ngoài (hàm
main, các Class khác): Nó đóng sập cửa, hoạt động khép kín y hệt nhưprivate. Không ai ở ngoài có thể truy cập được. - Đối với con cháu (Lớp kế thừa): Nó mở toang cửa, hoạt động thoải mái y hệt như
public.
Đây là từ khóa sinh ra DÀNH RIÊNG cho Kế thừa!
3. Nghệ thuật phân quyền trong hệ thống Backend
Trong thực tế, khi thiết kế kiến trúc hệ thống, một kỹ sư cứng cáp sẽ biết cách kết hợp cả private và protected trong cùng một Lớp cha để phân quyền dữ liệu.
Ví dụ: Bạn đang thiết kế hệ thống quản lý nhân sự.
- Tên, Email, Chức vụ: Có thể cho phép Lớp con truy cập để tái sử dụng in log, hiển thị thông tin -> Dùng
protected. - Lương cơ bản, Mật khẩu: Là dữ liệu cực kỳ nhạy cảm. Kể cả Lớp con cũng không được phép tự ý sửa đổi bừa bãi. Bắt buộc phải là
private. Nếu con muốn xem hoặc đổi, phải dùng Getter/Setter của cha!
Hãy xem sự lợi hại của kiến trúc này trong code:
#include <iostream>
#include <string>
using namespace std;
// --- LỚP CHA ---
class NhanVien {
private:
double luongCoBan; // Cấm tuyệt đối! Con cái cũng không được đụng vào.
protected:
string hoTen; // Bảo vật truyền gia: Con cái được phép dùng.
public:
NhanVien(string ten, double luong) : hoTen(ten), luongCoBan(luong) {}
// Cung cấp Getter để Lớp con có thể "xem" lương một cách hợp pháp
double getLuong() const { return luongCoBan; }
};
// --- LỚP CON ---
class DevBackend : public NhanVien {
private:
string ngonNguCode;
public:
DevBackend(string ten, double luong, string ngonNgu)
: NhanVien(ten, luong)
{
ngonNguCode = ngonNgu;
}
void inThongTin() {
// Truy cập TRỰC TIẾP hoTen vì nó là protected
cout << "[Info] Dev: " << hoTen << "\n";
cout << "[Info] Ngon ngu: " << ngonNguCode << "\n";
// Bị CẤM truy cập trực tiếp luongCoBan vì nó là private của cha
// cout << luongCoBan; // -> BÁO LỖI NGAY!
// Phải truy cập GIÁN TIẾP qua cửa chính Getter
cout << "[Info] Luong: $" << getLuong() << "\n";
}
};
int main() {
cout << "--- HE THONG PHAN QUYEN KẾ THỪA ---\n\n";
DevBackend dev("Hieu", 2500.0, "Golang & PHP");
dev.inThongTin();
// Hacker ở ngoài main cố tình phá hoại:
// dev.hoTen = "Hacker"; // LỖI: protected chặn người ngoài
// dev.luongCoBan = 99999.0; // LỖI: private chặn người ngoài
return 0;
}
Nhận xét: Tuyệt vời! Kiến trúc của chúng ta giờ đây vững như bàn thạch. Tính Đóng gói (Encapsulation) được bảo toàn 100%, mà tính Tái sử dụng (Inheritance) vẫn hoạt động vô cùng trơn tru. Hệ thống phân định rõ ràng ranh giới: Cái gì thuộc về "mật" của cha thì con chỉ được nhìn qua kính (Getter), cái gì là "truyền thống" của gia đình thì con được quyền cầm nắm (protected).
Tạm kết & Gợi mở
Anh em đã nắm trong tay bộ 3 quyền lực nhất của OOP: private (Chỉ mình ta), protected (Cho con cháu), và public (Cho thiên hạ).
Nhưng nhìn vào hàm Constructor của Lớp con DevBackend ở trên, có lẽ anh em sẽ thấy một cú pháp khá lạ lẫm: : NhanVien(ten, luong).
Tại sao Lớp con khi nhận tham số vào, lại phải "đá" ngược tham số đó lên cho Lớp cha xử lý? Khi một Object Lớp con được new ra trong RAM, phần bộ nhớ của cha hay của con sẽ được khởi tạo trước? Và khi Object đó bị hủy đi, Destructor nào sẽ chạy trước để dọn rác?
Vòng đời của Object trong Kế thừa phức tạp hơn những gì chúng ta tưởng. Hẹn gặp lại anh em ở Bài 37: Cài đặt kế thừa (Phần 1) - Cuộc chiến Constructor và định lý quả trứng con gà!
All rights reserved