Skip to content

🎯 Pointers & References: Nắm Vững Con Trỏ Module 1

"A pointer is just a number — the address of a byte in memory." — Bjarne Stroustrup

Bạn đã bao giờ nghe ai đó nói "con trỏ C++ khó lắm" chưa? Thực ra, con trỏ không khó — nó chỉ khác so với những gì bạn quen thuộc. Bài này sẽ xây dựng mental model vững chắc để bạn không bao giờ sợ pointer nữa.

🎯 Mục tiêu

Sau bài học này, bạn sẽ:

  • Hiểu con trỏ thực sự là gì (địa chỉ bộ nhớ — không hơn, không kém)
  • Phân biệt rõ pointer vs reference và biết khi nào dùng cái nào
  • Sử dụng nullptr đúng cách và hiểu tại sao NULL bị deprecated
  • Nhận diện dangling pointer — kẻ giết người thầm lặng trong C++
  • Áp dụng const correctness với pointer
  • Biết khi nào không nên dùng raw pointer (smart pointer preview)

📍 Bạn Đang Ở Đây

Phase 1 — Module 1: Nền Tảng C++
┌──────────────────────────────────────────────────┐
│ 01 Memory Mindset  ✅                            │
│ 02 Pointers & References  ◀── BẠN ĐANG Ở ĐÂY   │
│ 03 Vector First                                  │
│ 04 Move Semantics                                │
│ 05 RAII & Smart Pointers                         │
└──────────────────────────────────────────────────┘

1. Mental Model: "Pointer = Địa Chỉ Nhà" 🏠

Hãy tưởng tượng bạn có một mảnh giấy ghi địa chỉ nhà bạn: "123 Nguyễn Huệ, Q1".

  • Mảnh giấy = con trỏ (pointer)
  • Địa chỉ trên giấy = giá trị con trỏ (memory address)
  • Ngôi nhà thực tế = dữ liệu mà pointer trỏ tới

Mảnh giấy KHÔNG phải ngôi nhà. Bạn có thể:

  • Viết địa chỉ khác lên giấy → reseat pointer
  • Xé bỏ mảnh giấy → delete pointer (nhà vẫn còn)
  • Nhà bị phá → dangling pointer 💀 (giấy vẫn ghi địa chỉ cũ)
  • Giấy trắng, chưa ghi gì → null pointer

Minh Họa Bộ Nhớ

Stack Memory:
┌────────────────┬────────────┐
│  Tên biến      │  Giá trị   │  Địa chỉ
├────────────────┼────────────┤
│  value         │     42     │  0x1000
├────────────────┼────────────┤
│  ptr           │   0x1000   │  0x1008
└────────────────┴────────────┘

  ptr ─────────────→ value
  (mảnh giấy)       (ngôi nhà)

  *ptr  = 42          ← đọc giá trị ngôi nhà
  &value = 0x1000     ← lấy địa chỉ ngôi nhà

💡 Kích thước con trỏ

Trên hệ thống 64-bit, mọi con trỏ đều có kích thước 8 byte — bất kể nó trỏ tới int (4B), char (1B) hay struct (hàng trăm byte). Vì con trỏ chỉ lưu địa chỉ, và địa chỉ trên 64-bit luôn cần 8 byte.


2. Pointer Cơ Bản

2.1 Khai Báo Con Trỏ

cpp
// Ba cách viết — HOÀN TOÀN giống nhau:
int* ptr1;   // ← Phong cách C++ (khuyến khích)
int *ptr2;   // ← Phong cách C truyền thống
int * ptr3;  // ← Hợp lệ nhưng ít dùng

⚠️ Bẫy khai báo nhiều biến

cpp
int* a, b;   // ❌ a là int*, nhưng b chỉ là int!
int *a, *b;  // ✅ Cả hai đều là int*

// Tốt nhất — mỗi dòng một biến:
int* a;
int* b;

Dấu * gắn với tên biến, không gắn với kiểu. Đây là bẫy kinh điển từ thời C.

2.2 Hai Toán Tử Quan Trọng Nhất

Toán tửTênÝ nghĩaVí dụ
&Address-ofLấy địa chỉ của biến&value0x1000
*DereferenceTruy cập giá trị tại địa chỉ*ptr42
cpp
#include <iostream>

