Skip to content

🏛️ 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 struct vs class và 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


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ầnVai tròVí dụ
Data membersLưu trữ trạng thái nội bộowner_, balance_
ConstructorKhởi tạo object hợp lệBankAccount(...)
Member functionsHành vi, thao tác trên dữ liệudeposit(), withdraw()
Access specifiersKiểm soát ai được truy cập gìprivate:, public:
const methodsCam kết không thay đổi objectbalance() 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++, structclass gần như giống hệt nhau. Khác biệt duy nhất:

structclass
Default accesspublicprivate
Default inheritancepublicprivate

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 public

Nguyê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()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_case cho hàm/biến, trailing _ cho members
  • C++ Core Guidelines: Tương tự Google style
  • LLVM/Clang: CamelCase cho class, camelCase cho 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 item
  • status_ 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:

  1. total_ luôn được recalculate mỗi khi items thay đổi — không ai "set total" trực tiếp
  2. Status chỉ chuyển theo luồng hợp lệ — không thể nhảy từ Pending sang Delivered
  3. 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()public, gọi thoải mái
  • (B) ❌gpa_private, không truy cập trực tiếp được
  • (C) ❌student_id_protected, chỉ class con mới truy cập được
  • (D) ✅update_gpa()public, có validation bên trong
  • (E) ✅gpa()public const 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ề:

  1. Access modifiers đang ở đâu?
  2. Hàm discount_priceside effect gì?
  3. Có invariant nào bị vi phạm không?
🔍 Phân tích chi tiết

Vấn đề 1: Public data membersname, 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ểmdiscount_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()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 ngay

Nguyê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ĩaGhi nhớ
EncapsulationẨn dữ liệu, kiểm soát truy cập"Mọi thứ private trừ khi có lý do"
class vs structDefault access khác nhaustruct = POD, class = có invariant
private / public / protectedBa tầng kiểm soátprivate → public → protected (ít dùng)
const member functionCam kết không sửa objectGetter luôn là const
Initializer listKhởi tạo trực tiếp, hiệu quảBắt buộc cho const/ref members
NamingTrailing _, không prefix getbalance() thay vì getBalance()
InvariantRàng buộc luôn đúngConstructor + 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ếu const, không gọi được trên const Circle&
  • [ ] C) Nên dùng struct thay vì class
  • [ ] D) radius_ phải là public

💡 Giải thích: Hàm print_area nhận const Circle&, nhưng area() không được đánh dấu const. Compiler không thể đảm bảo area() 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: const members 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::string mớ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".