Giao diện
🛡️ RAII & Constructors: Tài Nguyên Là Trách Nhiệm Module 2
RAII không phải là một kỹ thuật — nó là triết lý cốt lõi của C++. Mọi tài nguyên (bộ nhớ, file, socket, mutex) đều phải có chủ sở hữu, và chủ sở hữu đó tự động dọn dẹp khi hết nhiệm vụ.
🎯 Mục tiêu
- Hiểu RAII là pattern nền tảng để đảm bảo resource safety trong C++
- Nắm vững vòng đời constructor / destructor — khi nào tạo, khi nào huỷ
- Phân biệt và áp dụng Rule of 0 / 3 / 5 — biết khi nào cần định nghĩa special members
- Sử dụng smart pointers (
unique_ptr,shared_ptr) thay thế rawnew/delete - Hiểu tại sao RAII giúp code exception-safe mà không cần
try/finally
1. RAII Là Gì? — Triết Lý Cốt Lõi
1.1 Định Nghĩa
RAII = Resource Acquisition Is Initialization
Nguyên lý: Việc chiếm giữ tài nguyên xảy ra cùng lúc với khởi tạo đối tượng.
Nói cách khác:
- Constructor → chiếm tài nguyên (mở file, cấp phát bộ nhớ, lock mutex)
- Destructor → giải phóng tài nguyên (đóng file, giải phóng bộ nhớ, unlock mutex)
- Khi object ra khỏi scope → destructor tự động chạy → tài nguyên được dọn dẹp
🎓 Professor Tom's Insight
RAII trả lời câu hỏi muôn thuở: "Ai chịu trách nhiệm giải phóng tài nguyên này?"
Câu trả lời của C++: Type system. Compiler đảm bảo destructor luôn được gọi — bạn không cần nhớ, không cần finally, không cần defer. Nó là tự động.
1.2 So Sánh Với Các Ngôn Ngữ Khác
| Ngôn ngữ | Cơ chế dọn dẹp tài nguyên | Đặc điểm |
|---|---|---|
| C++ | RAII (destructor tự động) | Deterministic — bạn biết chính xác khi nào tài nguyên được giải phóng |
| Java | try-finally / try-with-resources | Phải viết tay hoặc implement AutoCloseable |
| Python | with / context manager | Gần RAII nhất, nhưng chỉ cho một số tài nguyên |
| Go | defer | Chạy cuối function — không phải cuối scope |
| C | Tự gọi free() / fclose() | Quên = memory leak. Không có cơ chế tự động |
Điểm khác biệt quyết định: C++ là ngôn ngữ duy nhất có deterministic destruction — bạn biết chính xác thời điểm destructor chạy, không phụ thuộc garbage collector.
1.3 Tài Nguyên Là Gì?
Tài nguyên không chỉ là bộ nhớ. RAII quản lý mọi thứ cần "trả lại" sau khi dùng:
| Tài nguyên | Acquire (Constructor) | Release (Destructor) |
|---|---|---|
| Heap memory | new / malloc | delete / free |
| File handle | fopen / open | fclose / close |
| Mutex lock | lock() | unlock() |
| Network socket | connect() | close() |
| DB connection | connect() | disconnect() |
| Thread | std::thread(...) | join() / detach() |
2. Vòng Đời Constructor & Destructor
2.1 Stack Object — Tự Động Hoàn Toàn
Scope bắt đầu
│
├─→ FileHandle file("data.txt"); ← Constructor gọi → file mở
│ │
│ ├─→ file.read(); ← Sử dụng tài nguyên
│ │
│ ├─→ process(file); ← Truyền cho function khác
│ │
│ └─→ // Có exception? Không sao!
│
└─→ } ← Scope kết thúc → Destructor gọi → file đóng TỰ ĐỘNG
Kể cả khi exception được throw!cpp
#include <cstdio>
#include <stdexcept>
#include <iostream>
class FileHandle {
FILE* file_;
public:
explicit FileHandle(const char* path)
: file_{std::fopen(path, "r")}
{
if (!file_) {
throw std::runtime_error("Không thể mở file");
}
std::cout << "📂 File opened: " << path << "\n";
}
~FileHandle() {
if (file_) {
std::fclose(file_);
std::cout << "📁 File closed\n";
}
}
// Đọc sẽ học ở section tiếp theo tại sao cần 2 dòng này
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
int main() {
{
FileHandle log("server.log"); // Constructor → mở file
// Làm việc với file...
} // ← Destructor tự động gọi ở đây
std::cout << "File đã được đóng an toàn.\n";
}Output:
📂 File opened: server.log
📁 File closed
File đã được đóng an toàn.2.2 Heap Object — Cần delete (Hoặc Smart Pointer)
cpp
// ❌ NGUY HIỂM: Quản lý thủ công
FileHandle* file = new FileHandle("data.txt");
// ... nếu exception xảy ra ở đây → LEAK!
delete file; // Phải nhớ gọi delete
// ✅ AN TOÀN: Dùng smart pointer
auto file = std::make_unique<FileHandle>("data.txt");
// Khi unique_ptr ra khỏi scope → tự gọi delete → destructor chạy2.3 Thứ Tự Destruction
Destructor chạy theo thứ tự ngược với constructor:
cpp
void demo() {
FileHandle a("first.txt"); // Constructor 1
FileHandle b("second.txt"); // Constructor 2
FileHandle c("third.txt"); // Constructor 3
}
// Destructor thứ tự: c → b → a (LIFO — Last In, First Out)📌 Quy tắc ghi nhớ
LIFO — Đối tượng tạo sau bị huỷ trước. Giống stack: push a, b, c → pop c, b, a. Điều này đảm bảo nếu b phụ thuộc a, thì b bị huỷ trước khi a biến mất.
3. Ví Dụ RAII Hoàn Chỉnh: File Handle
Đây là implementation đầy đủ theo chuẩn modern C++:
cpp
#include <cstdio>
#include <stdexcept>
#include <utility> // std::exchange
class FileHandle {
FILE* file_;
public:
// Constructor: chiếm tài nguyên
explicit FileHandle(const char* path, const char* mode = "r")
: file_{std::fopen(path, mode)}
{
if (!file_) {
throw std::runtime_error(
std::string("Không thể mở file: ") + path
);
}
}
// Destructor: giải phóng tài nguyên
~FileHandle() {
if (file_) {
std::fclose(file_);
}
}
// ❌ Xoá copy — không cho phép 2 object cùng đóng 1 file
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// ✅ Cho phép move — chuyển quyền sở hữu
FileHandle(FileHandle&& other) noexcept
: file_{std::exchange(other.file_, nullptr)}
{}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (file_) std::fclose(file_); // Đóng file cũ
file_ = std::exchange(other.file_, nullptr);
}
return *this;
}
// API sử dụng
[[nodiscard]] FILE* get() const noexcept { return file_; }
explicit operator bool() const noexcept { return file_ != nullptr; }
};⚠️ Tại sao phải xoá copy constructor?
Nếu cho phép copy FileHandle, cả 2 object sẽ giữ cùng một FILE*. Khi object đầu tiên bị huỷ → fclose() được gọi. Khi object thứ hai bị huỷ → fclose() trên pointer đã đóng → Undefined Behavior!
cpp
FileHandle a("data.txt");
FileHandle b = a; // ❌ Nếu cho phép: cả a và b giữ cùng FILE*
// b bị huỷ → fclose(file_)
// a bị huỷ → fclose(file_) LẦN NỮA → 💥 Crash / UB4. Rule of 0 / 3 / 5
4.1 Năm Special Members
C++ có 5 special member functions mà compiler có thể tự sinh:
| # | Special Member | Vai trò | Signature |
|---|---|---|---|
| 1 | Destructor | Dọn dẹp tài nguyên | ~T() |
| 2 | Copy Constructor | Tạo bản sao | T(const T&) |
| 3 | Copy Assignment | Gán bản sao | T& operator=(const T&) |
| 4 | Move Constructor | Chuyển quyền sở hữu | T(T&&) |
| 5 | Move Assignment | Gán chuyển quyền sở hữu | T& operator=(T&&) |
4.2 Rule of 0 — Lựa Chọn Ưu Tiên
Nếu class không quản lý raw resource → ĐỪNG định nghĩa bất kỳ special member nào.
cpp
// ✅ Rule of 0 — compiler tự sinh tất cả, và sinh ĐÚNG
class UserProfile {
std::string name_;
std::string email_;
int age_;
public:
UserProfile(std::string name, std::string email, int age)
: name_{std::move(name)}
, email_{std::move(email)}
, age_{age}
{}
// Không cần destructor, copy, move
// std::string tự biết cách copy/move/destroy
};📌 Rule of 0 là mục tiêu
Hầu hết class trong production code nên follow Rule of 0. Bí quyết: dùng smart pointers và standard containers thay vì raw resources. Khi mỗi member tự biết quản lý mình → class không cần lo.
4.3 Rule of 3 — Legacy C++
Nếu bạn định nghĩa một trong ba: destructor, copy constructor, copy assignment → phải định nghĩa CẢ BA.
cpp
// Rule of 3 (C++98/03 — trước khi có move semantics)
class LegacyBuffer {
int* data_;
size_t size_;
public:
LegacyBuffer(size_t size)
: data_{new int[size]}, size_{size} {}
// Destructor → cần Rule of 3
~LegacyBuffer() { delete[] data_; }
// Copy constructor — deep copy
LegacyBuffer(const LegacyBuffer& other)
: data_{new int[other.size_]}, size_{other.size_}
{
std::copy(other.data_, other.data_ + size_, data_);
}
// Copy assignment — copy-and-swap idiom
LegacyBuffer& operator=(const LegacyBuffer& other) {
if (this != &other) {
LegacyBuffer temp(other);
std::swap(data_, temp.data_);
std::swap(size_, temp.size_);
}
return *this;
}
};4.4 Rule of 5 — Modern C++
Rule of 3 + Move Constructor + Move Assignment = Rule of 5. Thêm move semantics để tránh deep copy không cần thiết.
cpp
class Buffer {
std::unique_ptr<int[]> data_;
size_t size_;
public:
explicit Buffer(size_t size)
: data_{std::make_unique<int[]>(size)}, size_{size} {}
// Destructor — unique_ptr tự xử lý delete
~Buffer() = default;
// Copy — deep copy thủ công vì unique_ptr không copy được
Buffer(const Buffer& other)
: data_{std::make_unique<int[]>(other.size_)}
, size_{other.size_}
{
std::copy(other.data_.get(),
other.data_.get() + size_,
data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
Buffer temp(other);
std::swap(data_, temp.data_);
std::swap(size_, temp.size_);
}
return *this;
}
// Move — mặc định vì unique_ptr hỗ trợ move
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
};4.5 Tóm Tắt Quyết Định
Class có quản lý raw resource không?
│
├── KHÔNG → Rule of 0 ✅ (không định nghĩa gì)
│
└── CÓ → Bạn CẦN copy không?
│
├── KHÔNG → Delete copy, define move + destructor
│ (ví dụ: FileHandle, Socket)
│
└── CÓ → Rule of 5 (define tất cả 5)
(ví dụ: Buffer cần deep copy)5. Smart Pointers: Tự Động Giải Phóng Bộ Nhớ
5.1 std::unique_ptr<T> — Sở Hữu Duy Nhất
unique_ptr là lựa chọn mặc định khi cần cấp phát heap. Một unique_ptr sở hữu duy nhất object — không thể copy, chỉ có thể move.
cpp
#include <memory>
#include <string>
#include <iostream>
struct Order {
int id_;
std::string customer_;
double total_;
Order(int id, std::string customer, double total)
: id_{id}, customer_{std::move(customer)}, total_{total}
{
std::cout << "🛒 Order #" << id_ << " created\n";
}
~Order() {
std::cout << "🗑️ Order #" << id_ << " destroyed\n";
}
};
void process_orders() {
// ✅ make_unique — an toàn, không bao giờ leak
auto order = std::make_unique<Order>(
1001, "khach@email.com", 299.99
);
std::cout << "Processing order #" << order->id_ << "\n";
// Chuyển ownership cho function khác
auto archived = std::move(order);
// order giờ là nullptr — không còn sở hữu gì
if (!order) {
std::cout << "order đã được chuyển ownership\n";
}
} // archived bị huỷ → Order destructor chạy → tự động deleteOutput:
🛒 Order #1001 created
Processing order #1001
order đã được chuyển ownership
🗑️ Order #1001 destroyed📌 Performance: unique_ptr = Zero Overhead
std::unique_ptr có chi phí bằng 0 so với raw pointer. Compiler tối ưu hoá hoàn toàn — assembly code sinh ra giống hệt dùng raw new/delete.
Không có lý do nào để dùng raw new/delete trong application code.
5.2 std::shared_ptr<T> — Sở Hữu Chung
shared_ptr cho phép nhiều owner cùng giữ object. Nội bộ dùng reference counting — object bị huỷ khi owner cuối cùng biến mất.
cpp
#include <memory>
#include <iostream>
#include <vector>
struct Config {
std::string db_host_;
int db_port_;
Config(std::string host, int port)
: db_host_{std::move(host)}, db_port_{port}
{
std::cout << "⚙️ Config loaded\n";
}
~Config() {
std::cout << "⚙️ Config released\n";
}
};
void shared_ownership_demo() {
auto config = std::make_shared<Config>("localhost", 5432);
std::cout << "ref count: " << config.use_count() << "\n"; // 1
{
auto copy = config; // ref count = 2
std::cout << "ref count: " << config.use_count() << "\n"; // 2
auto another = config; // ref count = 3
std::cout << "ref count: " << config.use_count() << "\n"; // 3
}
// copy và another ra khỏi scope → ref count = 1
std::cout << "ref count: " << config.use_count() << "\n"; // 1
} // config ra khỏi scope → ref count = 0 → destructor chạy5.3 Khi Nào Dùng Cái Nào?
| Loại pointer | Ownership | Chi phí | Khi nào dùng |
|---|---|---|---|
unique_ptr | Duy nhất | Zero | Mặc định — hầu hết mọi trường hợp |
shared_ptr | Chia sẻ | Ref counting + control block | Khi thực sự cần nhiều owner (hiếm) |
weak_ptr | Không sở hữu | Kiểm tra expired | Phá vòng circular reference |
Raw pointer T* | Không sở hữu | Zero | Quan sát — KHÔNG BAO GIỜ delete qua nó |
⚠️ Sai lầm phổ biến với shared_ptr
shared_ptr không phải là "unique_ptr nhưng tốt hơn". Nó có chi phí:
- Thêm control block (16-32 bytes) cho mỗi object
- Atomic reference counting → overhead trên multi-thread
- Dễ gây circular reference → memory leak
Quy tắc: Bắt đầu bằng unique_ptr. Chỉ chuyển sang shared_ptr khi bạn có thể giải thích rõ TẠI SAO cần shared ownership.
6. RAII Không Chỉ Là Bộ Nhớ
6.1 Mutex Lock — std::lock_guard
cpp
#include <mutex>
#include <vector>
class ThreadSafeCounter {
mutable std::mutex mutex_;
int count_{0};
public:
void increment() {
std::lock_guard<std::mutex> lock(mutex_); // Lock
++count_;
} // ← lock_guard destructor → unlock TỰ ĐỘNG
[[nodiscard]] int get() const {
std::lock_guard<std::mutex> lock(mutex_);
return count_;
} // ← unlock tự động, kể cả khi có exception
};6.2 File I/O — std::fstream
cpp
#include <fstream>
#include <string>
void write_log(const std::string& message) {
std::ofstream file("app.log", std::ios::app); // Mở file
if (file.is_open()) {
file << message << "\n";
}
} // ← fstream destructor → đóng file TỰ ĐỘNG6.3 Timer — Đo Thời Gian Thực Thi
cpp
#include <chrono>
#include <iostream>
#include <string>
class ScopedTimer {
std::string label_;
std::chrono::high_resolution_clock::time_point start_;
public:
explicit ScopedTimer(std::string label)
: label_{std::move(label)}
, start_{std::chrono::high_resolution_clock::now()}
{}
~ScopedTimer() {
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<
std::chrono::microseconds>(end - start_);
std::cout << label_ << ": " << duration.count() << " μs\n";
}
};
void expensive_operation() {
ScopedTimer timer("expensive_operation"); // Bắt đầu đo
// ... xử lý nặng ...
} // ← Destructor → in thời gian thực thi📌 Nhận diện pattern RAII
Mỗi khi bạn thấy một cặp acquire/release (mở/đóng, lock/unlock, start/stop), đó là lúc cần RAII wrapper. Tạo class mà constructor = acquire, destructor = release.
7. Exception Safety — Tại Sao RAII Quan Trọng
7.1 Vấn Đề Với Raw new/delete
cpp
// ❌ KHÔNG AN TOÀN — exception sẽ gây leak
void process_unsafe() {
int* data = new int[1000];
std::string* name = new std::string("hello");
do_work(data); // ← Nếu throw exception ở đây?
more_work(name); // ← Hoặc ở đây?
delete[] data; // KHÔNG BAO GIỜ chạy tới đây nếu exception!
delete name; // LEAK!
}7.2 RAII Giải Quyết Hoàn Toàn
cpp
// ✅ AN TOÀN — exception hay không, destructor vẫn chạy
void process_safe() {
auto data = std::make_unique<int[]>(1000);
auto name = std::make_unique<std::string>("hello");
do_work(data.get()); // Exception? unique_ptr vẫn cleanup
more_work(name.get()); // Exception? vẫn cleanup
// Không cần delete — unique_ptr tự xử lý
}7.3 Stack Unwinding — Cơ Chế Đằng Sau
Khi exception được throw, C++ thực hiện stack unwinding:
1. Exception thrown tại dòng X
2. C++ runtime "cuộn ngược" stack
3. Mỗi object trên stack → destructor được gọi
4. Tài nguyên được giải phóng theo thứ tự LIFO
5. Exception tiếp tục propagate lên cho đến khi gặp catchcpp
void stack_unwinding_demo() {
FileHandle a("a.txt"); // Constructor
FileHandle b("b.txt"); // Constructor
throw std::runtime_error("Lỗi!");
// Code dưới đây KHÔNG chạy
// NHƯNG destructor của b rồi a VẪN chạy!
}❌ Anti-Pattern: Raw new/delete Trong Application Code
cpp
// ❌ MỖI dòng new không có RAII wrapper = một memory leak tiềm ẩn
Widget* w = new Widget();
// 47 dòng code ở giữa...
// Exception? Quên delete? Lệnh return sớm?
delete w; // Hy vọng chạy tới đây... hy vọng thôi 🙏Quy tắc production: Nếu bạn viết new mà không wrap ngay trong smart pointer, code review sẽ reject. Không có ngoại lệ.
8. Business Example: Database Connection Pool
Một ví dụ thực tế từ production — quản lý database connection bằng RAII:
cpp
#include <memory>
#include <queue>
#include <mutex>
#include <stdexcept>
#include <iostream>
// Giả lập database connection
class DbConnection {
int id_;
bool connected_{true};
public:
explicit DbConnection(int id) : id_{id} {
std::cout << "🔌 Connection #" << id_ << " established\n";
}
~DbConnection() {
if (connected_) {
std::cout << "🔌 Connection #" << id_ << " closed\n";
}
}
void execute(const std::string& query) {
std::cout << " [Conn #" << id_ << "] " << query << "\n";
}
[[nodiscard]] int id() const { return id_; }
DbConnection(const DbConnection&) = delete;
DbConnection& operator=(const DbConnection&) = delete;
DbConnection(DbConnection&&) = default;
DbConnection& operator=(DbConnection&&) = default;
};
// Connection Pool
class ConnectionPool {
std::queue<std::unique_ptr<DbConnection>> available_;
std::mutex mutex_;
int next_id_{1};
public:
explicit ConnectionPool(int initial_size) {
for (int i = 0; i < initial_size; ++i) {
available_.push(
std::make_unique<DbConnection>(next_id_++)
);
}
}
// Lấy connection từ pool
[[nodiscard]] std::unique_ptr<DbConnection> acquire() {
std::lock_guard<std::mutex> lock(mutex_);
if (available_.empty()) {
return std::make_unique<DbConnection>(next_id_++);
}
auto conn = std::move(available_.front());
available_.pop();
return conn;
}
// Trả connection về pool
void release(std::unique_ptr<DbConnection> conn) {
std::lock_guard<std::mutex> lock(mutex_);
available_.push(std::move(conn));
}
};
// ✅ RAII wrapper — tự động trả connection khi ra khỏi scope
class ScopedConnection {
ConnectionPool& pool_;
std::unique_ptr<DbConnection> conn_;
public:
explicit ScopedConnection(ConnectionPool& pool)
: pool_{pool}
, conn_{pool.acquire()}
{}
~ScopedConnection() {
if (conn_) {
pool_.release(std::move(conn_));
}
}
DbConnection* operator->() { return conn_.get(); }
DbConnection& operator*() { return *conn_; }
ScopedConnection(const ScopedConnection&) = delete;
ScopedConnection& operator=(const ScopedConnection&) = delete;
};Sử dụng:
cpp
void handle_request(ConnectionPool& pool) {
ScopedConnection conn(pool); // Acquire từ pool
conn->execute("SELECT * FROM users");
conn->execute("UPDATE stats SET hits = hits + 1");
// Exception? Không sao — destructor trả connection về pool
} // ← ScopedConnection destructor → connection trả về pool🎓 Đây là production pattern thực tế
Pattern này (RAII wrapper quanh connection pool) được sử dụng trong mọi C++ backend lớn: game server, trading system, web framework. Nó đảm bảo connection không bao giờ bị leak — kể cả khi exception xảy ra.
9. Bài Tập Nhanh
⚡ Fast Exercise: Xác Định Special Members
Cho class sau quản lý raw socket:
cpp
class Socket {
int fd_; // File descriptor
public:
Socket(const char* host, int port); // Mở connection
// ... thiếu gì?
};Câu hỏi: Bạn cần định nghĩa những special members nào? Tại sao?
Đáp án:
Vì Socket quản lý raw resource (fd_), bạn cần:
- Destructor —
close(fd_) - Delete copy constructor — không cho 2 Socket cùng đóng 1 fd
- Delete copy assignment — tương tự
- Move constructor — chuyển ownership fd
- Move assignment — chuyển ownership fd
cpp
class Socket {
int fd_;
public:
Socket(const char* host, int port);
~Socket() { if (fd_ >= 0) close(fd_); }
// Xoá copy
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
// Cho phép move
Socket(Socket&& other) noexcept
: fd_{std::exchange(other.fd_, -1)} {}
Socket& operator=(Socket&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
};Hoặc tốt hơn — Rule of 0: Dùng smart pointer với custom deleter:
cpp
class Socket {
struct FdCloser {
void operator()(int* fd) const {
if (fd && *fd >= 0) close(*fd);
delete fd;
}
};
std::unique_ptr<int, FdCloser> fd_;
public:
Socket(const char* host, int port)
: fd_{new int{connect_to(host, port)}} {}
// Rule of 0 — unique_ptr tự lo copy/move/destroy!
};10. Spot The Bug
🐛 Spot-the-Bug
Tìm tất cả vấn đề trong đoạn code sau (ít nhất 4 lỗi):
cpp
class ResourceManager {
int* data;
FILE* log;
std::string name;
public:
ResourceManager(int size, const char* logFile) {
data = new int[size];
log = fopen(logFile, "w");
}
void process() {
fprintf(log, "Processing %s\n", name.c_str());
// ... heavy work ...
}
~ResourceManager() {
delete data;
fclose(log);
}
};
void use() {
ResourceManager a(100, "app.log");
ResourceManager b = a; // Copy
b.process();
}💡 Gợi ý
Nghĩ về: copy semantics, array delete, null checking, exception safety trong constructor...
✅ Đáp án chi tiết
Lỗi 1: delete data thay vì delete[] datadata được cấp phát bằng new int[size] → phải dùng delete[]. Dùng delete = Undefined Behavior.
Lỗi 2: Không kiểm tra fopen trả về nullptr Nếu file không mở được, log = nullptr → fprintf(nullptr, ...) = crash.
Lỗi 3: Không xoá/định nghĩa copy constructor & copy assignmentResourceManager b = a → copy shallow → cả a và b giữ cùng data và log. Khi b bị huỷ → delete[] và fclose. Khi a bị huỷ → double free + double fclose = 💥
Lỗi 4: Constructor không exception-safe Nếu fopen thành công nhưng code sau throw → data đã new nhưng destructor chưa chắc chạy. Nên dùng member initializer list + RAII wrapper.
Lỗi 5: Destructor không kiểm tra nullptr Nếu log = nullptr (fopen thất bại), fclose(nullptr) = UB.
Phiên bản sửa:
cpp
class ResourceManager {
std::unique_ptr<int[]> data_;
std::unique_ptr<FILE, decltype(&fclose)> log_;
std::string name_;
public:
ResourceManager(int size, const char* logFile)
: data_{std::make_unique<int[]>(size)}
, log_{fopen(logFile, "w"), &fclose}
, name_{}
{
if (!log_) {
throw std::runtime_error("Cannot open log file");
}
}
// Rule of 0! unique_ptr tự lo tất cả.
};11. Scenario: Hệ Thống Log Production
🎬 Scenario: RAII Logger cho Microservice
Bạn đang xây dựng hệ thống logging cho microservice. Yêu cầu:
- Mở file log khi service khởi động
- Ghi log thread-safe (nhiều request đồng thời)
- Tuyệt đối không được quên đóng file — kể cả khi crash
Thiết kế RAII:
cpp
#include <fstream>
#include <mutex>
#include <string>
#include <chrono>
#include <iomanip>
class Logger {
std::ofstream file_;
std::mutex mutex_;
static std::string timestamp() {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::ostringstream ss;
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
return ss.str();
}
public:
explicit Logger(const std::string& path)
: file_{path, std::ios::app}
{
if (!file_.is_open()) {
throw std::runtime_error("Cannot open log: " + path);
}
log("INFO", "Logger initialized");
}
~Logger() {
log("INFO", "Logger shutting down");
// fstream destructor sẽ đóng file tự động
}
void log(const std::string& level, const std::string& message) {
std::lock_guard<std::mutex> lock(mutex_);
file_ << "[" << timestamp() << "] "
<< "[" << level << "] "
<< message << "\n";
file_.flush();
}
// Rule of 0: fstream + mutex không cần custom special members
// Nhưng delete copy vì mutex không copyable
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
// Sử dụng — RAII đảm bảo log file luôn được đóng
int main() {
Logger logger("service.log");
logger.log("INFO", "Service started on port 8080");
logger.log("DEBUG", "Processing request #1");
logger.log("ERROR", "Database timeout after 5s");
// Service shutdown → Logger destructor → file đóng an toàn
}Điểm quan trọng: Constructor mở file, destructor đóng file. Lock guard bảo vệ thread safety. Tất cả đều RAII — không có resource nào bị "quên".
12. Tổng Kết
| Khái niệm | Ý nghĩa | Ghi nhớ |
|---|---|---|
| RAII | Constructor = acquire, Destructor = release | Type system quản lý tài nguyên |
| Rule of 0 | Không define special members | Dùng smart pointers → compiler lo |
| Rule of 5 | Define cả 5 special members | Khi quản lý raw resource |
unique_ptr | Sở hữu duy nhất, zero overhead | Lựa chọn mặc định |
shared_ptr | Sở hữu chia sẻ, ref counting | Chỉ khi thực sự cần shared ownership |
| Stack unwinding | Destructor chạy khi exception | RAII = exception-safe tự động |
Checklist Trước Khi Code
- [ ] Mỗi raw resource có RAII wrapper?
- [ ] Dùng
unique_ptrthay vì rawnew/delete? - [ ] Class follow Rule of 0 hay Rule of 5?
- [ ] Copy semantics đúng (delete nếu non-copyable)?
- [ ] Move semantics có
noexcept?
➡️ Tiếp Theo
🧠 Quiz
Câu 1: RAII nghĩa là gì?
- [ ] A) Reclaim All Allocated Items — Thu hồi tất cả item đã cấp phát
- [x] B) Resource Acquisition Is Initialization — Chiếm tài nguyên = Khởi tạo
- [ ] C) Runtime Automatic Interface Integration — Tích hợp interface tự động
- [ ] D) Reference And Immutable Inheritance — Tham chiếu và kế thừa bất biến
💡 Giải thích: RAII = Resource Acquisition Is Initialization. Tài nguyên được chiếm trong constructor và giải phóng trong destructor. Đây là triết lý cốt lõi của C++ resource management.
Câu 2: Khi nào nên dùng Rule of 5 thay vì Rule of 0?
- [ ] A) Luôn luôn — Rule of 5 an toàn hơn
- [ ] B) Khi class có nhiều hơn 3 member variables
- [x] C) Khi class quản lý raw resource mà không dùng smart pointer
- [ ] D) Khi class cần hỗ trợ inheritance
💡 Giải thích: Rule of 5 cần thiết khi class trực tiếp quản lý raw resource (raw pointer, file descriptor, socket). Nếu dùng smart pointers thay vì raw resource → Rule of 0 là đủ. Luôn ưu tiên Rule of 0.
Câu 3: std::unique_ptr so với raw pointer có chi phí runtime bao nhiêu?
- [x] A) Zero — compiler tối ưu hoàn toàn
- [ ] B) Thêm 8 bytes cho metadata
- [ ] C) Thêm reference counting overhead
- [ ] D) Chậm hơn ~10% do destructor overhead
💡 Giải thích:
unique_ptrlà zero-overhead abstraction. Compiler tối ưu hoá hoàn toàn — assembly code sinh ra giống hệt raw pointer + manual delete. Không có lý do nào để dùng rawnew/delete.
Câu 4: Đoạn code sau có vấn đề gì?
cpp
class Handle {
int* ptr_;
public:
Handle(int val) : ptr_{new int{val}} {}
~Handle() { delete ptr_; }
};
void test() {
Handle a(42);
Handle b = a;
}- [ ] A) Không có vấn đề — code hoạt động bình thường
- [ ] B) Memory leak — ptr_ không được delete
- [x] C) Double free — cả a và b cùng delete một pointer
- [ ] D) Dangling pointer — ptr_ trỏ tới stack memory
💡 Giải thích:
Handle b = adùng compiler-generated copy constructor → shallow copy → cảa.ptr_vàb.ptr_trỏ tới cùng địa chỉ. Khibbị huỷ →delete ptr_. Khiabị huỷ →delete ptr_lần nữa = double free = Undefined Behavior. Fix: Delete copy hoặc implement deep copy (Rule of 3/5).
Câu 5: Khi nào nên dùng shared_ptr thay vì unique_ptr?
- [ ] A) Khi object lớn và cần tối ưu bộ nhớ
- [ ] B) Luôn luôn — shared_ptr an toàn hơn unique_ptr
- [ ] C) Khi cần truyền pointer vào function
- [x] D) Khi nhiều owner thực sự cần giữ object sống
💡 Giải thích:
shared_ptrchỉ nên dùng khi có shared ownership thực sự — nhiều nơi cần giữ object sống và không ai biết ai hết sở hữu cuối cùng. Đây là trường hợp hiếm. Mặc định luôn dùngunique_ptr— nó có zero overhead và ownership rõ ràng hơn.