int main() {
    int value = 42;
    int* ptr = &value;    // ptr lưu địa chỉ của value

    std::cout << value  << '\n';  // 42
    std::cout << &value << '\n';  // 0x7ffd... (địa chỉ)
    std::cout << ptr    << '\n';  // 0x7ffd... (cùng địa chỉ)
    std::cout << *ptr   << '\n';  // 42 (dereference)

    // Thay đổi qua pointer:
    *ptr = 99;
    std::cout << value << '\n';   // 99 — value đã thay đổi!
}

Luồng dữ liệu:

  &value ──→ ptr     (gán địa chỉ)
  *ptr   ──→ value   (truy cập ngược lại)
  *ptr = 99 ──→ value bây giờ = 99

2.3 nullptr — Luôn Khởi Tạo Con Trỏ

cpp
// ❌ SAI — pointer chưa khởi tạo (chứa rác)
int* dangerous;         // trỏ tới đâu? KHÔNG AI BIẾT!
*dangerous = 10;        // 💥 Undefined Behavior

// ✅ ĐÚNG — khởi tạo ngay khi khai báo
int* safe = nullptr;    // rõ ràng: "chưa trỏ tới đâu cả"

// Kiểm tra trước khi dùng:
if (safe != nullptr) {
    *safe = 10;         // an toàn
}

// C++17 — viết gọn hơn:
if (safe) {             // nullptr tự convert thành false
    *safe = 10;
}

🔥 Production Anti-Pattern: Dùng NULL hoặc 0

cpp
// ❌ Code cũ (C-style) — ĐỪNG viết thế này!
int* p1 = NULL;    // NULL là macro, có thể gây ambiguous overload
int* p2 = 0;       // 0 là integer literal, dễ nhầm lẫn

void foo(int n);
void foo(int* p);
foo(NULL);          // 💥 Gọi foo(int) hay foo(int*)? Ambiguous!

// ✅ Modern C++ — luôn dùng nullptr
int* p3 = nullptr;  // nullptr là kiểu std::nullptr_t, rõ ràng
foo(nullptr);        // ✅ Luôn gọi foo(int*), không ambiguous

nullptr được giới thiệu trong C++11 chính xác để giải quyết vấn đề này. Trong code mới, không có lý do gì để dùng NULL hay 0.


3. References: "Bí Danh An Toàn" 🏷️

Reference (tham chiếu) là một cái tên khác cho biến đã tồn tại — giống như bạn có tên khai sinh và biệt danh, nhưng cả hai đều chỉ cùng một người.

3.1 Cú Pháp Cơ Bản

cpp
int value = 42;
int& ref = value;     // ref là "bí danh" của value

std::cout << ref;     // 42 — KHÔNG cần dereference
ref = 99;
std::cout << value;   // 99 — value thay đổi theo!

std::cout << &ref;    // cùng địa chỉ với &value
std::cout << &value;  // chứng minh: ref IS value

3.2 Ba Quy Tắc Sắt Đá

cpp
// Quy tắc 1: PHẢI khởi tạo ngay khi khai báo
int& ref;              // ❌ Lỗi compile — reference phải có "chủ"

// Quy tắc 2: KHÔNG THỂ reseat (đổi sang biến khác)
int a = 10, b = 20;
int& ref = a;
ref = b;               // ⚠️ KHÔNG phải reseat! Gán giá trị b cho a
                        // Bây giờ a = 20, ref vẫn trỏ tới a

// Quy tắc 3: KHÔNG có null reference
int& ref = nullptr;    // ❌ Lỗi compile — reference không thể null

3.3 Minh Họa Bộ Nhớ

Stack Memory:
┌────────────────┬────────────┐
│  value         │     42     │  addr: 0x1000
├────────────────┼────────────┤
│  ref           │  (= value) │  addr: 0x1000  ← CÙNG ĐỊA CHỈ!
└────────────────┴────────────┘

  ref không chiếm bộ nhớ riêng (về mặt logic)
  Nó là tên khác cho cùng ô nhớ 0x1000

🎮 Scenario: Khi Nào Dùng Reference?

Bạn viết hàm tính tổng vector có 1 triệu phần tử:

