Ai được phép chạm vào dữ liệu của bạn
Khi viết lớp đầu tiên, có lẽ bạn đã để lộ mọi thành viên ra bên ngoài. Cách đó vẫn chạy, nhưng nó bỏ đi một trong những lý do chính khiến lớp tồn tại: tính đóng gói (encapsulation), tức che giấu trạng thái bên trong của lớp để phần còn lại của chương trình chỉ tương tác với nó qua một bề mặt được kiểm soát. Access specifier (phạm vi truy cập) là cách bạn vạch ra ranh giới đó.
Có đúng ba loại: public, private và protected. Mỗi loại gắn nhãn cho các thành viên đứng sau nó, và nhãn đó quyết định đoạn code nào được phép đọc hoặc ghi chúng. Làm đúng thì lớp của bạn tự bảo vệ các quy tắc của nó; làm sai thì một bug ở bất cứ đâu cũng có thể làm hỏng trạng thái của đối tượng.
Ba access specifier
Một specifier là một từ khóa theo sau là dấu hai chấm. Mọi thành viên khai báo sau nó, cho tới specifier tiếp theo, đều thuộc mức truy cập đó.
Bỏ chú thích ở dòng cuối và trình biên dịch sẽ từ chối build: balance là private, nên main không thể động trực tiếp vào nó. Đó chính là mục đích: cách duy nhất để thay đổi số dư là qua deposit, nghĩa là sau này bạn có thể thêm bước kiểm tra (không cho nạp số âm, ghi log, giới hạn) ở một chỗ duy nhất và tin chắc nó luôn được áp dụng.
Đây là bảng tổng hợp đầy đủ:
// reachable from...
// public anywhere (any code that has the object)
// private only the same class's own members (+ friends)
// protected the same class's members AND derived classes (+ friends)
class và struct: mặc định khác nhau
Bạn có thể viết bao nhiêu khối specifier tùy thích, theo thứ tự bất kỳ. Các thành viên đứng trước specifier đầu tiên có mức truy cập nào thì tùy vào việc bạn dùng class hay struct:
- Trong
class, thành viên mặc định làprivate. - Trong
struct, thành viên mặc định làpublic.
Mặc định đó là khác biệt duy nhất ở mức ngôn ngữ giữa hai từ khóa. Một struct cũng có thể có phương thức, hàm khởi tạo và phần private giống hệt class.
Quy ước thường là dùng struct cho những gói dữ liệu public đơn giản và class khi bạn muốn có hành vi và trạng thái ẩn, nhưng trình biên dịch không bắt buộc điều đó, chỉ có mặc định là khác nhau.
Đóng gói với getter và setter
Khuôn mẫu hằng ngày là: dữ liệu nằm trong phần private, và một phương thức public cung cấp quyền truy cập có kiểm soát. Getter chỉ đọc sẽ trả về giá trị; setter kiểm tra trước khi gán. Đây là lúc private phát huy tác dụng.
Vì celsius là private, không có cách nào lén đưa một giá trị không hợp lệ vào: mọi lần ghi đều phải đi qua setCelsius, hàm bảo vệ bất biến của lớp. Để ý các getter được đánh dấu const: chúng hứa không sửa đổi đối tượng, nên bạn gọi được chúng cả trên các đối tượng const Temperature.
protected và kế thừa
protected chỉ có ý nghĩa khi có kế thừa. Với code bên ngoài, nó hoạt động như private, nhưng lớp dẫn xuất có thể truy cập nó. Dùng nó cho những thành viên mà lớp con thật sự cần nhưng bên ngoài vẫn không nên chạm vào.
Một lỗi phổ biến của người mới là dùng protected cho mọi thành viên dữ liệu "phòng khi lớp con cần". Điều đó âm thầm nới rộng hợp đồng của lớp: giờ lớp con nào cũng có thể phụ thuộc vào trường đó, và bạn không còn tự do thay đổi nó. Hãy ưu tiên private và chỉ nâng lên protected khi lớp dẫn xuất thật sự cần quyền truy cập.
Lối thoát friend
Đôi khi một hàm hoặc một lớp bên ngoài thật sự cần nhìn vào bên trong lớp của bạn. Trường hợp kinh điển là nạp chồng toán tử <<, thứ không thể làm thành phương thức thành viên. Từ khóa friend cấp cho đúng thực thể được nêu tên đó quyền truy cập vào các thành viên private và protected của bạn, và không ai khác.
friend là một ngoại lệ có chủ ý và rất chính xác: chính lớp nêu đích danh ai được tin tưởng, nên không ai có thể tự cấp quyền cho mình từ bên ngoài. Hãy dùng nó một cách tiết kiệm; nếu thấy mình thêm quá nhiều friend, có lẽ các thành viên đó ngay từ đầu không nên là private, hoặc thiết kế của bạn cần được xem lại.
Lỗi thường gặp cần tránh
- Để mọi thành viên là
public. Nghe thì dễ, nhưng bạn mất hết khả năng kiểm tra và các bất biến mà tính đóng gói mang lại. Mặc định hãy để dữ liệuprivatecùng các phương thứcpublic. - Quên mặc định của
class.class Foo { int x; };khiếnxlà private, nênfoo.x = 5sẽ không biên dịch được. Nếu bạn muốn một gói dữ liệu đơn giản, hãy dùngstructhoặc thêm nhãnpublic:. - Lạm dụng
protected. Đây là ranh giới yếu hơnprivatevà chỉ liên quan khi có kế thừa. Dùng nó khắp nơi sẽ gắn chặt lớp con với những trường mà bạn có thể muốn thay đổi. - Coi
privatelà tính năng bảo mật. Đó là quy tắc lúc biên dịch nhằm ngăn truy cập nhầm lẫn, không phải mã hóa. Các byte vẫn nằm trong bộ nhớ;privatelà để thiết kế gọn gàng, không phải để giữ bí mật.
Tiếp theo: struct
Giờ bạn đã thấy struct thực chất chỉ là một class có thành viên mặc định là public. Trang tiếp theo, struct, đi sâu vào khi nào mặc định public đó chính là thứ bạn cần, tức những cấu trúc gọn nhẹ để gom các giá trị liên quan, và cách struct được dùng trong C++ chuẩn mực bên cạnh các lớp đầy đủ tính năng.
Câu hỏi thường gặp
public, private và protected trong C++ khác nhau thế nào?
Thành viên public truy cập được từ bất cứ đâu. Thành viên private chỉ truy cập được từ bên trong chính lớp đó (và các friend của nó). protected giống private nhưng cho phép thêm các lớp dẫn xuất truy cập. Theo quy ước, bạn giữ dữ liệu ở private và để lộ hành vi qua các phương thức public.
Thành viên trong C++ có mặc định là private không?
Trong class thì có: mọi thứ đều là private cho đến khi bạn viết một access specifier. Trong struct, mặc định là public. Mặc định duy nhất này là khác biệt thật sự duy nhất giữa class và struct trong C++; cả hai đều có thể có phương thức, hàm khởi tạo và access specifier.
Từ khóa friend trong C++ dùng để làm gì?
friend cấp cho một hàm hoặc một lớp cụ thể quyền truy cập vào các thành viên private và protected của bạn. Đây là một ngoại lệ có chủ ý và rất hẹp đối với tính đóng gói: lớp tự nêu đích danh ai được tin tưởng, nên quyền truy cập không bao giờ được cấp một cách ngầm định.