Giao diện
🎯 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 saoNULLbị deprecated - Nhận diện dangling pointer — kẻ giết người thầm lặng trong C++
- Áp dụng
constcorrectness 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ĩa | Ví dụ |
|---|---|---|---|
& | Address-of | Lấy địa chỉ của biến | &value → 0x1000 |
* | Dereference | Truy cập giá trị tại địa chỉ | *ptr → 42 |
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ờ = 992.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 ambiguousnullptr đượ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 value3.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ể null3.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ểm | Pointer (int*) | Reference (int&) |
|---|---|---|
| Có thể null | ✅ nullptr | ❌ Luôn phải valid |
| Có thể reseat | ✅ ptr = &other | ❌ Gắn 1 lần duy nhất |
| Cần dereference | ✅ *ptr | ❌ Tự động |
| Pointer arithmetic | ✅ ptr++, ptr + n | ❌ Không hỗ trợ |
| Khai báo không init | ✅ (nhưng nguy hiểm) | ❌ Lỗi compile |
| Kích thước | 8 byte (64-bit) | 0 byte (logic)* |
| Cú pháp | Phức tạp hơn | Sạch hơn |
| Khi nào dùng | Optional, ownership, arrays | Params, 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.cpp6. 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) = 0x10087.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 code và hiể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:
- Ai chịu trách nhiệm
delete? → Memory leak nếu quên - 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ên9. 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ó mentor9.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ặcT(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:
*ptr = 30;→ptrtrỏ tớia, nêna = 30ptr = &b;→ptrbây giờ trỏ tớib(reseat!)*ptr = 40;→ptrtrỏ tớib, nênb = 40ref = 50;→refvẫn là alias củaa(không reseat được!), nêna = 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
retVậ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
*và&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;trongdemo_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.
ptrkhông thể thay đổi giá trị (là const pointer) - [x] B. Giá trị mà
ptrtrỏ tới không thể thay đổi quaptr - [ ] C. Cả
ptrvà giá trị đều không đổi - [ ] D.
ptrphải được khởi tạo bằngconst 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
nullptrlà undefined behavior. Đáp án A an toàn vì checkif (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 size và result
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
nullptrhoặc địa chỉ hợp lệ - [ ] Dùng
nullptr(C++11), KHÔNG dùngNULLhay0
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