cpp
// ❌ Copy cả vector — chậm, tốn RAM:
int sum(std::vector<int> v) { /* ... */ }

// ✅ Dùng const reference — zero copy:
int sum(const std::vector<int>& v) {
    int total = 0;
    for (int x : v) total += x;
    return total;
}

const& là pattern phổ biến nhất trong C++ — truyền tham số không copy, không sửa đổi.


4. Pointer vs Reference: Bảng So Sánh Toàn Diện

Đặc điểmPointer (int*)Reference (int&)
Có thể nullnullptr❌ Luôn phải valid
Có thể reseatptr = &other❌ Gắn 1 lần duy nhất
Cần dereference*ptr❌ Tự động
Pointer arithmeticptr++, ptr + n❌ Không hỗ trợ
Khai báo không init✅ (nhưng nguy hiểm)❌ Lỗi compile
Kích thước8 byte (64-bit)0 byte (logic)*
Cú phápPhức tạp hơnSạch hơn
Khi nào dùngOptional, ownership, arraysParams, aliases

* Về mặt implementation, compiler thường triển khai reference bằng pointer. Nhưng ở level abstraction, reference không "chiếm" bộ nhớ riêng.

Quy Tắc Chọn Lựa Nhanh

Bạn cần truyền tham số vào hàm?
├─ Không cần sửa đổi?
│  └─ ✅ const T&  (phổ biến nhất)
├─ Cần sửa đổi?
│  └─ ✅ T&
├─ Giá trị có thể "không tồn tại" (optional)?
│  └─ ✅ T* hoặc std::optional<T>&
└─ Cần chuyển ownership?
   └─ ✅ std::unique_ptr<T> (xem Bài 05)

💡 Quy tắc ngón tay cái

"Dùng reference khi có thể, pointer khi cần thiết."

Trong modern C++ (C++17/20), bạn sẽ dùng reference khoảng 80% trường hợp và raw pointer chỉ khoảng 5% (phần còn lại là smart pointer).


5. Dangling Pointer — Kẻ Giết Người Thầm Lặng 💀

Dangling pointer là pointer trỏ tới vùng nhớ đã bị giải phóng. Giống như bạn cầm mảnh giấy ghi địa chỉ nhà, nhưng nhà đã bị phá — bạn tới đó thì chỉ thấy đống đổ nát (hoặc tệ hơn: nhà người khác).

5.1 Trường Hợp 1: Trả Về Địa Chỉ Biến Cục Bộ

cpp
// ❌ BUG — Classic dangling pointer
int* createValue() {
    int local = 42;       // biến cục bộ — sống trên stack
    return &local;        // trả về địa chỉ stack frame sắp bị hủy
}   // ← local bị hủy ở đây!

int main() {
    int* ptr = createValue();
    std::cout << *ptr;    // 💥 Undefined Behavior!
    // Có thể in 42, có thể in rác, có thể crash
}

Minh họa:

Khi gọi createValue():          Sau khi return:
┌──────────────────┐            ┌──────────────────┐
│ createValue()    │            │  ??? (đã giải    │
│ local = 42       │ 0x2000    │  phóng)          │ 0x2000
└──────────────────┘            └──────────────────┘

  ptr sẽ trỏ tới 0x2000 ──────────────┘
  Nhưng 0x2000 không còn valid!

✅ Cách sửa:

cpp
// Cách 1: Trả về giá trị (copy/move — compiler tối ưu)
int createValue() {
    int local = 42;
    return local;       // ✅ Return by value — an toàn
}

// Cách 2: Dùng smart pointer nếu cần heap allocation
std::unique_ptr<int> createValue() {
    return std::make_unique<int>(42);  // ✅ Ownership rõ ràng
}

5.2 Trường Hợp 2: Use-After-Free

cpp
// ❌ BUG — Use after delete
int* ptr = new int(42);
delete ptr;             // giải phóng bộ nhớ
std::cout << *ptr;      // 💥 Use-after-free!

// ✅ ĐÚNG — nullptr hóa sau delete
int* ptr = new int(42);
delete ptr;
ptr = nullptr;          // đánh dấu "không còn valid"

