Giao diện
🏛️ OOP & Encapsulation: Lớp Vỏ Bảo Vệ Module 2
Mọi chương trình lớn đều cần tổ chức. Khi codebase vượt qua vài trăm dòng, bạn không thể quản lý bằng hàm rời rạc được nữa. OOP trong C++ không phải lý thuyết hàn lâm — nó là công cụ sinh tồn để giữ code không biến thành mớ hỗn độn.
🎯 Mục tiêu
Sau bài này, bạn sẽ:
- Hiểu tại sao encapsulation quan trọng (không chỉ biết cú pháp)
- Viết được class C++ hoàn chỉnh với access modifiers
- Phân biệt
structvsclassvà biết khi nào dùng cái nào - Áp dụng const correctness — thói quen quan trọng nhất khi viết class
- Sử dụng member initializer list đúng cách
- Nhận diện anti-pattern "public data member" và cách phòng tránh
📋 PREREQUISITES
- ✅ Vector — Container đầu tiên
- ✅ Hiểu biến, hàm, vòng lặp cơ bản
- ✅ Biết
std::stringvàstd::vectorở mức cơ bản
1. Tại Sao OOP? — Vấn Đề Thực Sự
Khi code bắt đầu "vỡ trận"
Hãy tưởng tượng bạn đang viết phần mềm quản lý tài khoản ngân hàng. Ban đầu đơn giản:
cpp
// Cách tiếp cận "thô" — không có class
std::string owner = "Nguyen Van A";
double balance = 1000.0;
// Rút tiền
balance -= 500.0; // OK
// Nhưng... không ai chặn được điều này:
balance = -999999.0; // ❌ Số dư âm? Ngân hàng phá sản!Bạn thấy vấn đề chưa? Bất kỳ ai trong codebase đều có thể sửa balance thành giá trị vô nghĩa. Không có rào chắn. Không có validation. Không có kiểm soát.
Encapsulation = Kiểm soát truy cập
Encapsulation (đóng gói) là ý tưởng đơn giản nhưng mạnh mẽ:
Dữ liệu nhạy cảm phải được bảo vệ. Muốn thay đổi? Phải đi qua "cổng chính" (methods) có kiểm tra.
Giống như tài khoản ngân hàng thật — bạn không tự in tiền được. Muốn rút tiền? Đi qua quầy giao dịch, hệ thống kiểm tra số dư, rồi mới cho rút.
┌─────────────────────────────────────────────────┐
│ BankAccount (Class) │
├─────────────────────────────────────────────────┤
│ 🔒 PRIVATE (Bên trong két sắt) │
│ ┌─────────────────────────────────────┐ │
│ │ owner_: "Nguyen Van A" │ │
│ │ balance_: 1000.0 │ │
│ └─────────────────────────────────────┘ │
│ │
│ 🔓 PUBLIC (Quầy giao dịch) │
│ ┌─────────────────────────────────────┐ │
│ │ deposit(amount) ──► validate ─►✅ │ │
│ │ withdraw(amount) ──► validate ─►✅ │ │
│ │ balance() const ──► read-only ─►✅│ │
│ └─────────────────────────────────────┘ │
│ │
│ ❌ Truy cập trực tiếp balance_ từ bên ngoài? │
│ → COMPILER ERROR │
└─────────────────────────────────────────────────┘📌 Tư duy production
Encapsulation không phải "bài tập lý thuyết" — nó là system integrity. Trong production, một biến bị sửa sai giá trị có thể gây mất tiền, mất dữ liệu, hoặc crash ở 3 giờ sáng. Class với access control là tuyến phòng thủ đầu tiên.
2. Giải Phẫu Một Class C++
Class đầu tiên: BankAccount
cpp
#include <iostream>
#include <string>
#include <utility> // std::move
class BankAccount {
private:
// === DATA MEMBERS (thuộc tính) ===
std::string owner_;
double balance_;
public:
// === CONSTRUCTOR (hàm khởi tạo) ===
BankAccount(std::string owner, double initial_balance)
: owner_{std::move(owner)}, balance_{initial_balance}
{
if (initial_balance < 0) {
throw std::invalid_argument("Số dư khởi tạo không được âm");
}
}
// === MEMBER FUNCTIONS (phương thức) ===
bool deposit(double amount) {
if (amount <= 0) return false;
balance_ += amount;
return true;
}
bool withdraw(double amount) {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
// Getter — const vì không sửa đổi object
[[nodiscard]] double balance() const { return balance_; }
[[nodiscard]] const std::string& owner() const { return owner_; }
// In thông tin — cũng là const
void print() const {
std::cout << "Chủ TK: " << owner_
<< " | Số dư: " << balance_ << '\n';
}
};
int main() {
BankAccount acc{"Nguyen Van A", 1000.0};
acc.print(); // Chủ TK: Nguyen Van A | Số dư: 1000
acc.deposit(500.0);
acc.withdraw(200.0);
acc.print(); // Chủ TK: Nguyen Van A | Số dư: 1300
// acc.balance_ = -999; // ❌ COMPILE ERROR — private!
// Đây chính là sức mạnh encapsulation
std::cout << "Số dư hiện tại: " << acc.balance() << '\n'; // OK
return 0;
}Phân tích từng thành phần
| Thành phần | Vai trò | Ví dụ |
|---|---|---|
| Data members | Lưu trữ trạng thái nội bộ | owner_, balance_ |
| Constructor | Khởi tạo object hợp lệ | BankAccount(...) |
| Member functions | Hành vi, thao tác trên dữ liệu | deposit(), withdraw() |
| Access specifiers | Kiểm soát ai được truy cập gì | private:, public: |
| const methods | Cam kết không thay đổi object | balance() const |
Con trỏ this — "Tôi" trong class
Mỗi member function đều ngầm nhận một con trỏ this trỏ đến object đang gọi:
cpp
class Counter {
private:
int count_ = 0;
public:
void increment() {
this->count_++; // Tường minh dùng this
count_++; // Ngầm định — trình biên dịch tự hiểu
// Hai dòng trên tương đương
}
// Fluent API: trả về *this để chain gọi
Counter& add(int n) {
count_ += n;
return *this;
}
[[nodiscard]] int count() const { return count_; }
};
// Sử dụng:
Counter c;
c.add(3).add(5).add(2); // Method chaining nhờ return *this
std::cout << c.count(); // 10💡 Khi nào viết this-> tường minh?
Hầu hết trường hợp bạn không cần viết this->. Chỉ cần khi có name collision (tham số trùng tên data member) hoặc khi trả về *this cho method chaining.
3. struct vs class — Khác Biệt Duy Nhất
Sự thật đơn giản
Trong C++, struct và class gần như giống hệt nhau. Khác biệt duy nhất:
struct | class | |
|---|---|---|
| Default access | public | private |
| Default inheritance | public | private |
Chỉ vậy thôi. Không có gì khác nữa.
cpp
// Hai cách viết TƯƠNG ĐƯƠNG:
struct PointA {
double x; // public (mặc định của struct)
double y;
};
class PointB {
public: // Phải khai báo tường minh
double x;
double y;
};Convention trong thực tế
Mặc dù compiler không phân biệt, developer phân biệt rõ:
cpp
// ✅ struct: Dữ liệu đơn thuần, không có invariant cần bảo vệ
struct Color {
uint8_t r, g, b, a;
};
struct Point2D {
double x, y;
};
struct Config {
int port;
std::string host;
bool verbose;
};
// ✅ class: Có invariant, logic nghiệp vụ, cần encapsulation
class BankAccount {
// balance_ không bao giờ được âm → cần bảo vệ
};
class DatabaseConnection {
// resource management → cần constructor/destructor
};
class HttpRequest {
// headers phải valid, body phải match content-type → cần validation
};📌 Quy tắc ngón tay cái
struct = "Tập hợp dữ liệu, mọi giá trị đều hợp lệ" (ví dụ: tọa độ, màu sắc, config)
class = "Có quy tắc ràng buộc (invariant) cần bảo vệ" (ví dụ: số dư ≥ 0, connection phải mở trước khi query)
4. Access Modifiers — Ba Tầng Kiểm Soát
private — Két sắt
cpp
class Vault {
private:
std::string secret_ = "mật_khẩu_ngân_hàng";
public:
bool authenticate(const std::string& password) const {
return password == secret_; // Chỉ class tự truy cập được
}
};
Vault v;
// v.secret_; // ❌ COMPILE ERROR
v.authenticate("abc"); // ✅ Đi qua phương thức publicNguyên tắc: Mặc định mọi thứ đều private. Chỉ mở public khi thực sự cần.
public — Cửa chính
Phần giao diện (interface) mà bên ngoài được phép sử dụng:
cpp
class Calculator {
public:
// API công khai — đây là "hợp đồng" với người dùng class
double add(double a, double b) const { return a + b; }
double multiply(double a, double b) const { return a * b; }
private:
// Chi tiết implementation — có thể thay đổi mà không ảnh hưởng bên ngoài
double round_result(double val, int decimals) const;
};protected — Dành cho "người thừa kế"
cpp
class Shape {
protected:
// Chỉ class con (derived) mới truy cập được
double x_, y_;
public:
void move(double dx, double dy) {
x_ += dx;
y_ += dy;
}
};
class Circle : public Shape {
private:
double radius_;
public:
double area() const {
// ✅ Truy cập x_, y_ được vì protected
return 3.14159 * radius_ * radius_;
}
};⚠️ protected — Cẩn thận khi dùng
Trong thực tế, protected ít dùng hơn bạn nghĩ. Nó tạo coupling giữa class cha và class con. Nhiều codebase lớn (Google, LLVM) khuyên: ưu tiên private + public getter thay vì protected trực tiếp.
Tóm tắt bằng bảng
┌────────────────────────────────────────────────────┐
│ AI TRUY CẬP ĐƯỢC? │
├──────────────┬──────────┬───────────┬──────────────┤
│ Modifier │ Class │ Derived │ Bên ngoài │
│ │ chính nó │ class │ │
├──────────────┼──────────┼───────────┼──────────────┤
│ private │ ✅ │ ❌ │ ❌ │
│ protected │ ✅ │ ✅ │ ❌ │
│ public │ ✅ │ ✅ │ ✅ │
└──────────────┴──────────┴───────────┴──────────────┘5. const Correctness — Triết Lý "Không Sửa Thì Nói Rõ"
Tại sao const quan trọng đến vậy?
Trong C++, const không chỉ là "gợi ý" — nó là hợp đồng được compiler kiểm tra:
cpp
class Temperature {
private:
double celsius_;
public:
explicit Temperature(double c) : celsius_{c} {}
// ✅ const method — cam kết KHÔNG sửa đổi object
[[nodiscard]] double celsius() const { return celsius_; }
[[nodiscard]] double fahrenheit() const {
return celsius_ * 9.0 / 5.0 + 32.0;
}
// Non-const — CÓ sửa đổi object
void set(double new_celsius) { celsius_ = new_celsius; }
};Hậu quả khi thiếu const
cpp
// Hàm nhận const reference — RẤT phổ biến trong C++
void print_temp(const Temperature& t) {
std::cout << t.celsius() << "°C\n"; // ✅ OK — celsius() là const
// t.set(100.0); // ❌ COMPILE ERROR — không thể gọi non-const
// method trên const reference
}Nếu bạn quên đánh dấu celsius() là const:
cpp
// ❌ SAI — thiếu const
double celsius() { return celsius_; } // Compiler nghĩ hàm này CÓ THỂ sửa object
void print_temp(const Temperature& t) {
t.celsius(); // ❌ COMPILE ERROR!
// Mặc dù celsius() không sửa gì, nhưng compiler không biết điều đó
}☠️ Anti-Pattern: Quên const trên getter
Đây là lỗi #1 của người mới viết class trong C++. Hàm getter luôn luôn phải là const. Thiếu const = bạn đang nói với compiler "hàm này có thể sửa object", và mọi nơi nhận const& sẽ không gọi được hàm đó.
const như tài liệu sống
cpp
class UserProfile {
private:
std::string name_;
int age_;
std::vector<std::string> hobbies_;
public:
// Chỉ cần đọc method signatures, bạn biết ngay:
// - Hàm nào chỉ ĐỌC (const)
// - Hàm nào SỬA ĐỔI (non-const)
[[nodiscard]] const std::string& name() const { return name_; } // 📖 Đọc
[[nodiscard]] int age() const { return age_; } // 📖 Đọc
[[nodiscard]] const auto& hobbies() const { return hobbies_; } // 📖 Đọc
void rename(std::string new_name) { name_ = std::move(new_name); } // ✏️ Sửa
void add_hobby(std::string h) { hobbies_.push_back(std::move(h)); } // ✏️ Sửa
void celebrate_birthday() { ++age_; } // ✏️ Sửa
};📌 Quy tắc vàng
Mọi method không sửa đổi object → đánh dấu const.
Đây là thói quen nhỏ nhưng tạo ra khác biệt lớn. Code review ở Google, Meta, Microsoft đều coi thiếu const trên getter là bug cần fix.
6. Member Initializer List — Cách Khởi Tạo Đúng
Tại sao không dùng assignment trong constructor body?
cpp
class Employee {
private:
std::string name_;
int id_;
double salary_;
public:
// ❌ CÁCH TỆ: Assignment trong body
Employee(std::string name, int id, double salary) {
name_ = name; // Bước 1: default-construct name_ (string rỗng)
id_ = id; // Bước 2: rồi mới gán lại
salary_ = salary; // → Lãng phí: construct 2 lần!
}
// ✅ CÁCH TỐT: Member initializer list
Employee(std::string name, int id, double salary)
: name_{std::move(name)}, id_{id}, salary_{salary}
{
// Body trống — mọi thứ đã khởi tạo xong rồi
// Chỉ cần body khi có logic phức tạp (validation, logging)
}
};Bắt buộc dùng initializer list khi:
cpp
class MustUseInitList {
private:
const int kMaxSize; // 1. const member — không thể gán lại
int& ref_; // 2. reference member — phải bind lúc tạo
// std::unique_ptr<T> ptr_; // 3. Move-only types
public:
MustUseInitList(int max, int& r)
: kMaxSize{max}, ref_{r} // BẮT BUỘC dùng init list
{}
};In-class member initializers (C++11)
cpp
class ServerConfig {
private:
// Default values — rõ ràng, dễ đọc
int port_ = 8080;
std::string host_ = "localhost";
bool verbose_ = false;
int max_connections_ = 100;
public:
// Constructor có thể override một số giá trị
explicit ServerConfig(int port) : port_{port} {}
// Hoặc dùng default constructor — mọi thứ có giá trị mặc định
ServerConfig() = default;
};📌 Best practice: Kết hợp cả hai
Dùng in-class initializers cho giá trị mặc định hợp lý. Dùng initializer list trong constructor khi cần override. Đây là style được khuyên dùng trong C++ Core Guidelines (C.45, C.48).
7. Naming Conventions — Cái Tên Nói Lên Tất Cả
Trailing underscore cho private members
cpp
class UserSession {
private:
std::string token_; // ✅ Trailing underscore
int user_id_; // ✅ Dễ phân biệt: đây là data member
bool is_authenticated_; // ✅ Nhất quán
public:
// Getter KHÔNG có prefix "get"
[[nodiscard]] const std::string& token() const { return token_; }
[[nodiscard]] int user_id() const { return user_id_; }
[[nodiscard]] bool is_authenticated() const { return is_authenticated_; }
// Setter rõ ràng — dùng tên mô tả hành vi
void authenticate(const std::string& new_token) {
token_ = new_token;
is_authenticated_ = true;
}
void logout() {
token_.clear();
is_authenticated_ = false;
}
};Tại sao balance() thay vì getBalance()?
cpp
// ❌ Java-style — verbose không cần thiết trong C++
int getAge() const;
std::string getName() const;
// ✅ C++-style — ngắn gọn, đọc tự nhiên hơn
int age() const;
const std::string& name() const;
// Khi đọc code:
auto a = user.age(); // ✅ Tự nhiên như tiếng Anh
auto a = user.getAge(); // ❌ "get" thừa — biết rồi, đang lấy giá trị mà!💡 Style guides phổ biến
- Google C++ Style:
snake_casecho hàm/biến, trailing_cho members - C++ Core Guidelines: Tương tự Google style
- LLVM/Clang:
CamelCasecho class,camelCasecho hàm
Chọn một style và giữ nhất quán trong toàn project. Nhất quán quan trọng hơn style nào.
8. Ví Dụ Thực Tế: Hệ Thống Đơn Hàng
Bài toán
Xây dựng class Order cho hệ thống e-commerce, đảm bảo:
total_luôn khớp với tổng giá các itemstatus_chỉ chuyển theo luồng hợp lệ (pending → confirmed → shipped → delivered)- Không ai có thể sửa trực tiếp dữ liệu nội bộ
cpp
#include <iostream>
#include <string>
#include <vector>
#include <numeric> // std::accumulate
#include <stdexcept>
#include <utility>
enum class OrderStatus {
Pending,
Confirmed,
Shipped,
Delivered,
Cancelled
};
struct OrderItem {
std::string name;
double price;
int quantity;
[[nodiscard]] double subtotal() const {
return price * quantity;
}
};
class Order {
private:
std::string order_id_;
std::string customer_;
std::vector<OrderItem> items_;
double total_ = 0.0;
OrderStatus status_ = OrderStatus::Pending;
// Helper nội bộ — private, không ai bên ngoài gọi được
void recalculate_total() {
total_ = std::accumulate(
items_.begin(), items_.end(), 0.0,
[](double sum, const OrderItem& item) {
return sum + item.subtotal();
}
);
}
public:
Order(std::string id, std::string customer)
: order_id_{std::move(id)}, customer_{std::move(customer)}
{}
// === OPERATIONS (thay đổi state) ===
void add_item(std::string name, double price, int qty) {
if (status_ != OrderStatus::Pending) {
throw std::logic_error("Chỉ thêm item khi đơn hàng đang Pending");
}
if (price < 0 || qty <= 0) {
throw std::invalid_argument("Giá và số lượng phải hợp lệ");
}
items_.push_back({std::move(name), price, qty});
recalculate_total(); // Invariant: total_ luôn đồng bộ
}
bool remove_item(size_t index) {
if (status_ != OrderStatus::Pending) return false;
if (index >= items_.size()) return false;
items_.erase(items_.begin() + static_cast<ptrdiff_t>(index));
recalculate_total(); // Invariant: total_ luôn đồng bộ
return true;
}
void confirm() {
if (status_ != OrderStatus::Pending || items_.empty()) {
throw std::logic_error("Không thể xác nhận đơn hàng rỗng hoặc không ở trạng thái Pending");
}
status_ = OrderStatus::Confirmed;
}
void ship() {
if (status_ != OrderStatus::Confirmed) {
throw std::logic_error("Phải confirm trước khi ship");
}
status_ = OrderStatus::Shipped;
}
// === QUERIES (chỉ đọc — tất cả const) ===
[[nodiscard]] const std::string& order_id() const { return order_id_; }
[[nodiscard]] const std::string& customer() const { return customer_; }
[[nodiscard]] double total() const { return total_; }
[[nodiscard]] OrderStatus status() const { return status_; }
[[nodiscard]] size_t item_count() const { return items_.size(); }
[[nodiscard]] const std::vector<OrderItem>& items() const {
return items_;
}
void print_summary() const {
std::cout << "Đơn hàng: " << order_id_
<< " | Khách: " << customer_
<< " | " << items_.size() << " items"
<< " | Tổng: " << total_ << " VND\n";
for (const auto& item : items_) {
std::cout << " - " << item.name
<< " x" << item.quantity
<< " = " << item.subtotal() << " VND\n";
}
}
};
int main() {
Order order{"ORD-001", "Tran Thi B"};
order.add_item("Laptop", 25000000, 1);
order.add_item("Chuột không dây", 350000, 2);
order.print_summary();
// Invariant tự động: total_ = 25000000 + 700000 = 25700000
std::cout << "Tổng tiền: " << order.total() << " VND\n";
order.confirm();
order.ship();
// order.add_item("USB", 100000, 1); // ❌ throw — đã ship rồi!
return 0;
}Chú ý cách class Order bảo vệ invariant:
total_luôn được recalculate mỗi khi items thay đổi — không ai "set total" trực tiếp- Status chỉ chuyển theo luồng hợp lệ — không thể nhảy từ Pending sang Delivered
- Không thể thêm item sau khi đã confirm
9. Bài Tập Nhanh ⚡
🧩 Fast Exercise: Truy Cập Hợp Lệ?
Cho class sau, xác định dòng nào hợp lệ khi gọi từ main():
cpp
class Student {
private:
std::string name_;
double gpa_;
protected:
int student_id_;
public:
Student(std::string n, double g, int id)
: name_{std::move(n)}, gpa_{g}, student_id_{id} {}
const std::string& name() const { return name_; }
double gpa() const { return gpa_; }
void update_gpa(double new_gpa) {
if (new_gpa >= 0.0 && new_gpa <= 4.0) gpa_ = new_gpa;
}
};
int main() {
Student s{"Lan", 3.5, 12345};
s.name(); // (A)
s.gpa_ = 4.0; // (B)
s.student_id_ = 999; // (C)
s.update_gpa(3.8); // (D)
s.gpa(); // (E)
}💡 Đáp án
- (A) ✅ —
name()làpublic, gọi thoải mái - (B) ❌ —
gpa_làprivate, không truy cập trực tiếp được - (C) ❌ —
student_id_làprotected, chỉ class con mới truy cập được - (D) ✅ —
update_gpa()làpublic, có validation bên trong - (E) ✅ —
gpa()làpublicconst getter
Bài học: Từ bên ngoài class, chỉ public members là truy cập được. protected chỉ dành cho inheritance, còn private chỉ dành cho chính class đó.
10. Spot the Bug 🐛
☠️ Bug Hunt: Class Nào Có Vấn Đề?
cpp
class Product {
public:
std::string name;
double price;
int stock;
double discount_price(double percent) {
price = price * (1.0 - percent / 100.0); // (1)
return price;
}
};
int main() {
Product p{"Áo thun", 200000, 50};
// In giá giảm 10%
std::cout << "Giá sale: " << p.discount_price(10) << '\n';
// In giá gốc???
std::cout << "Giá gốc: " << p.price << '\n';
}Có bao nhiêu vấn đề? Liệt kê tất cả.
💡 Gợi ý
Suy nghĩ về:
- Access modifiers đang ở đâu?
- Hàm
discount_pricecó side effect gì? - Có invariant nào bị vi phạm không?
🔍 Phân tích chi tiết
Vấn đề 1: Public data members — name, price, stock đều public. Bất kỳ ai cũng có thể p.price = -1 hoặc p.stock = -999.
Vấn đề 2: Side effect nguy hiểm — discount_price() sửa luôn price gốc! Gọi 2 lần = giảm giá 2 lần. Đây là bug kinh điển.
Vấn đề 3: Thiếu const — Hàm lẽ ra chỉ tính giá giảm, không nên sửa state.
Fix:
cpp
class Product {
private:
std::string name_;
double price_;
int stock_;
public:
Product(std::string name, double price, int stock)
: name_{std::move(name)}, price_{price}, stock_{stock} {}
// const — chỉ tính toán, không sửa gì
[[nodiscard]] double discount_price(double percent) const {
return price_ * (1.0 - percent / 100.0);
}
[[nodiscard]] double price() const { return price_; }
};Bây giờ discount_price() là pure query — gọi bao nhiêu lần cũng cho cùng kết quả.
11. Production Scenario 🏭
🏭 Tình huống thực tế: Config Validator
Trong hệ thống production, config sai = service crash. Hãy xây dựng class AppConfig đảm bảo mọi config luôn hợp lệ ngay từ lúc khởi tạo:
cpp
#include <string>
#include <stdexcept>
class AppConfig {
private:
int port_;
std::string db_host_;
int max_connections_;
int timeout_ms_;
// Validation logic nội bộ
static void validate_port(int p) {
if (p < 1 || p > 65535) {
throw std::out_of_range("Port phải từ 1-65535, nhận: " + std::to_string(p));
}
}
static void validate_connections(int c) {
if (c < 1 || c > 10000) {
throw std::out_of_range("Max connections phải từ 1-10000");
}
}
public:
AppConfig(int port, std::string host, int max_conn, int timeout)
: port_{port}
, db_host_{std::move(host)}
, max_connections_{max_conn}
, timeout_ms_{timeout}
{
// Validate TẤT CẢ trong constructor
validate_port(port_);
validate_connections(max_connections_);
if (db_host_.empty()) {
throw std::invalid_argument("DB host không được rỗng");
}
if (timeout_ms_ < 0) {
throw std::invalid_argument("Timeout không được âm");
}
}
// Chỉ expose getters — config immutable sau khi tạo
[[nodiscard]] int port() const { return port_; }
[[nodiscard]] const std::string& db_host() const { return db_host_; }
[[nodiscard]] int max_connections() const { return max_connections_; }
[[nodiscard]] int timeout_ms() const { return timeout_ms_; }
};
// Sử dụng:
// AppConfig cfg{8080, "db.prod.internal", 500, 30000}; ✅
// AppConfig bad{99999, "", -1, -1}; ❌ throw ngayNguyên tắc: Nếu object tồn tại, nó PHẢI ở trạng thái hợp lệ. Constructor là nơi đảm bảo điều đó. Đây là nền tảng của RAII — pattern bạn sẽ học ở bài tiếp theo.
12. Performance Note ⚡
⚡ Truyền Object: const& vs Copy
Đây là tối ưu #1 được nhắc trong mọi code review C++:
cpp
// ❌ Copy — tạo bản sao TOÀN BỘ object
void process_order(Order order) {
// Mỗi lần gọi = copy toàn bộ vector<OrderItem>
// Với đơn hàng 1000 items → copy 1000 items!
}
// ✅ const reference — ZERO copy, chỉ truyền địa chỉ (8 bytes)
void process_order(const Order& order) {
// Đọc được mọi const method
// Không copy gì cả
// const đảm bảo không sửa đổi
order.print_summary();
}
// ✅ Nếu CẦN sửa đổi object gốc
void update_order(Order& order) {
order.add_item("Gift wrap", 10000, 1);
}Quy tắc:
- Kiểu nhỏ (int, double, bool, char): truyền by value
- Kiểu lớn (string, vector, class): truyền by
const& - Cần sửa đổi: truyền by
&(non-const reference)
13. Tổng Kết
| Khái niệm | Ý nghĩa | Ghi nhớ |
|---|---|---|
| Encapsulation | Ẩn dữ liệu, kiểm soát truy cập | "Mọi thứ private trừ khi có lý do" |
| class vs struct | Default access khác nhau | struct = POD, class = có invariant |
| private / public / protected | Ba tầng kiểm soát | private → public → protected (ít dùng) |
| const member function | Cam kết không sửa object | Getter luôn là const |
| Initializer list | Khởi tạo trực tiếp, hiệu quả | Bắt buộc cho const/ref members |
| Naming | Trailing _, không prefix get | balance() thay vì getBalance() |
| Invariant | Ràng buộc luôn đúng | Constructor + private = bảo vệ invariant |
📝 Quiz — Kiểm Tra Hiểu Biết
🧠 Quiz
Câu 1: Khác biệt giữa struct và class trong C++ là gì?
- [ ] A) struct không có constructor, class có
- [ ] B) struct chỉ chứa dữ liệu, class chứa cả hàm
- [x] C) struct mặc định public, class mặc định private
- [ ] D) struct trên stack, class trên heap
💡 Giải thích: Trong C++, struct và class gần như hoàn toàn giống nhau. Khác biệt duy nhất là default access specifier: struct = public, class = private. Cả hai đều có thể có constructor, hàm, inheritance, v.v.
Câu 2: Đoạn code sau có lỗi gì?
cpp
class Circle {
private:
double radius_;
public:
Circle(double r) : radius_{r} {}
double area() { return 3.14159 * radius_ * radius_; }
};
void print_area(const Circle& c) {
std::cout << c.area();
}- [ ] A) Constructor thiếu
explicit - [x] B)
area()thiếuconst, không gọi được trênconst Circle& - [ ] C) Nên dùng struct thay vì class
- [ ] D)
radius_phải làpublic
💡 Giải thích: Hàm
print_areanhậnconst Circle&, nhưngarea()không được đánh dấuconst. Compiler không thể đảm bảoarea()không sửa object, nên từ chối compile. Fix:double area() const { ... }.
Câu 3: Khi nào BẮT BUỘC phải dùng member initializer list?
- [ ] A) Khi class có nhiều hơn 3 data members
- [ ] B) Khi constructor có parameters
- [x] C) Khi class có const members hoặc reference members
- [ ] D) Khi class kế thừa từ class khác
💡 Giải thích:
constmembers không thể gán lại sau khi tạo, và references phải được bind ngay lúc khởi tạo. Cả hai bắt buộc dùng member initializer list (hoặc in-class initializer). Inheritance cũng cần init list cho base class, nhưng câu C chính xác nhất.
Câu 4: Tại sao getter nên trả về const std::string& thay vì std::string?
cpp
// Cách A:
std::string name() const { return name_; }
// Cách B:
const std::string& name() const { return name_; }- [ ] A) Cách A gây undefined behavior
- [x] B) Cách A tạo bản sao không cần thiết, cách B trả về reference tránh copy
- [ ] C) Cách B nhanh hơn vì compiler optimize tốt hơn
- [ ] D) Không khác biệt, chỉ là style
💡 Giải thích: Cách A tạo một
std::stringmới (copy) mỗi lần gọi. Với string ngắn thì không đáng kể, nhưng với string dài hoặc gọi trong vòng lặp thì rất tốn. Cách B trả về reference — zero copy, chỉ truyền địa chỉ. Tuy nhiên, cần cẩn thận: reference trả về sẽ invalid nếu object bị hủy.
Module 2 Checkpoint 🏁
Bài 1/3 của Module 2 hoàn tất! Bạn đã hiểu cách xây dựng class với encapsulation — lớp vỏ bảo vệ đầu tiên cho dữ liệu trong C++.
Tiếp theo: RAII & Constructors — pattern quan trọng nhất trong C++. Nếu encapsulation là "khóa cửa", thì RAII là "hệ thống tự động đóng cửa khi bạn rời khỏi phòng".