C++ Pointers: Hướng Dẫn Toàn Diện từ Raw đến Smart

Pointers là tính năng khiến C++ vừa cực kỳ mạnh mẽ, vừa nổi tiếng là khó nhằn. Nó cho bạn quyền truy cập trực tiếp vào memory — khả năng thao tác address, xây dựng các data structures phức tạp, và viết code chạy sát hardware. Nhưng power đi kèm responsibility: dùng sai pointer, bạn sẽ gặp crash, memory leaks, hoặc security vulnerabilities.
Bài này đưa bạn từ raw pointer cơ bản qua smart pointers hiện đại, cover mọi thứ bạn cần để viết C++ code an toàn và hiệu quả.
Bạn Sẽ Học Được
✅ Raw pointer fundamentals: khai báo, dereferencing, và address-of
✅ Pointer arithmetic và cách nó map vào memory layout
✅ Mối quan hệ giữa pointers và arrays
✅ References vs pointers: khi nào dùng cái nào
✅ Dynamic memory với new/delete và tại sao nó nguy hiểm
✅ Smart pointers: unique_ptr, shared_ptr, weak_ptr
✅ RAII và ownership semantics
✅ Các bugs pointer thường gặp và cách tránh
✅ Best practices C++ hiện đại cho memory safety
Raw Pointer Cơ Bản
Pointer là một biến lưu trữ memory address. Chỉ vậy thôi. Mọi pointer, bất kể type, đều giữ một address — một con số xác định vị trí trong memory.
Khai Báo và Khởi Tạo
#include <iostream>
int main() {
int value = 42;
// Ba cách khai báo tương đương
int* p1 = &value; // Ưa thích: * đi với type
int *p2 = &value; // C-style: * đi với biến
int * p3 = &value; // Cũng được, ít phổ biến
// & là toán tử "address-of" (lấy địa chỉ)
std::cout << "value: " << value << '\n'; // 42
std::cout << "address: " << &value << '\n'; // 0x7ffd... (địa chỉ hex nào đó)
std::cout << "p1: " << p1 << '\n'; // cùng địa chỉ
// * là toán tử "dereference" (đi theo địa chỉ)
std::cout << "*p1: " << *p1 << '\n'; // 42
// Sửa đổi bản gốc thông qua pointer
*p1 = 100;
std::cout << "value: " << value << '\n'; // 100
return 0;
}Mental Model
Hãy nghĩ memory như một mảng khổng lồ các bytes. Mỗi byte có một index (address của nó). Pointer đơn giản chỉ lưu một trong những index đó.
Khi bạn viết *p1, bạn đang nói: "Đi đến address lưu trong p1, và đưa cho tôi value ở đó."
Null Pointers
Pointer không trỏ đến gì valid nên được set thành nullptr:
int* ptr = nullptr; // C++ hiện đại (C++11+)
int* old = NULL; // C-style, tránh dùng trong modern code
int* bad = 0; // Chạy được nhưng mơ hồ — 0 là int hay pointer?
// Luôn check trước khi dereference
if (ptr != nullptr) {
std::cout << *ptr << '\n';
} else {
std::cout << "ptr is null\n";
}
// Viết tắt (pointers có tính truthy/falsy)
if (ptr) {
std::cout << *ptr << '\n';
}Rule: Luôn khởi tạo pointers. Pointer chưa khởi tạo chứa garbage address — dereference nó là undefined behavior.
Pointer Arithmetic
Pointer arithmetic là một trong những feature mạnh nhất (và nguy hiểm nhất) của C++. Khi bạn cộng hoặc trừ từ pointer, compiler tự động scale theo size của type mà pointer trỏ đến.
Cách Hoạt Động
#include <iostream>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // Trỏ đến arr[0]
// Cộng 1 di chuyển sizeof(int) bytes, KHÔNG phải 1 byte
std::cout << "*p: " << *p << '\n'; // 10
std::cout << "*(p + 1): " << *(p + 1) << '\n'; // 20
std::cout << "*(p + 2): " << *(p + 2) << '\n'; // 30
// Increment di chuyển đến phần tử tiếp theo
p++;
std::cout << "*p after p++: " << *p << '\n'; // 20
// Trừ pointer cho ra số phần tử, không phải số bytes
int* start = arr;
int* end = arr + 5;
std::cout << "Elements: " << (end - start) << '\n'; // 5
// Toán tử so sánh hoạt động trên pointers
if (start < end) {
std::cout << "start nằm trước end\n";
}
return 0;
}Minh Họa Memory Layout
p + 1 không cộng 1 byte — nó cộng sizeof(int) (thường là 4 bytes). Nên p + 1 di chuyển từ 0x1000 đến 0x1004.
Vùng Nguy Hiểm
int arr[] = {10, 20, 30};
int* p = arr;
int* danger = p + 100; // Compile bình thường! Không kiểm tra giới hạn
// *danger = 42; // Undefined behavior: ghi vào vùng memory ngẫu nhiên
// Đây là nguyên nhân gốc của lỗ hổng buffer overflow
char buffer[10];
char* cp = buffer;
// Ghi quá buffer làm hỏng memory liền kề
// *(cp + 100) = 'X'; // Buffer overflow!Giải pháp hiện đại: Dùng
std::arrayhoặcstd::vectorvới.at()để có bounds-checked access. Chỉ dùng pointer arithmetic cho performance-critical code mà bạn đã verify bounds rồi.
Pointers và Arrays
Trong C++, arrays và pointers có mối quan hệ sâu sắc. Tên array decay (suy biến) thành pointer đến phần tử đầu tiên trong hầu hết context.
Array Decay
#include <iostream>
void printSize(int* arr) {
// sizeof(arr) là size của pointer, KHÔNG phải array!
std::cout << "sizeof trong hàm: " << sizeof(arr) << '\n'; // 8 (trên 64-bit)
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
// sizeof hoạt động đúng trong scope khai báo
std::cout << "sizeof arr: " << sizeof(arr) << '\n'; // 20 (5 * 4 bytes)
// Array decay thành pointer khi truyền vào hàm
printSize(arr); // Mất thông tin size!
// Tên array và pointer đến phần tử đầu tiên hoán đổi được
int* p = arr; // Decay ngầm định
int* p2 = &arr[0]; // Tường minh — cùng kết quả
// Những cách này tương đương
std::cout << arr[2] << '\n'; // 30 — ký hiệu subscript
std::cout << *(arr + 2) << '\n'; // 30 — pointer arithmetic
std::cout << *(p + 2) << '\n'; // 30 — qua pointer
std::cout << p[2] << '\n'; // 30 — subscript trên pointer cũng được!
return 0;
}Tại Sao std::array và std::vector Tốt Hơn
#include <array>
#include <vector>
#include <iostream>
// std::array: size cố định, stack-allocated, biết size của nó
void printArray(const std::array<int, 5>& arr) {
std::cout << "Size: " << arr.size() << '\n'; // 5 — size được giữ nguyên!
for (int val : arr) {
std::cout << val << ' ';
}
std::cout << '\n';
}
// std::vector: dynamic size, heap-allocated, biết size của nó
void printVector(const std::vector<int>& vec) {
std::cout << "Size: " << vec.size() << '\n';
// Truy cập có bounds-checking
try {
std::cout << vec.at(100) << '\n'; // Throw std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "Out of range: " << e.what() << '\n';
}
}
int main() {
std::array<int, 5> arr = {10, 20, 30, 40, 50};
printArray(arr);
std::vector<int> vec = {10, 20, 30, 40, 50};
printVector(vec);
return 0;
}Pointers to Pointers
Pointer có thể trỏ đến pointer khác. Điều này tạo ra nhiều level of indirection.
#include <iostream>
int main() {
int value = 42;
int* ptr = &value; // Pointer to int
int** pptr = &ptr; // Pointer to pointer to int
int*** ppptr = &pptr; // Ba level deep (hiếm gặp trong thực tế)
std::cout << "value: " << value << '\n'; // 42
std::cout << "*ptr: " << *ptr << '\n'; // 42
std::cout << "**pptr: " << **pptr << '\n'; // 42
std::cout << "***ppptr: " << ***ppptr << '\n'; // 42
// Sửa đổi thông qua double pointer
**pptr = 100;
std::cout << "value sau **pptr = 100: " << value << '\n'; // 100
return 0;
}Các use case phổ biến cho pointer-to-pointer:
- Thay đổi pointer bên trong hàm (truyền
int**để thay đổi nơiint*trỏ đến) - Mảng 2D dynamic (mảng các pointers đến mảng)
- C-style APIs trả dữ liệu qua output parameters
Giải pháp hiện đại:
std::vector<std::vector<int>>cho mảng 2D,std::optionalhoặc return values thay vì output parameters.
Function Pointers
Functions cũng sống trong memory, nên bạn có thể có pointers đến chúng:
#include <iostream>
#include <functional>
// Hàm thường
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// Hàm nhận function pointer làm tham số
int apply(int a, int b, int (*operation)(int, int)) {
return operation(a, b);
}
int main() {
// Khai báo function pointer
int (*op)(int, int) = &add; // & là optional cho functions
std::cout << "add: " << op(3, 4) << '\n'; // 7
op = multiply; // Gán lại cho hàm khác
std::cout << "multiply: " << op(3, 4) << '\n'; // 12
// Truyền function pointer làm argument
std::cout << "apply add: " << apply(3, 4, add) << '\n'; // 7
std::cout << "apply multiply: " << apply(3, 4, multiply) << '\n'; // 12
// Giải pháp hiện đại: std::function (linh hoạt hơn)
std::function<int(int, int)> func = add;
std::cout << "std::function: " << func(3, 4) << '\n'; // 7
// Cũng hoạt động với lambdas
func = [](int a, int b) { return a - b; };
std::cout << "lambda: " << func(10, 3) << '\n'; // 7
return 0;
}Type alias giúp function pointers dễ đọc hơn:
// Không có alias — khó đọc
int (*getOperation(char op))(int, int);
// Có alias — gọn hơn nhiều
using BinaryOp = int (*)(int, int);
BinaryOp getOperation(char op);
// Hoặc tốt hơn nữa với std::function
using BinaryOp = std::function<int(int, int)>;const và Pointers
const với pointers có bốn biến thể. Đọc chúng từ phải sang trái để hiểu:
int value = 42;
int other = 99;
// 1. Pointer to non-const: có thể sửa value và pointer
int* p1 = &value;
*p1 = 100; // OK: sửa value
p1 = &other; // OK: thay đổi nơi p1 trỏ đến
// 2. Pointer to const: không sửa được value, có thể đổi pointer
const int* p2 = &value; // Đọc: "p2 là pointer to const int"
// *p2 = 100; // LỖI: value là const
p2 = &other; // OK: bản thân pointer không phải const
// 3. Const pointer to non-const: có thể sửa value, không đổi được pointer
int* const p3 = &value; // Đọc: "p3 là const pointer to int"
*p3 = 100; // OK: value không phải const
// p3 = &other; // LỖI: pointer là const
// 4. Const pointer to const: không sửa được cả hai
const int* const p4 = &value; // Đọc: "p4 là const pointer to const int"
// *p4 = 100; // LỖI: value là const
// p4 = &other; // LỖI: pointer là constQuy tắc đọc từ phải sang trái:
| Khai báo | Đọc là |
|---|---|
int* p | p là pointer to int |
const int* p | p là pointer to const int |
int* const p | p là const pointer to int |
const int* const p | p là const pointer to const int |
Best practice: Dùng const mọi nơi có thể. Nếu hàm không sửa đổi data được trỏ đến, dùng const:
// Tốt: cam kết không sửa đổi string
void print(const std::string* s) {
std::cout << *s << '\n';
}
// Tốt hơn: dùng const reference
void print(const std::string& s) {
std::cout << s << '\n';
}References vs Pointers
References là aliases — tên khác cho biến đã tồn tại. Chúng giống pointers nhưng hành vi khác:
#include <iostream>
int main() {
int value = 42;
// Reference: phải khởi tạo, không thể null, không thể gán lại
int& ref = value;
ref = 100;
std::cout << value << '\n'; // 100 — ref CHÍNH LÀ value
// Pointer: có thể null, có thể gán lại, phải dereference
int* ptr = &value;
*ptr = 200;
std::cout << value << '\n'; // 200
int other = 50;
// Reference không thể gán lại
ref = other; // Gán VALUE của other vào value, KHÔNG rebind ref
std::cout << value << '\n'; // 50 (value thay đổi, ref vẫn alias value)
// Pointer có thể gán lại
ptr = &other; // Giờ ptr trỏ đến other
*ptr = 999;
std::cout << other << '\n'; // 999
std::cout << value << '\n'; // 50 (không đổi)
return 0;
}Khi Nào Dùng Cái Nào
| Tình huống | Dùng Reference | Dùng Pointer |
|---|---|---|
| Tham số hàm (chỉ đọc) | const T& | — |
| Tham số hàm (sửa đổi) | T& | — |
| Tham số optional (có thể null) | — | T* |
| Có thể gán lại (đổi nơi trỏ đến) | — | T* |
| Class member (optional) | — | T* hoặc smart pointer |
| Trả về từ factory | — | unique_ptr<T> |
| Operator overloading | T& (chaining) | — |
Nguyên tắc chung: Ưu tiên references. Dùng pointers khi cần null hoặc reseating.
Các Pattern Tham Số Hàm
#include <string>
#include <iostream>
// Pattern 1: Truy cập chỉ đọc — const reference
void display(const std::string& name) {
std::cout << "Hello, " << name << '\n';
}
// Pattern 2: Sửa đổi bản gốc — non-const reference
void toUpper(std::string& s) {
for (char& c : s) {
c = std::toupper(c);
}
}
// Pattern 3: Tham số optional — pointer
void greet(const std::string& name, const std::string* title = nullptr) {
if (title) {
std::cout << *title << " " << name << '\n';
} else {
std::cout << name << '\n';
}
}
// Pattern 4: Chuyển ownership — smart pointer (xem phần tiếp theo)
int main() {
std::string name = "alice";
display(name); // "Hello, alice"
toUpper(name);
display(name); // "Hello, ALICE"
std::string dr = "Dr.";
greet(name); // "ALICE"
greet(name, &dr); // "Dr. ALICE"
return 0;
}Dynamic Memory: new và delete
new cấp phát memory trên heap (free store) và trả về pointer. delete giải phóng nó. Đây là cách cũ — bạn hiếm khi nên dùng trực tiếp trong modern C++.
Cách Dùng Cơ Bản
#include <iostream>
int main() {
// Cấp phát một int
int* p = new int(42);
std::cout << *p << '\n'; // 42
delete p; // Giải phóng memory
// Cấp phát mảng
int* arr = new int[5]{10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << ' ';
}
std::cout << '\n';
delete[] arr; // Phải dùng delete[] cho mảng!
return 0;
}5 Lỗi Chí Mạng khi Quản Lý Memory Thủ Công
// Lỗi 1: Memory Leak — quên delete
void leak() {
int* p = new int(42);
// Ôi, hàm return mà không delete
// Memory bị leak — mất cho đến khi chương trình kết thúc
}
// Lỗi 2: Double Delete — delete hai lần
void doubleFree() {
int* p = new int(42);
delete p;
// delete p; // UNDEFINED BEHAVIOR! Crash, corruption, hoặc tệ hơn
}
// Lỗi 3: Use After Free — truy cập memory đã delete
void useAfterFree() {
int* p = new int(42);
delete p;
// std::cout << *p; // UNDEFINED BEHAVIOR! Có thể in 42, crash, hoặc bất kỳ thứ gì
}
// Lỗi 4: Mismatched delete — dùng delete thay vì delete[]
void mismatchedDelete() {
int* arr = new int[100];
// delete arr; // SAI! Phải dùng delete[] cho mảng
delete[] arr; // Đúng
}
// Lỗi 5: Dangling Pointer — trỏ đến local đã hết hạn
int* dangling() {
int local = 42;
return &local; // CẢNH BÁO: local chết khi hàm return
// Pointer trả về trỏ đến rác
}Đây là lý do smart pointers tồn tại. Mỗi bug trên đều được ngăn chặn bằng cách dùng
unique_ptrvàshared_ptr.
Smart Pointers: Cách Hiện Đại
Smart pointers là RAII wrappers tự động quản lý heap memory. Chúng được giới thiệu trong C++11 và nên là lựa chọn mặc định cho dynamic allocation.
std::unique_ptr — Sở Hữu Độc Quyền
unique_ptr sở hữu memory độc quyền. Khi unique_ptr bị hủy (ra khỏi scope), memory được giải phóng. Nó không thể copy, chỉ có thể move.
#include <memory>
#include <iostream>
class Database {
public:
Database(const std::string& name) : name_(name) {
std::cout << "Connected to " << name_ << '\n';
}
~Database() {
std::cout << "Disconnected from " << name_ << '\n';
}
void query(const std::string& sql) {
std::cout << "[" << name_ << "] " << sql << '\n';
}
private:
std::string name_;
};
int main() {
// Tạo với make_unique (ưa thích — exception safe)
auto db = std::make_unique<Database>("production");
db->query("SELECT * FROM users");
// Không copy được — ownership là độc quyền
// auto db2 = db; // COMPILE ERROR
// Có thể move — chuyển ownership
auto db2 = std::move(db);
// db giờ là nullptr
if (!db) {
std::cout << "db rỗng sau move\n";
}
db2->query("SELECT * FROM orders");
// db2 bị hủy ở đây — "Disconnected from production" được in
return 0;
}Dùng unique_ptr khi:
- Có một owner duy nhất rõ ràng cho resource
- Bạn implement factory pattern
- Cần polymorphism (base pointer đến derived objects)
- Cái này cover 90%+ nhu cầu dynamic allocation
std::shared_ptr — Sở Hữu Chung
shared_ptr dùng reference counting. Memory được giải phóng khi shared_ptr cuối cùng trỏ đến nó bị hủy.
#include <memory>
#include <iostream>
#include <vector>
class Config {
public:
Config(std::string env) : env_(std::move(env)) {
std::cout << "Config created for " << env_ << '\n';
}
~Config() {
std::cout << "Config destroyed for " << env_ << '\n';
}
const std::string& env() const { return env_; }
private:
std::string env_;
};
class Service {
public:
Service(std::string name, std::shared_ptr<Config> config)
: name_(std::move(name)), config_(std::move(config)) {}
void run() {
std::cout << name_ << " running in " << config_->env() << '\n';
}
private:
std::string name_;
std::shared_ptr<Config> config_;
};
int main() {
auto config = std::make_shared<Config>("production");
std::cout << "ref count: " << config.use_count() << '\n'; // 1
{
Service auth("AuthService", config);
Service api("ApiService", config);
std::cout << "ref count: " << config.use_count() << '\n'; // 3
auth.run();
api.run();
} // auth và api bị hủy, ref count giảm xuống 1
std::cout << "ref count: " << config.use_count() << '\n'; // 1
// Config bị hủy khi config ra khỏi scope
return 0;
}Dùng shared_ptr khi:
- Nhiều phần code thực sự cần shared ownership
- Hệ thống caching nơi objects được reference từ nhiều nơi
- Observer patterns với shared state
Cảnh báo:
shared_ptrcó overhead (reference count, control block). Đừng dùng làm mặc định — hãy thửunique_ptrtrước.
std::weak_ptr — Phá Vỡ Vòng Lặp
weak_ptr là non-owning reference đến object được quản lý bởi shared_ptr. Nó giải quyết vấn đề circular reference.
#include <memory>
#include <iostream>
class Node {
public:
std::string name;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr phá vỡ cycle!
Node(std::string n) : name(std::move(n)) {
std::cout << "Node " << name << " created\n";
}
~Node() {
std::cout << "Node " << name << " destroyed\n";
}
};
int main() {
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
auto c = std::make_shared<Node>("C");
// Forward links: shared_ptr (ownership)
a->next = b;
b->next = c;
// Backward links: weak_ptr (non-owning)
c->prev = b;
b->prev = a;
// Truy cập weak_ptr an toàn
if (auto locked = c->prev.lock()) {
std::cout << "C's prev: " << locked->name << '\n'; // B
}
// Kiểm tra object được reference có còn tồn tại không
std::weak_ptr<Node> observer = a;
if (!observer.expired()) {
auto locked = observer.lock();
std::cout << "Node vẫn còn sống: " << locked->name << '\n';
}
return 0;
// Tất cả nodes bị hủy — không memory leak!
}Nếu không có weak_ptr: Nếu prev là shared_ptr, A→B→C→B sẽ tạo cycle — không node nào bao giờ được giải phóng.
Sơ Đồ Quyết Định Smart Pointer
RAII: Triết Lý Memory Của C++
RAII (Resource Acquisition Is Initialization) là idiom quan trọng nhất trong C++. Ý tưởng: gắn vòng đời resource với vòng đời object.
#include <fstream>
#include <mutex>
#include <memory>
#include <iostream>
// RAII ví dụ 1: File handle
void processFile(const std::string& path) {
std::ifstream file(path); // Mở file trong constructor
if (!file.is_open()) {
std::cerr << "Không mở được " << path << '\n';
return;
}
std::string line;
while (std::getline(file, line)) {
std::cout << line << '\n';
}
// file đóng tự động bởi destructor — kể cả khi exception được throw!
}
// RAII ví dụ 2: Mutex lock
std::mutex mtx;
void safeIncrement(int& counter) {
std::lock_guard<std::mutex> lock(mtx); // Lock trong constructor
counter++;
// Unlock tự động bởi destructor — kể cả khi exception được throw!
}
// RAII ví dụ 3: Custom resource wrapper
class Socket {
public:
Socket(const std::string& host, int port) {
// Giả lập kết nối
fd_ = 42; // Thực tế đây sẽ là socket() + connect()
std::cout << "Connected to " << host << ":" << port << '\n';
}
~Socket() {
if (fd_ >= 0) {
// close(fd_); // Thực tế, đóng socket
std::cout << "Socket closed\n";
}
}
// Ngăn copying (một socket = một connection)
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
// Cho phép moving
Socket(Socket&& other) noexcept : fd_(other.fd_) {
other.fd_ = -1;
}
private:
int fd_ = -1;
};
int main() {
{
Socket conn("localhost", 8080);
// Dùng connection...
} // Socket đóng tự động ở đây
return 0;
}Pattern RAII:
- Acquire resources trong constructor
- Release resources trong destructor
- Compiler đảm bảo destructor chạy khi object rời scope — kể cả trong exceptions
- Smart pointers là RAII wrapper cho heap memory
Các Patterns và Idioms Phổ Biến
Pimpl Pattern (Pointer to Implementation)
Ẩn implementation details sau một pointer. Giảm compile-time dependencies và giữ headers gọn.
// widget.h — public header
#include <memory>
#include <string>
class Widget {
public:
Widget(const std::string& name);
~Widget(); // Phải khai báo — unique_ptr cần complete type để delete
// Move operations
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void doWork();
private:
struct Impl; // Forward declaration — không cần #include!
std::unique_ptr<Impl> pImpl_;
};
// widget.cpp — implementation
#include "widget.h"
#include <iostream>
// Include heavy headers chỉ ở đây, không phải trong .h
struct Widget::Impl {
std::string name;
int counter = 0;
// Có thể include heavy types: database connections, network sockets, v.v.
};
Widget::Widget(const std::string& name) : pImpl_(std::make_unique<Impl>()) {
pImpl_->name = name;
}
Widget::~Widget() = default; // Phải ở .cpp nơi Impl là complete
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::doWork() {
pImpl_->counter++;
std::cout << pImpl_->name << " work #" << pImpl_->counter << '\n';
}Observer Pattern với weak_ptr
#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
class EventEmitter {
public:
using Listener = std::shared_ptr<std::function<void(const std::string&)>>;
void subscribe(Listener listener) {
listeners_.push_back(listener);
}
void emit(const std::string& event) {
// Dọn dẹp listeners đã expired và notify các listeners còn active
listeners_.erase(
std::remove_if(listeners_.begin(), listeners_.end(),
[](const std::weak_ptr<std::function<void(const std::string&)>>& wp) {
return wp.expired();
}),
listeners_.end()
);
for (auto& wp : listeners_) {
if (auto sp = wp.lock()) {
(*sp)(event);
}
}
}
private:
std::vector<std::weak_ptr<std::function<void(const std::string&)>>> listeners_;
};Các Cạm Bẫy Thường Gặp và Cách Tránh
Cạm Bẫy 1: Trả Về Reference Đến Local
// SAI: reference đến local đã bị hủy
const std::string& getName() {
std::string name = "Alice";
return name; // Dangling reference! name bị hủy ở đây
}
// ĐÚNG: return by value (copy elision giúp hiệu quả)
std::string getName() {
std::string name = "Alice";
return name; // Moved/elided — thực tế không copy
}Cạm Bẫy 2: Slicing với Pointers và References
class Base {
public:
virtual void speak() { std::cout << "Base\n"; }
virtual ~Base() = default; // Luôn virtual destructor!
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
};
// SAI: slicing (chỉ copy phần Base)
void sliced(Base b) {
b.speak(); // Luôn in "Base"!
}
// ĐÚNG: dùng reference hoặc pointer cho polymorphism
void polymorphic(Base& b) {
b.speak(); // In "Derived" nếu b thực sự là Derived
}
void polymorphicPtr(Base* b) {
b->speak(); // Cũng hoạt động với pointer
}
// TỐT NHẤT: dùng smart pointer cho ownership
void owned(std::unique_ptr<Base> b) {
b->speak();
}Cạm Bẫy 3: Quên Virtual Destructor
class Base {
public:
// KHÔNG CÓ virtual destructor:
~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
int* data_ = new int[1000];
public:
~Derived() {
delete[] data_;
std::cout << "Derived destroyed\n";
}
};
// Cái này leak memory!
Base* p = new Derived();
delete p; // Chỉ gọi ~Base(), không gọi ~Derived()! data_ bị leak
// FIX: làm destructor virtual
// virtual ~Base() { ... }Cạm Bẫy 4: Share unique_ptr
// SAI: cố share unique_ptr
auto ptr = std::make_unique<Widget>("w1");
// auto copy = ptr; // COMPILE ERROR — không thể copy unique_ptr
// Nếu thực sự cần sharing, nâng cấp lên shared_ptr
auto shared = std::make_shared<Widget>("w1");
auto copy = shared; // OK — ref count = 2Tổng Kết Best Practices Hiện Đại
Những Quy Tắc Vàng
- Không bao giờ dùng
new/deletetrực tiếp — dùngmake_uniquevàmake_shared - Mặc định dùng
unique_ptr— chỉ dùngshared_ptrkhi ownership thực sự shared - Ưu tiên references hơn pointers cho tham số hàm
- Dùng
constmọi nơi có thể —const T&cho read-only,const T* constkhi cần - Luôn khởi tạo pointers —
nullptrnếu chưa có giá trị - Làm destructors
virtualtrong base classes có virtual functions - Tuân theo Rule of Five — nếu bạn define một trong destructor, copy/move constructors, hoặc copy/move assignments, define tất cả năm (hoặc
= deletecái không cần)
Bảng Tóm Tắt Pointer C++ Hiện Đại
| Task | Cách Cũ | Cách Hiện Đại |
|---|---|---|
| Cấp phát single object | new T(args) | std::make_unique<T>(args) |
| Cấp phát shared object | new T(args) | std::make_shared<T>(args) |
| Cấp phát mảng | new T[n] | std::vector<T>(n) |
| Truyền chỉ đọc | const T* | const T& |
| Truyền có sửa đổi | T* | T& |
| Tham số optional | T* (null = vắng mặt) | std::optional<T> (C++17) |
| Null pointer | NULL hoặc 0 | nullptr |
| C-style cast | (int*)ptr | static_cast<int*>(ptr) |
| Duyệt mảng | for (int i = 0; ...) | Range-based for (auto& x : arr) |
| Function pointer | void (*fp)(int) | std::function<void(int)> |
Khi Nào Raw Pointers Vẫn OK
Raw pointers không phải là evil — chúng chỉ là non-owning. Dùng chúng khi:
- Observing không cần ownership (tham số hàm không nhận ownership)
- Giao tiếp với C APIs (phải convert smart pointers với
.get()) - Performance-critical inner loops nơi overhead smart pointer quan trọng
- Implement data structures riêng (linked lists, trees)
Điểm phân biệt then chốt: owning raw pointers là nguy hiểm; non-owning raw pointers thì ổn.
// OK: non-owning raw pointer (không quản lý lifetime)
void process(const Widget* widget) {
if (widget) {
widget->doWork();
}
}
// OK: caller quản lý lifetime với smart pointer
auto widget = std::make_unique<Widget>("w1");
process(widget.get()); // Truyền raw pointer để observeBài Tập Thực Hành
Test hiểu biết của bạn với những bài tập sau:
Bài 1: Tìm Bug
int* createArray(int size) {
int arr[size]; // VLA — không phải standard C++!
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
return arr; // Cái gì sai ở đây?
}Đáp án
arr là mảng local trên stack. Trả về pointer đến nó tạo ra dangling pointer — memory không valid sau khi hàm return. Fix: dùng std::vector<int> và return by value, hoặc std::make_unique<int[]>(size).
Bài 2: Sửa Memory Leak
void processData() {
int* data = new int[1000];
// ... xử lý có thể throw ...
if (data[0] < 0) {
return; // Leak!
}
delete[] data;
}Đáp án
Nếu early return được chạy, delete[] không bao giờ được gọi. Fix: dùng auto data = std::make_unique<int[]>(1000); — destructor chạy bất kể hàm exit kiểu nào.
Bài 3: Chọn Smart Pointer Đúng
Bạn đang xây scene graph cho game. Mỗi node có một parent và nhiều children. Smart pointer nào nên dùng cho parent vs children?
Đáp án
- Children:
std::vector<std::unique_ptr<Node>>— parent sở hữu children - Parent: raw pointer
Node*hoặcstd::weak_ptr<Node>— child không sở hữu parent
Cách này ngăn circular references và làm ownership rõ ràng.
Bài Viết Liên Quan
- C++ Phase 1: Fundamentals - C++ cơ bản bao gồm references và giới thiệu pointer
- C++ Phase 2: OOP trong Modern C++ - Classes, destructors, và nền tảng RAII
- C++ Phase 3: Modern C++ Features - Smart pointers, move semantics, và modern features
- So Sánh Pointer Go và C++ - So sánh pointer hai ngôn ngữ
- C++ Learning Roadmap - Lộ trình học C++ hoàn chỉnh
Kết Luận
Pointers là con dao hai lưỡi của C++. Raw pointers cho bạn quyền kiểm soát trực tiếp memory — thiết yếu cho systems programming, game engines, và embedded systems. Nhưng quyền kiểm soát đó đòi hỏi kỷ luật.
Modern C++ đã tiến hóa để giữ sức mạnh trong khi giảm đáng kể nguy hiểm:
unique_ptrxử lý 90%+ nhu cầu dynamic allocation — zero overhead, tự động cleanupshared_ptrcover các trường hợp còn lại nơi ownership thực sự shared- References thay thế pointers hoàn toàn cho hầu hết tham số hàm
- RAII đảm bảo resources luôn được cleanup, kể cả trong exceptions
Thứ tự ưu tiên rõ ràng: bắt đầu với stack allocation → dùng unique_ptr khi cần heap → dùng shared_ptr chỉ khi ownership thực sự shared → dùng raw pointers chỉ để non-owning observation.
Làm chủ hierarchy này, bạn sẽ viết C++ vừa high-performance vừa memory-safe.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.