// Nhưng TỐT HƠN — dùng smart pointer:
auto ptr = std::make_unique<int>(42);
// Tự giải phóng khi ra khỏi scope — không cần delete!

5.3 Trường Hợp 3: Dangling Reference (Ít Gặp Hơn Nhưng Nguy Hiểm)

cpp
// ❌ BUG — Dangling reference
int& getRef() {
    int local = 42;
    return local;        // ⚠️ Compiler cảnh báo, nhưng vẫn compile!
}

int main() {
    int& ref = getRef();
    std::cout << ref;    // 💥 Undefined Behavior
}

⚠️ Compiler Không Phải Lúc Nào Cũng Cứu Bạn

Với GCC/Clang, flag -Wall -Werror sẽ bắt được nhiều lỗi dangling. Nhưng không phải tất cả. Luôn bật warning flags tối đa:

bash
g++ -std=c++20 -Wall -Wextra -Werror -Wpedantic main.cpp

6. const Với Pointers: Đọc Từ Phải Sang Trái 📖

const và pointer kết hợp tạo ra 4 biến thể. Quy tắc nhớ: đọc khai báo từ phải sang trái.

6.1 Bốn Biến Thể

cpp
int value = 42;
int other = 99;

// 1. Pointer thường → biến thường
int* ptr = &value;
*ptr = 10;              // ✅ thay đổi giá trị
ptr = &other;           // ✅ trỏ sang biến khác

// 2. Pointer tới const (pointer-to-const)
const int* ptrToConst = &value;
// Đọc: "ptrToConst là pointer tới const int"
*ptrToConst = 10;       // ❌ Lỗi! không thể thay đổi giá trị
ptrToConst = &other;    // ✅ có thể trỏ sang biến khác

// 3. Const pointer (const-pointer)
int* const constPtr = &value;
// Đọc: "constPtr là const pointer tới int"
*constPtr = 10;         // ✅ có thể thay đổi giá trị
constPtr = &other;      // ❌ Lỗi! không thể trỏ sang biến khác

// 4. Const pointer tới const
const int* const both = &value;
// Đọc: "both là const pointer tới const int"
*both = 10;             // ❌ Lỗi!
both = &other;          // ❌ Lỗi!

6.2 Bảng Tóm Tắt

Khai báoĐổi giá trị (*ptr)Đổi địa chỉ (ptr)Nhớ nhanh
int*Tự do hoàn toàn
const int*"Nhìn nhưng không sờ"
int* const"Trung thành 1 địa chỉ"
const int* const"Khóa chặt"

6.3 Quy Tắc Đọc Phải-Sang-Trái

const int* const ptr
  ↑     ↑    ↑    ↑
  │     │    │    └── ptr (tên biến)
  │     │    └─────── là const (không đổi địa chỉ)
  │     └──────────── pointer tới
  └────────────────── const int (không đổi giá trị)

→ "ptr là const pointer tới const int"

💡 Mẹo thực hành

Khi viết hàm nhận pointer, mặc định dùng const:

cpp
// ✅ "Tôi sẽ đọc nhưng không sửa dữ liệu của bạn"
void print(const int* data, size_t size);

// Chỉ bỏ const khi thực sự cần sửa:
void reset(int* data, size_t size);

Đây là const correctness — một trong những discipline quan trọng nhất của C++.


7. Pointer Arithmetic: Di Chuyển Trong Mảng 🔢

Pointer arithmetic cho phép bạn "nhảy" giữa các phần tử trong vùng nhớ liên tục (mảng). Mỗi bước nhảy bằng đúng sizeof(T) byte.

7.1 Cách Hoạt Động

cpp
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;         // arr tự decay thành pointer tới phần tử đầu

std::cout << *ptr;      // 10 — phần tử [0]
std::cout << *(ptr+1);  // 20 — phần tử [1]
std::cout << *(ptr+2);  // 30 — phần tử [2]

// Thực tế: arr[i] chính xác tương đương *(arr + i)
std::cout << arr[2];    // 30
std::cout << *(arr+2);  // 30 — CÙNG kết quả!

Minh họa bộ nhớ:

