This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Ngôn ngữ C++ giới thiệu mô hình lập trình hướng đối tượng (OOP) để giúp quản lý độ phức tạp của các ứng dụng lớn bằng cách gom nhóm cả dữ liệu (Attributes) và hành vi (Methods) vào một khối duy nhất gọi là Class (Lớp). Đặc biệt, C++ sở hữu một tính năng vô cùng độc đáo mà các ngôn ngữ có Garbage Collector như Java hay JS không có: Hàm hủy (Destructor) hoạt động tự động khi đối tượng ra khỏi scope, làm nền tảng cho nguyên lý RAII (Resource Acquisition Is Initialization) và các quy tắc quản lý tài nguyên nghiêm ngặt.
1. Lớp (Class) và Đối tượng (Object)
Trong C++, Class là một khuôn mẫu (Blueprint) do lập trình viên định nghĩa để tạo ra các đối tượng (Objects) cụ thể. Class cung cấp các mức độ kiểm soát truy cập (Access Specifiers) để bảo vệ dữ liệu bên trong:
-
private: Chỉ có các hàm thành viên bên trong Class mới được truy cập trực tiếp. Đây là mặc định của Class nhằm mục đích đóng gói (Encapsulation). public: Có thể truy cập tự do từ bất cứ đâu bên ngoài lớp.-
protected: Cho phép lớp con (Derived Class) kế thừa và truy cập trực tiếp, nhưng bên ngoài thì không.
2. Constructors & Destructors: Tối ưu hóa với Initialization List
- Constructor (Hàm khởi tạo): Trùng tên với lớp, tự động chạy khi đối tượng được tạo lập nhằm gán giá trị mặc định hoặc cấp phát tài nguyên.
-
Destructor (Hàm hủy): Trùng tên lớp nhưng có dấu ngã
~ở trước, tự động chạy khi đối tượng bị tiêu hủy (ra khỏi scope hoặc gọi delete).
Bản chất tối ưu của Constructor Initialization List:
Có hai cách để gán giá trị cho thành viên lớp trong hàm khởi tạo:
// CACH 1: Dung Initialization List (KHUYÊN DÙNG - TỐI ƯU NHẤT)
class Player {
std::string name;
public:
Player(std::string n) : name(n) {} // name khoi tao truc tiep bang 'n'
};
// CACH 2: Gan trong than ham (CHẬM HƠN)
class Player {
std::string name;
public:
Player(std::string n) {
name = n; // name dau tien duoc khoi tao mac dinh truoc, sau do moi chay phep gan
}
};
Ở Cách 2, trình biên dịch buộc phải thực hiện 2 bước: mặc định khởi tạo đối tượng
name thành chuỗi rỗng trước, sau đó mới gọi toán tử gán operator= để ghi đè
giá trị n vào. Trong khi đó, Cách 1 (sử dụng dấu hai chấm
:) sẽ truyền thẳng dữ liệu khởi tạo trực tiếp cho biến, giảm thiểu 50% số lần gọi hàm
khởi tạo phụ, đặc biệt quan trọng với các cấu trúc dữ liệu lớn hoặc phức tạp.
3. Triết lý quản lý tài nguyên RAII & Viết một File Wrapper an toàn
RAII (Resource Acquisition Is Initialization) là triết lý trung tâm của C++: *Cấp phát tài nguyên trong hàm khởi tạo và giải phóng tài nguyên đó trong hàm hủy*. Tài nguyên ở đây có thể là bộ nhớ Heap, file descriptor, socket, hay mutex khóa luồng (thread lock).
Dưới đây là một ví dụ thực tế về việc xây dựng một lớp RAII tự động đóng File để chống rò rỉ tài nguyên hệ thống (File Handle Leak), ngay cả khi chương trình xảy ra sự cố đột ngột:
#include <iostream>
#include <cstdio>
#include <string>
class FileWrapper {
private:
FILE* fileHandle;
public:
// Resource Acquisition in Constructor
FileWrapper(const std::string& filename, const std::string& mode) {
fileHandle = std::fopen(filename.c_str(), mode.c_str());
if (!fileHandle) {
std::cerr << "Khong the mo file: " << filename << std::endl;
} else {
std::cout << "[RAII] Da mo file handle thanh cong." << std::endl;
}
}
// Resource Release in Destructor
~FileWrapper() {
if (fileHandle) {
std::fclose(fileHandle);
std::cout << "[RAII] Tu dong dong file handle va giai phong tai nguyen." << std::endl;
}
}
void write(const std::string& text) {
if (fileHandle) {
std::fputs(text.c_str(), fileHandle);
}
}
};
int main() {
{
FileWrapper logFile("app_log.txt", "w");
logFile.write("Log line 1: Khoi dong Google V8 Engine.\n");
// logFile tu dong dong tai day khi ra khoi scope, khong lo quen close!
}
return 0;
}
4. Quản lý tài nguyên nâng cao: Rule of Three, Rule of Five & Rule of Zero
Khi một lớp tự quản lý tài nguyên thô (như con trỏ Heap cấp phát bằng new), việc gán hoặc
copy đối tượng mặc định của trình biên dịch (Shallow Copy) sẽ dẫn đến lỗi bộ nhớ nghiêm trọng (như
giải phóng vùng nhớ hai lần - Double Free). Do đó, bạn phải nắm rõ các quy tắc vàng:
Quy tắc ba (Rule of Three - chuẩn C++98):
Nếu một lớp cần định nghĩa thủ công bất kỳ một trong ba hàm sau, thì nó bắt buộc phải định nghĩa hai hàm còn lại để đảm bảo an toàn bộ nhớ:
- Destructor (Hàm hủy)
- Copy Constructor (Hàm khởi tạo sao chép) (Thực hiện Deep Copy vùng nhớ Heap mới)
- Copy Assignment Operator (Toán tử gán sao chép)
Quy tắc năm (Rule of Five - chuẩn C++11 trở đi):
Với sự ra đời của cơ chế dịch chuyển Move Semantics, để tối ưu hóa hiệu năng, ta bổ sung thêm 2 hàm dịch chuyển giúp tái sử dụng vùng nhớ mà không cần copy:
- Move Constructor (Hàm khởi tạo dịch chuyển)
- Move Assignment Operator (Toán tử gán dịch chuyển)
Quy tắc không (Rule of Zero):
Đây là triết lý thiết kế hiện đại nhất:
Tránh tự quản lý tài nguyên thô trực tiếp trong các lớp nghiệp vụ. Thay vào đó, hãy
sử dụng các container chuẩn như std::vector, std::string, hoặc các con trỏ
thông minh std::unique_ptr. Các đối tượng này đã tự triển khai Rule of Five bên trong,
giúp lớp của bạn không cần khai báo bất kỳ hàm hủy hay hàm sao chép nào mà vẫn an toàn tuyệt đối.
5. Code thực hành: oop_basics.cpp
Đoạn mã sau mô phỏng kết nối tài nguyên để minh họa trực quan Constructor, Destructor và cơ chế tự giải phóng vùng nhớ RAII:
#include <iostream>
#include <string>
// Lớp giả lập một tài nguyên kết nối (Database/File) để minh họa cơ chế RAII
class ResourceConnection {
private:
std::string resourceName;
public:
// Constructor (Hàm khởi tạo) sử dụng Initializer List
ResourceConnection(std::string name) : resourceName(name) {
std::cout << "[Constructor] Da mo va cap phat tai nguyen: " << resourceName << std::endl;
}
// Destructor (Hàm hủy) - Tự động được gọi khi đối tượng ra khỏi scope
~ResourceConnection() {
std::cout << "[Destructor] Tu dong giai phong tai nguyen: " << resourceName << std::endl;
}
void executeQuery(std::string query) {
std::cout << "Dang thuc thi truy van tren [" << resourceName << "]: " << query << std::endl;
}
};
int main() {
std::cout << "--- Bat dau phan main ---" << std::endl;
{
// Khởi tạo đối tượng trong một khối scope giới hạn
std::cout << "\nBuoc vao scope con:" << std::endl;
ResourceConnection conn("V8_Memory_Pool");
conn.executeQuery("SELECT * FROM JavaScript_AST");
std::cout << "Chuan bi thoat khoi scope con..." << std::endl;
// conn sẽ tự động bị hủy tại đây và destructor được gọi giải phóng bộ nhớ/tài nguyên
}
std::cout << "\nDa thoat khoi scope con." << std::endl;
std::cout << "--- Ket thuc phan main ---" << std::endl;
return 0;
}
6. Tổ chức mã nguồn đa tệp (Multi-file) trong C++: Tách Header (.hpp) và Source (.cpp)
Khi xây dựng dự án C++ thực tế, các lớp (Class) không được viết chung trong file
main.cpp mà được tách biệt thành các module riêng. Cấu trúc chuẩn của một lớp C++ gồm:
-
File tiêu đề (Header file -
.hpphoặc.h): Khai báo cấu trúc của lớp (khai báo thuộc tính và nguyên mẫu phương thức). Đây là bản vẽ thiết kế (Blueprint) của lớp. -
File nguồn (Source file -
.cpp): Chứa định nghĩa chi tiết của các phương thức. Cần `#include` file header tương ứng để kết nối.
Tránh trùng lặp với #pragma once
Trong C++, ngoài cách dùng Header Guards kiểu cổ điển (`#ifndef`), chúng ta có thể sử dụng chỉ thị tiền xử lý hiện đại và gọn gàng hơn:
#pragma once
#include <string>
class ResourceConnection {
private:
std::string resourceName;
public:
ResourceConnection(std::string name);
~ResourceConnection();
void executeQuery(std::string query);
};
Định nghĩa phương thức trong Source File
Khi viết code triển khai trong file .cpp, ta cần sử dụng toán tử phân giải phạm vi
:: để chỉ định phương thức đó thuộc lớp nào:
#include "Resource.hpp"
#include <iostream>
ResourceConnection::ResourceConnection(std::string name) : resourceName(name) {
std::cout << "[Constructor] Da mo: " << resourceName << std::endl;
}
ResourceConnection::~ResourceConnection() {
std::cout << "[Destructor] Da dong: " << resourceName << std::endl;
}
void ResourceConnection::executeQuery(std::string query) {
std::cout << "Query: " << query << std::endl;
}
Phạm vi chia sẻ (extern) và Che giấu chi tiết trong C++
-
Biến toàn cục dùng chung (
extern): Khai báo biến toàn cục ở file header bằngexternvà định nghĩa nó ở đúng một file.cpp. Giúp chia sẻ context (như cấu hình ứng dụng, log engine) xuyên suốt các file. -
Namespace vô danh (Anonymous Namespace): Trong C++, thay vì dùng từ khóa
statictoàn cục để ẩn biến/hàm đối với file khác, người ta khuyến khích bọc chúng trong một namespace không tên. Điều này đảm bảo các biến/hàm đó chỉ hiển thị bên trong file.cpphiện tại:namespace { int localConfigValue = 42; // Chỉ file nguồn hiện tại truy cập được }
Cách biên dịch nhiều file nguồn trong C++
Để biên dịch chương trình multi-file, hãy truyền tất cả các file nguồn .cpp vào trình
biên dịch:
g++ -std=c++17 main.cpp Resource.cpp -o my_app
Câu hỏi ôn tập kiến thức
Tại sao hàm hủy (Destructor) lại đóng vai trò cực kỳ quan trọng trong nguyên lý RAII?
7. Thành viên tĩnh (Static Members) & Phương thức tĩnh (Static Methods)
Trong C++, từ khóa static cho phép bạn tạo ra các biến và phương thức được
chia sẻ chung giữa tất cả đối tượng của lớp thay vì mỗi đối tượng có một bản sao
riêng. Điều này rất hữu ích khi bạn cần theo dõi một giá trị toàn cục ở cấp độ lớp, chẳng hạn như số
lượng đối tượng đã tạo hoặc cấu hình chung cho tất cả instance.
Đặc điểm của thành viên tĩnh:
- Được cấp phát trong vùng nhớ dữ liệu toàn cục (Data Segment), không phải trong stack hay heap của từng đối tượng.
- Khởi tạo một lần duy nhất khi chương trình bắt đầu, không phụ thuộc vào số lượng đối tượng được tạo.
-
Phương thức tĩnh không có quyền truy cập vào
thisvì nó không được liên kết với bất kỳ đối tượng cụ thể nào. - Có thể được gọi mà không cần tạo đối tượng:
MyClass::staticMethod().
#include <iostream>
#include <string>
class Player {
private:
std::string name;
int health;
static int playerCount; // Khai báo biến tĩnh
public:
Player(std::string n, int h) : name(n), health(h) {
playerCount++; // Tăng bộ đếm mỗi khi tạo đối tượng mới
std::cout << "[Player Created] " << name << " | Total: " << playerCount << std::endl;
}
~Player() {
playerCount--;
std::cout << "[Player Destroyed] " << name << " | Total: " << playerCount << std::endl;
}
// Phương thức tĩnh - có thể gọi mà không cần đối tượng
static int getPlayerCount() {
return playerCount;
}
// Phương thức thông thường
void takeDamage(int dmg) {
health -= dmg;
std::cout << name << " took " << dmg << " dmg (HP: " << health << ")" << std::endl;
}
};
// Định nghĩa và khởi tạo biến tĩnh (bắt buộc)
int Player::playerCount = 0;
int main() {
{
Player p1("Alice", 100);
Player p2("Bob", 100);
std::cout << "Active players: " << Player::getPlayerCount() << std::endl;
p1.takeDamage(20);
}
std::cout << "After scope: " << Player::getPlayerCount() << std::endl;
return 0;
}
8. Phương thức const & Tính bất biến (Const-Correctness)
Phương thức const là một cách để
cam kết với trình biên dịch rằng phương thức này sẽ không chỉnh sửa trạng thái của đối
tượng
(không thay đổi bất kỳ thuộc tính thành viên nào). Điều này cực kỳ quan trọng để viết code an toàn và
dễ bảo trì.
Ưu điểm của const member function:
- Trình biên dịch sẽ phát hiện bất kỳ lỗi nào nếu bạn cố gắng sửa đổi dữ liệu thành viên bên trong phương thức.
- Cho phép bạn gọi phương thức đó trên các đối tượng
const. - Cải thiện khả năng đọc code bằng cách rõ ràng thể hiện ý định của phương thức.
#include <iostream>
#include <string>
class BankAccount {
private:
std::string accountHolder;
double balance;
public:
BankAccount(std::string holder, double initial)
: accountHolder(holder), balance(initial) {}
// Phương thức const - không thay đổi đối tượng
double getBalance() const {
return balance;
}
std::string getHolder() const {
return accountHolder;
}
void printInfo() const {
std::cout << "Account: " << accountHolder << " | Balance: $" << balance << std::endl;
// this->balance = 0; // LỖI! Trình biên dịch không cho phép
}
// Phương thức không const - có thể sửa đổi
void deposit(double amount) {
balance += amount;
std::cout << "Deposited: $" << amount << std::endl;
}
};
int main() {
const BankAccount account("John Doe", 5000);
// Chỉ có thể gọi phương thức const
account.printInfo();
std::cout << "Balance: $" << account.getBalance() << std::endl;
// account.deposit(100); // LỖI! Không thể gọi phương thức không-const trên đối tượng const
return 0;
}
9. Nạp chồng toán tử (Operator Overloading) & Tính toán đối tượng
C++ cho phép bạn định nghĩa lại cách hoạt động của các toán tử như +,
==, << cho các lớp tự định nghĩa của bạn. Điều này giúp code trở nên
trực quan và dễ đọc hơn, cho phép bạn viết vector1 + vector2 thay vì
vector1.add(vector2).
Các toán tử có thể nạp chồng: +, -, *,
/, ==, !=, <, >,
<<, >>, ++, --, v.v.
Các toán tử KHÔNG thể nạp chồng: :: (scope resolution),
. (member access), .* (pointer to member), ?: (ternary).
#include <iostream>
#include <cmath>
class Vector2D {
private:
double x, y;
public:
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Nạp chồng toán tử cộng
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Nạp chồng toán tử so sánh bằng
bool operator==(const Vector2D& other) const {
return x == other.x && y == other.y;
}
// Nạp chồng toán tử so sánh nhỏ hơn (dùng để so sánh độ dài)
bool operator<(const Vector2D& other) const {
double len1 = std::sqrt(x * x + y * y);
double len2 = std::sqrt(other.x * other.x + other.y * other.y);
return len1 < len2;
}
// Nạp chồng toán tử xuất (stream insertion)
// Phải là friend function hoặc non-member function
friend std::ostream& operator<<(std::ostream& os, const Vector2D& v);
double getX() const { return x; }
double getY() const { return y; }
};
std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
os << "Vector2D(" << v.x << ", " << v.y << ")";
return os;
}
int main() {
Vector2D v1(3, 4);
Vector2D v2(1, 2);
// Sử dụng operator+
Vector2D v3 = v1 + v2;
std::cout << v1 << " + " << v2 << " = " << v3 << std::endl;
// Sử dụng operator==
std::cout << "v1 == v2: " << (v1 == v2 ? "true" : "false") << std::endl;
// Sử dụng operator<
std::cout << "v1 < v2: " << (v1 < v2 ? "true" : "false") << std::endl;
return 0;
}
Bảng so sánh các mô hình Constructor
Dưới đây là bảng tổng hợp các loại constructor khác nhau và khi nào nên sử dụng chúng:
| Constructor Type | Signature | Use Case | Example |
|---|---|---|---|
| Default | MyClass() |
Tạo đối tượng mà không cần tham số | MyClass obj; |
| Parameterized | MyClass(Type1 a, Type2 b) |
Khởi tạo với dữ liệu cụ thể | MyClass obj(10, "hello"); |
| Copy | MyClass(const MyClass& other) |
Sao chép đối tượng (Deep Copy) | MyClass obj2(obj1); |
| Move | MyClass(MyClass&& other) |
Dịch chuyển tài nguyên (C++11+) | MyClass obj2(std::move(obj1)); |
| Copy Assignment | MyClass& operator=(const MyClass& other) |
Gán đối tượng đã tồn tại | obj1 = obj2; |
| Move Assignment | MyClass& operator=(MyClass&& other) |
Dịch chuyển gán (C++11+) | obj1 = std::move(obj2); |
Tải mã nguồn mẫu bài học
Bạn có thể tải file mã nguồn C++ mẫu hoàn chỉnh của bài học này để tiến hành thực hành trực tiếp trên máy tính cá nhân.
Tải oop_basics.cpp
Comments
Bình luận