Trong bài này chúng ta sẽ học tính đóng gói trong C++ (encapsulation): vì sao phải giấu dữ liệu của class, cách thiết kế getter và các hàm thay đổi dữ liệu có kiểm tra, phương thức const, và hàm friend. Đóng gói là tính chất đầu tiên trong 4 tính chất của OOP, và cũng là tính chất các bạn dùng nhiều nhất hằng ngày.
Bài 17 trong Serie Lập trình C++ từ A tới Z
Tính đóng gói trong C++ là gì?
Tính đóng gói (encapsulation) là việc gói dữ liệu và các hàm xử lý dữ liệu đó vào chung một class, đồng thời giấu dữ liệu khỏi bên ngoài. Bên ngoài chỉ được tương tác qua những phương thức mà class cho phép.
Ví dụ quen thuộc là cây ATM. Các bạn rút tiền bằng cách bấm nút, nhập số tiền, máy kiểm tra số dư rồi mới nhả tiền. Không ai được mở két ra tự lấy, dù tiền nằm ngay bên trong. Nút bấm là phần public, két tiền là phần private.
Lợi ích của đóng gói:
- Dữ liệu luôn hợp lệ: mọi thay đổi đi qua phương thức có kiểm tra, nên số lượng sách không bao giờ âm.
- Dễ sửa bên trong: đổi cách lưu dữ liệu (ví dụ lưu thêm lịch sử mượn) mà code bên ngoài không phải sửa gì, vì bên ngoài chỉ gọi phương thức.
- Dễ tìm lỗi: dữ liệu bị sai thì chỉ cần xem các phương thức của class, chứ không phải lục cả chương trình.
Thiết kế class có đóng gói
Ở bài lớp và đối tượng trong C++ chúng ta đã biết public và private. Đóng gói không chỉ là để thuộc tính private, mà còn là chọn phương thức public nào cho hợp lý:
#include <iostream>
#include <string>
class Sach
{
public:
Sach(const std::string& ten, int soLuong)
: ten_(ten), soLuong_(soLuong >= 0 ? soLuong : 0) {}
// getter: chi doc, khong sua
const std::string& ten() const { return ten_; }
int soLuong() const { return soLuong_; }
// thay vi setter soLuong tuy y, chi cho cac thao tac co nghia
bool muon()
{
if (soLuong_ == 0)
return false;
soLuong_--;
return true;
}
bool nhapThem(int soCuon)
{
if (soCuon <= 0)
return false;
soLuong_ += soCuon;
return true;
}
private:
std::string ten_;
int soLuong_;
};
int main()
{
Sach s("Lap trinh C++", -3); // so am bi chan ngay tu dau
std::cout << s.ten() << ": " << s.soLuong() << " cuon\n";
std::cout << "Muon: " << (s.muon() ? "duoc" : "het sach") << '\n';
std::cout << "Nhap -5 cuon: " << (s.nhapThem(-5) ? "ok" : "bi tu choi") << '\n';
std::cout << "Nhap 4 cuon: " << (s.nhapThem(4) ? "ok" : "bi tu choi") << '\n';
std::cout << "Muon: " << (s.muon() ? "duoc" : "het sach") << '\n';
std::cout << s.ten() << ": " << s.soLuong() << " cuon\n";
return 0;
}
Hàm Sach(...) cùng tên với class là hàm khởi tạo (constructor), chạy ngay khi tạo đối tượng. Phần : ten_(ten), soLuong_(...) gán giá trị ban đầu cho thuộc tính. Chi tiết về constructor có ở bài constructor và destructor, ở đây các bạn chỉ cần biết nó cũng kiểm tra dữ liệu: tạo sách với -3 cuốn thì số lượng thành 0.
Getter và setter
- Getter là phương thức trả về giá trị thuộc tính, như
ten()vàsoLuong(). - Setter là phương thức gán giá trị mới cho thuộc tính.
Nhiều người mới học viết đủ cặp getter/setter cho mọi thuộc tính, kiểu setSoLuong(int). Như vậy thì gần như không khác gì để public: ai cũng gán được bất kỳ con số nào.
Class Sach ở trên không có setSoLuong. Thay vào đó nó có muon() và nhapThem(), đúng với những việc thật sự xảy ra trong thư viện. Mỗi phương thức tự kiểm tra điều kiện, và trả về bool để bên ngoài biết thao tác có thành công không.
Lưu ý: Đừng viết setter cho mọi thuộc tính theo phản xạ. Hãy tự hỏi “ngoài đời, giá trị này thay đổi bằng hành động gì?” rồi đặt tên phương thức theo hành động đó: muon(), tra(), nhapThem(), doiMatKhau(). Thuộc tính nào không bao giờ đổi sau khi tạo, như mã sách, thì chỉ cần getter.
Phương thức const
Chữ const sau tên phương thức như int soLuong() const hứa rằng phương thức này không sửa thuộc tính nào của đối tượng. Quên const thì sao? Thử gọi getter không const trên một đối tượng được truyền vào hàm bằng const&:
#include <iostream>
#include <string>
class Sach
{
public:
Sach(const std::string& ten, int soLuong) : ten_(ten), soLuong_(soLuong) {}
int soLuong() { return soLuong_; } // quen const
const std::string& ten() const { return ten_; }
private:
std::string ten_;
int soLuong_;
};
void inSach(const Sach& s)
{
std::cout << s.ten() << ": " << s.soLuong() << " cuon\n";
}
int main()
{
Sach s("Lap trinh C++", 3);
inSach(s);
return 0;
}
inSach nhận const Sach&, tức là hứa không sửa sách. Trên đối tượng const chỉ được gọi phương thức const. soLuong() thiếu const nên g++ coi như nó có thể sửa dữ liệu và từ chối, dù thực tế hàm chỉ đọc. Lỗi discards qualifiers này rất hay gặp khi mới học.
Quy tắc đơn giản: phương thức nào không sửa thuộc tính thì luôn thêm const. Truyền đối tượng bằng const& như bài về hàm là cách làm chuẩn, nên thiếu const sẽ gặp lỗi khắp nơi.
Lỗi phá vỡ đóng gói: getter trả về tham chiếu
Getter trả về tham chiếu để khỏi sao chép là hợp lý, nhưng phải là tham chiếu const. Quên const thì getter thành cửa sau cho bên ngoài sửa thẳng thuộc tính private:
#include <iostream>
#include <string>
class DocGia
{
public:
DocGia(const std::string& ten) : ten_(ten) {}
std::string& ten() { return ten_; } // tra ve tham chieu khong const: lo du lieu
private:
std::string ten_;
};
int main()
{
DocGia d("Nguyen Van An");
d.ten() = ""; // sua thang thuoc tinh private
std::cout << "Ten doc gia: '" << d.ten() << "'\n";
return 0;
}
Tên độc giả bị xóa thành chuỗi rỗng, g++ không báo gì vì code hoàn toàn hợp lệ. ten_ vẫn là private trên giấy, nhưng getter đã trao tham chiếu tới nó ra ngoài.
Lưu ý: Getter trả về tham chiếu thì luôn viết const std::string& ten() const, có cả 2 chữ const. Hoặc trả về bản sao std::string ten() const nếu dữ liệu nhỏ.
Hàm friend
Đôi khi một hàm bên ngoài class cần đọc dữ liệu private, ví dụ hàm in phiếu kiểm kê. C++ cho phép class khai báo hàm đó là friend (bạn):
#include <iostream>
#include <string>
class Sach
{
public:
Sach(const std::string& ten, int soLuong) : ten_(ten), soLuong_(soLuong) {}
friend void inPhieuKiemKe(const Sach& s); // ham ban duoc xem du lieu private
private:
std::string ten_;
int soLuong_;
};
void inPhieuKiemKe(const Sach& s)
{
std::cout << "[Kiem ke] " << s.ten_ << " | ton kho: " << s.soLuong_ << '\n';
}
int main()
{
Sach s("Clean Code", 2);
inPhieuKiemKe(s);
return 0;
}
inPhieuKiemKe không phải phương thức của Sach nhưng đọc được ten_ và soLuong_. Quyền này do chính class cấp, bên ngoài không tự nhận mình là friend được.
friend là một lỗ hổng có kiểm soát trong đóng gói, nên dùng ít thôi. Trường hợp hợp lý nhất là nạp chồng toán tử << để in đối tượng bằng std::cout, chúng ta sẽ gặp ở bài nạp chồng toán tử. Còn ở ví dụ trên, thêm getter là đủ, không cần friend.
Bài tập
- Viết class
TaiKhoanThuViengồm mã thẻ (không đổi sau khi tạo), tiền phạt đang nợ. Thiết kế phương thứcghiPhat(int)vàthanhToan(int)sao cho tiền nợ không bao giờ âm. - Thêm getter cho class ở bài 1. Getter nào cần
const? - Viết hàm
void inTaiKhoan(const TaiKhoanThuVien& tk). Bỏconstở một getter và đọc thông báo lỗi. - Class
Sachcó nên có phương thứcdatSoLuong(int)không? Hãy nêu một tình huống thật trong thư viện cần sửa trực tiếp số lượng, và đề xuất tên phương thức hợp lý hơn.
Lời giải sẽ có trong bài tổng hợp bài tập OOP C++ nhé.
Kết
Tính đóng gói trong C++ không chỉ là viết chữ private, mà là thiết kế class sao cho đối tượng luôn ở trạng thái hợp lệ: thuộc tính giấu kín, thay đổi qua các phương thức có tên theo hành động thật và có kiểm tra, getter trả về const. Làm đúng điều này thì phần lớn lỗi dữ liệu sai bị chặn ngay từ khi biên dịch.
Muốn tra cứu chi tiết về phạm vi truy cập và hàm friend, các bạn xem thêm tài liệu chuẩn trên cppreference.
Bài trước là lớp và đối tượng trong C++, bài tiếp theo chúng ta học tính kế thừa trong C++ để dùng lại code giữa các class. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè đang học C++ nhé.