arr (kiểu int, mỗi phần tử 4 byte):
┌──────┬──────┬──────┬──────┬──────┐
│  10  │  20  │  30  │  40  │  50  │
└──────┴──────┴──────┴──────┴──────┘
0x1000 0x1004 0x1008 0x100C 0x1010
  ↑       ↑       ↑
  ptr   ptr+1   ptr+2

ptr + 1 = 0x1000 + 1 * sizeof(int) = 0x1004
ptr + 2 = 0x1000 + 2 * sizeof(int) = 0x1008

7.2 Khoảng Cách Giữa Hai Pointer

cpp
int arr[] = {10, 20, 30, 40, 50};
int* begin = &arr[0];
int* end   = &arr[4];

ptrdiff_t distance = end - begin;  // 4 (phần tử, không phải byte!)

⚠️ Pointer Arithmetic: Vùng "Di Sản"

Pointer arithmetic mạnh nhưng nguy hiểm. Trong modern C++:

  • Dùng std::vector + iterators thay vì raw array + pointer
  • Dùng std::span (C++20) khi cần "view" vào mảng
  • Dùng range-based for thay vì for (int* p = arr; p != end; ++p)
cpp
// ❌ C-style — dễ sai, khó đọc
for (int* p = arr; p < arr + 5; ++p) {
    std::cout << *p << ' ';
}

// ✅ Modern C++ — rõ ràng, an toàn
for (int x : arr) {
    std::cout << x << ' ';
}

// ✅ C++20 — span cho non-owning view
void process(std::span<const int> data) {
    for (int x : data) { /* ... */ }
}

Bạn cần hiểu pointer arithmetic để đọc C codehiểu C APIs, nhưng không nên viết code mới theo style này.


8. Smart Pointer Preview: Tương Lai Không Leak 🚀

Raw pointer có hai vấn đề lớn:

  1. Ai chịu trách nhiệm delete? → Memory leak nếu quên
  2. Pointer còn valid không? → Dangling nếu đã xóa

C++11 giải quyết cả hai bằng smart pointers:

cpp
#include <memory>

// std::unique_ptr — sở hữu DUY NHẤT
{
    auto ptr = std::make_unique<int>(42);
    std::cout << *ptr;     // 42
}   // ← Tự động delete ở đây, KHÔNG THỂ quên!

// std::shared_ptr — sở hữu CHIA SẺ (nhiều nơi cùng trỏ)
{
    auto p1 = std::make_shared<int>(42);
    auto p2 = p1;          // reference count = 2
}   // ← Delete khi count = 0

📍 Chúng ta sẽ đào sâu smart pointer ở Bài 05 (RAII)

Ở bài này, bạn chỉ cần nhớ một quy tắc:

Trong modern C++ (2024), raw new/delete gần như KHÔNG BAO GIỜ cần thiết.

cpp
// ❌ Old-school C++ (pre-2011)
int* data = new int[100];
// ... 50 dòng code ...
delete[] data;  // Dễ quên, dễ double-free

// ✅ Modern C++ — zero leak by design
auto data = std::make_unique<int[]>(100);
// Tự giải phóng — không cần nhớ, không thể quên

9. Tổng Hợp: Pointer & Reference Trong Thực Tế

9.1 Function Parameters — Use Case Phổ Biến Nhất

cpp
struct Student {
    std::string name;
    int grade;
};

// Pattern 1: Read-only — const reference (phổ biến nhất)
void printStudent(const Student& s) {
    std::cout << s.name << ": " << s.grade << '\n';
}

// Pattern 2: Modify — non-const reference
void upgradeGrade(Student& s) {
    s.grade += 10;
}

// Pattern 3: Optional parameter — pointer
void setMentor(Student& s, const Student* mentor) {
    if (mentor) {
        std::cout << s.name << " mentored by " << mentor->name;
    }
    // mentor có thể là nullptr — hợp lệ!
}

// Sử dụng:
Student alice{"Alice", 85};
Student bob{"Bob", 90};

printStudent(alice);           // const& — không copy
upgradeGrade(alice);           // & — sửa trực tiếp
setMentor(alice, &bob);       // pointer — có mentor
setMentor(bob, nullptr);      // pointer — không có mentor

9.2 Arrow Operator -> — Truy Cập Member Qua Pointer

cpp
Student* ptr = &alice;

// Hai cách viết tương đương:
(*ptr).name;    // dereference rồi truy cập — xấu
ptr->name;      // arrow operator — sạch hơn ✅

// ptr->member === (*ptr).member

💼 Production Story: Google's C++ Style

Google's C++ Style Guide (một trong những chuẩn công nghiệp phổ biến nhất) quy định:

  • Input parameters: const T& hoặc T (by value cho small types)
  • Output parameters: return by value (nhờ RVO/NRVO)
  • Optional input: const T*
  • Raw owning pointer: Cấm — phải dùng std::unique_ptr

Bạn đang học đúng pattern mà các kỹ sư ở Google, Meta, Microsoft sử dụng hàng ngày.


🏋️ Bài Tập Nhanh: Dự Đoán Output

🏋️ Fast Exercise

Đọc code sau và dự đoán output trước khi xem đáp án:

cpp
#include <iostream>

int main() {
    int a = 10;
    int b = 20;
    int* ptr = &a;
    int& ref = a;

    *ptr = 30;           // Dòng 1
    ptr = &b;            // Dòng 2
    *ptr = 40;           // Dòng 3
    ref = 50;            // Dòng 4

    std::cout << a << ' ' << b << ' '
              << *ptr << ' ' << ref << '\n';
}

Bạn nghĩ output là gì?

👉 Xem đáp án

Output: 50 40 40 50

Phân tích từng bước:

  1. *ptr = 30;ptr trỏ tới a, nên a = 30
  2. ptr = &b;ptr bây giờ trỏ tới b (reseat!)
  3. *ptr = 40;ptr trỏ tới b, nên b = 40
  4. ref = 50;ref vẫn là alias của a (không reseat được!), nên a = 50

Kết quả: a = 50, b = 40, *ptr = b = 40, ref = a = 50

Bài học: Pointer có thể reseat, reference thì không.


🐛 Spot the Bug

Đoạn C++ bên dưới có một lỗi nghiêm trọng dẫn tới undefined behavior. Bạn tìm được không?

cpp
#include <iostream>
#include <string>

std::string* findLonger(const std::string& a, const std::string& b) {
    std::string result;
    if (a.size() >= b.size()) {
        result = a;
    } else {
        result = b;
    }
    return &result;  // ← ???
}

int main() {
    std::string x = "Hello";
    std::string y = "World!";
    std::string* longer = findLonger(x, y);
    std::cout << *longer << '\n';
}
💡 Gợi ý

Hãy xem biến result sống ở đâu (stack hay heap?) và điều gì xảy ra khi hàm findLonger return.

✅ Lời giải

Bug: Trả về địa chỉ của biến cục bộ result — dangling pointer!

result là biến local của findLonger(). Khi hàm return, stack frame bị hủy → result không còn tồn tại → pointer trả về trỏ tới "đống đổ nát".

Cách sửa — return by value (RVO sẽ tối ưu):

cpp
std::string findLonger(const std::string& a, const std::string& b) {
    return (a.size() >= b.size()) ? a : b;
}

int main() {
    std::string x = "Hello";
    std::string y = "World!";
    std::string longer = findLonger(x, y);  // ✅ An toàn
    std::cout << longer << '\n';
}

Hoặc nếu muốn tránh copy, trả về const reference:

cpp
const std::string& findLonger(const std::string& a,
                               const std::string& b) {
    return (a.size() >= b.size()) ? a : b;  // ✅ trả ref tới tham số
}

Bài học: Không bao giờ trả về pointer/reference tới biến cục bộ. Compiler flag -Wreturn-local-addr sẽ cảnh báo lỗi này.


Performance Note

⚡ Compiler thực sự làm gì với reference?

Reference và pointer về mặt assembly thường giống nhau. Compiler implement reference bằng pointer ở cấp độ machine code.

cpp
void byRef(int& x)   { x = 42; }
void byPtr(int* x)   { *x = 42; }

Cả hai thường generate assembly giống hệt nhau:

asm
mov  DWORD PTR [rdi], 42
ret

Vậy tại sao dùng reference? Vì abstraction:

  • An toàn hơn: Không null, không reseat
  • Sạch hơn: Không cần *& rải rác
  • Ý định rõ hơn: "Tôi sẽ sửa biến này" vs "Đây có thể là null"

Zero-cost abstraction — bạn được safety mà không trả giá performance.


🎮 Playground: Thử Ngay Trên Godbolt

Copy đoạn code sau vào Godbolt Compiler Explorer (chọn compiler x86-64 gcc 13.2, flags: -std=c++20 -O2):

cpp
#include <iostream>
#include <memory>

// Demo 1: Pointer vs Reference
void demo_basics() {
    int value = 42;
    int* ptr = &value;    // pointer — lưu địa chỉ
    int& ref = value;     // reference — alias

    std::cout << "=== Demo 1: Basics ===\n";
    std::cout << "value   = " << value << '\n';
    std::cout << "*ptr    = " << *ptr << '\n';
    std::cout << "ref     = " << ref << '\n';
    std::cout << "&value  = " << &value << '\n';
    std::cout << "ptr     = " << ptr << '\n';
    std::cout << "&ref    = " << &ref << '\n';
    std::cout << "(ptr == &ref? " << (ptr == &ref ? "YES" : "NO") << ")\n\n";
}

// Demo 2: const correctness
void demo_const() {
    int a = 10, b = 20;
    const int* pToConst = &a;   // pointer-to-const
    int* const constP = &a;     // const-pointer

    // pToConst can change what it points to:
    pToConst = &b;               // ✅ OK

    // constP can change the value:
    *constP = 99;                // ✅ OK

    std::cout << "=== Demo 2: const ===\n";
    std::cout << "a = " << a << ", b = " << b << '\n';
    std::cout << "*pToConst = " << *pToConst << '\n';
    std::cout << "*constP   = " << *constP << '\n\n';
}

// Demo 3: Smart pointer preview
void demo_smart() {
    auto up = std::make_unique<int>(42);
    std::cout << "=== Demo 3: unique_ptr ===\n";
    std::cout << "*up = " << *up << '\n';
    // up tự giải phóng khi ra khỏi scope
}

int main() {
    demo_basics();
    demo_const();
    demo_smart();
    return 0;
}

Thử thay đổi: Uncomment // *pToConst = 30; trong demo_const() — compiler sẽ báo lỗi gì?


📝 Quiz: Kiểm Tra Hiểu Biết

🧠 Quiz

Câu 1: const int* ptr nghĩa là gì?

  • [ ] A. ptr không thể thay đổi giá trị (là const pointer)
  • [x] B. Giá trị mà ptr trỏ tới không thể thay đổi qua ptr
  • [ ] C. Cả ptr và giá trị đều không đổi
  • [ ] D. ptr phải được khởi tạo bằng const int

💡 Giải thích: const int* ptr — đọc phải sang trái: "ptr là pointer tới const int". Bạn không thể dùng *ptr = ... để sửa giá trị, nhưng có thể ptr = &other để trỏ sang chỗ khác.

🧠 Quiz

Câu 2: Đoạn code nào gây Undefined Behavior?

  • [ ] A. int* p = nullptr; if (p) *p = 5;
  • [x] B. int* p = nullptr; *p = 5;
  • [ ] C. int x = 10; int& r = x; r = 20;
  • [ ] D. int x = 10; int* p = &x; p = nullptr;

💡 Giải thích: Dereference nullptr là undefined behavior. Đáp án A an toàn vì check if (p) trước. Đáp án B dereference trực tiếp mà không kiểm tra → crash hoặc tệ hơn.

🧠 Quiz

Câu 3: Sự khác biệt chính giữa pointer và reference là gì?

  • [ ] A. Reference nhanh hơn pointer
  • [ ] B. Pointer chiếm nhiều bộ nhớ hơn reference
  • [x] C. Reference phải được khởi tạo và không thể reseat
  • [ ] D. Pointer không thể trỏ tới struct/class

💡 Giải thích: Reference bắt buộc khởi tạo khi khai báo và không thể đổi sang biến khác (no reseat). Đây là ràng buộc giúp reference an toàn hơn pointer. Về performance, cả hai thường generate cùng machine code.


🎮 Scenario: Code Review Thực Tế

🎮 Scenario: Bạn review code của junior developer

Junior gửi PR với hàm sau:

cpp
void processData(int* data, int* size, int* result) {
    for (int i = 0; i < *size; i++) {
        *result += data[i];
    }
}

Bạn sẽ comment gì?

👉 Xem code review suggestions

Comment 1: Dùng reference thay pointer cho sizeresult

cpp
// size và result KHÔNG BAO GIỜ null → dùng reference
void processData(const int* data, int size, int& result);

Comment 2: data nên là const vì hàm chỉ đọc

cpp
void processData(const int* data, int size, int& result);

Comment 3: Modern C++ — dùng std::span

cpp
// C++20 — an toàn, rõ ràng, không cần size riêng
int processData(std::span<const int> data) {
    int result = 0;
    for (int x : data) {
        result += x;
    }
    return result;
}

Comment 4: Hoặc đơn giản — dùng std::accumulate

cpp
#include <numeric>
int total = std::accumulate(data.begin(), data.end(), 0);

Bài học: Raw pointer params thường là dấu hiệu của C-style code. Trong C++, prefer references, const, return by value, và standard library algorithms.


⚠️ Pitfalls Thường Gặp

⚠️ Cạm bẫy

Sai lầm 1: "Tôi dùng reference nên không cần lo dangling"

Sai: Reference cũng có thể dangle!

cpp
int& ref = *new int(42);  // ref tới heap object
delete &ref;               // xóa object
std::cout << ref;          // 💥 Dangling reference!

Đúng: Reference an toàn hơn nhưng không miễn nhiễm. Luôn đảm bảo đối tượng sống lâu hơn reference trỏ tới nó.

⚠️ Cạm bẫy

Sai lầm 2: "sizeof(ptr) cho biết kích thước dữ liệu"

Sai:

cpp
int arr[] = {1, 2, 3, 4, 5};
int* ptr = arr;
std::cout << sizeof(ptr);     // 8 — kích thước POINTER, không phải mảng!
std::cout << sizeof(arr);     // 20 — kích thước mảng (5 × 4 bytes)

Đúng: sizeof(pointer) luôn trả về kích thước con trỏ (8 byte trên 64-bit). Muốn biết kích thước mảng, dùng std::size(arr) (C++17) hoặc std::span.

⚠️ Cạm bẫy

Sai lầm 3: "Con trỏ và mảng là một"

Sai: Array decay thành pointer khi truyền vào hàm, nhưng chúng không giống nhau:

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;               // array decays to pointer

sizeof(arr) == 20;            // ✅ biết kích thước mảng
sizeof(ptr) == 8;             // chỉ biết kích thước pointer

// Trong hàm — thông tin kích thước bị mất:
void foo(int arr[]) {         // thực ra là int* arr
    sizeof(arr);              // 8, KHÔNG phải kích thước mảng!
}

Đúng: Dùng std::array hoặc std::vector để giữ thông tin kích thước, hoặc std::span<int> (C++20) cho non-owning view.


📋 Tóm Tắt Kiến Thức

✅ Checklist triển khai

Pointer Basics

  • [ ] Pointer lưu địa chỉ bộ nhớ, không phải giá trị
  • [ ] & lấy địa chỉ, * dereference
  • [ ] Luôn khởi tạo pointer bằng nullptr hoặc địa chỉ hợp lệ
  • [ ] Dùng nullptr (C++11), KHÔNG dùng NULL hay 0

Reference Basics

  • [ ] Reference là alias — phải khởi tạo, không reseat, không null
  • [ ] const T& là pattern phổ biến nhất cho function parameters
  • [ ] Reference an toàn hơn pointer nhưng không thể thay thế hoàn toàn

Safety

  • [ ] Nhận biết 3 dạng dangling: return local, use-after-free, dangling ref
  • [ ] Hiểu 4 biến thể const với pointer (đọc phải sang trái)
  • [ ] Bật -Wall -Wextra -Werror để compiler giúp bắt lỗi

Modern C++ Mindset

  • [ ] "Dùng reference khi có thể, pointer khi cần thiết"
  • [ ] Raw new/delete → thay bằng smart pointer
  • [ ] Pointer arithmetic → thay bằng iterators, ranges, std::span
  • [ ] Sẵn sàng cho Bài 05: RAII & Smart Pointers