// Hướng dẫn toàn tập · Dành cho gia sư

Con Trỏ
Từ Số 0

Hành trình chinh phục con trỏ từ khái niệm bộ nhớ cơ bản đến kỹ thuật nâng cao — được thiết kế để bạn giảng dạy dễ dàng, học viên hiểu nhanh.

● Cơ bản ● Trung cấp ● Nâng cao
// 00 · Nền tảng

Bộ nhớ máy tính hoạt động thế nào?

● Cơ bản

Trước khi học con trỏ, ta phải hiểu bộ nhớ RAM là gì. Hãy hình dung nó như một dãy ô nhớ khổng lồ — mỗi ô có một địa chỉ duy nhất và lưu trữ dữ liệu.

🏠 Phép so sánh: Dãy nhà

🏠

Địa chỉ nhà

Số nhà 0x1000 — địa chỉ trong bộ nhớ

👤

Người ở trong nhà

Giá trị được lưu tại địa chỉ đó — ví dụ 42

📋

Tờ giấy ghi địa chỉ

Con trỏ — lưu địa chỉ của ngôi nhà khác

📦 Sơ đồ bộ nhớ

Khi bạn khai báo int x = 42;, hệ thống cấp phát 4 byte trong RAM:

0x1000
42
x
0x1004
0x1008
0x100C
💡 Ghi chú cho gia sư

Hỏi học viên: "Nếu tôi muốn gửi cho bạn một quyển sách, tôi cần biết gì?" → Địa chỉ nhà bạn. Con trỏ chính là "địa chỉ" đó trong lập trình.

// 01 · Khái niệm

Con trỏ là gì?

● Cơ bản

Con trỏ là một biến đặc biệt — thay vì lưu dữ liệu thực (như số, ký tự), nó lưu địa chỉ bộ nhớ của biến khác.

Ví dụ đầu tiên

pointer_intro.c
C
#include <stdio.h>

int main() {
    int x = 42;          // biến bình thường
    int *p = &x;         // p lưu ĐỊA CHỈ của x

    printf("Giá trị x  = %d\n", x);
    printf("Địa chỉ x = %p\n", (void*)&x);
    printf("p chứa    = %p\n", (void*)p);
    printf("*p        = %d\n", *p);   // truy cập qua con trỏ

    return 0;
}
Giá trị x = 42 Địa chỉ x = 0x7ffd1234abc0 p chứa = 0x7ffd1234abc0 *p = 42

Sơ đồ bộ nhớ minh họa

0x1000
42
x
0x2000
0x1000
p

con trỏ p tại địa chỉ 0x2000 lưu giá trị 0x1000 — chính là địa chỉ của x

⚠️ Dễ nhầm

p = địa chỉ (là 0x1000) · *p = giá trị TẠI địa chỉ đó (là 42). Dấu * khi dereference KHÔNG giống dấu * khi khai báo!

🎯 Kiểm tra nhanh

Cho int a = 10; int *q = &a; — giá trị của *q là?
*q là toán tử dereference — đọc giá trị tại địa chỉ mà q đang trỏ tới. Vì q trỏ tới a, và a = 10, nên *q = 10.
// 02 · Syntax

Khai báo & Toán tử

● Cơ bản
Cú phápÝ nghĩaVí dụ
type *nameKhai báo con trỏint *p;
&variableLấy địa chỉ của biếnp = &x;
*pointerDereference (đọc/ghi giá trị)*p = 99;
NULL / nullptrCon trỏ rỗng (không trỏ đâu)p = NULL;

Ghi qua con trỏ

pointer_write.c
C
#include <stdio.h>

int main() {
    int x = 10;
    int *p = &x;

    printf("Trước: x = %d\n", x);
    *p = 99;   // GHI qua con trỏ — thay đổi x!
    printf("Sau:   x = %d\n", x);

    return 0;
}
Trước: x = 10 Sau: x = 99

Pointer Arithmetic — Cộng trừ địa chỉ

ptr_arith.c
C
int arr[] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p);       // 10  (arr[0])
printf("%d\n", *(p+1));   // 20  (arr[1]) — cộng 4 bytes
printf("%d\n", *(p+2));   // 30  (arr[2]) — cộng 8 bytes
p++;                      // p giờ trỏ tới arr[1]
💡 Mẹo giảng dạy

Khi p++ với int*, địa chỉ thực sự tăng 4 bytes (kích thước int), không phải 1. Đây là lý do pointer arithmetic rất mạnh khi duyệt mảng!

🔬 Trình diễn tương tác — Kích thước con trỏ

Chọn kiểu dữ liệu để xem cách pointer arithmetic hoạt động:

// Nhấn ▶ Chạy để xem kết quả
// 03 · Mảng

Con trỏ & Mảng

● Cơ bản

Trong C, tên mảng về bản chất là một con trỏ trỏ tới phần tử đầu tiên. arr&arr[0] có cùng giá trị!

array_ptr.c
C
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;   // không cần & vì arr đã là con trỏ

    // 3 cách truy cập arr[2] — tất cả đều giống nhau!
    printf("%d\n", arr[2]);      // Cách 1: chỉ số mảng
    printf("%d\n", *(arr+2));    // Cách 2: pointer arithmetic
    printf("%d\n", p[2]);        // Cách 3: dùng con trỏ như mảng

    // Duyệt mảng bằng con trỏ
    printf("Mảng: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *(p++));
    }
    return 0;
}
30 30 30 Mảng: 10 20 30 40 50

Sơ đồ — Mảng trong bộ nhớ

arr[0]
10
← p
arr[1]
20
arr[2]
30
arr[3]
40
arr[4]
50

Các phần tử nằm liên tiếp, mỗi ô cách nhau sizeof(int) = 4 bytes

✏️ Bài tập thực hành

Viết chương trình dùng con trỏ để:

  • Tính tổng các phần tử mảng
  • Tìm phần tử lớn nhất
  • Đảo ngược mảng tại chỗ (in-place)
// 04 · Hàm

Pass by Reference — Con trỏ & Hàm

● Trung cấp

Trong C, mọi thứ truyền vào hàm đều là copy (pass by value). Dùng con trỏ để hàm có thể thay đổi biến gốc.

So sánh: Value vs Reference

swap_demo.c
C
#include <stdio.h>

// ❌ Không hoạt động — chỉ đổi bản sao!
void swap_wrong(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
    // a, b là local — khi return, thay đổi mất!
}

// ✅ Đúng — truyền địa chỉ, thay đổi giá trị gốc
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {
    int x = 5, y = 10;

    swap_wrong(x, y);
    printf("Sau swap_wrong: x=%d y=%d\n", x, y); // 5 10 (không đổi!)

    swap(&x, &y);
    printf("Sau swap:       x=%d y=%d\n", x, y); // 10 5 ✓

    return 0;
}
Sau swap_wrong: x=5 y=10 Sau swap: x=10 y=5

Trả về nhiều giá trị qua con trỏ

multi_return.c
C
// Hàm tìm min và max cùng lúc
void minmax(int *arr, int n, int *min, int *max) {
    *min = *max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main() {
    int data[] = {3, 7, 1, 9, 4};
    int lo, hi;
    minmax(data, 5, &lo, &hi);
    printf("Min=%d, Max=%d\n", lo, hi);
}
Min=1, Max=9

✏️ Bài tập

Viết hàm divide(int a, int b, int *quotient, int *remainder) trả về cả thương và dư qua con trỏ.

// 05 · Chuỗi

Con trỏ & Chuỗi

● Trung cấp

Chuỗi trong C là mảng ký tự kết thúc bằng '\0'. char* là con trỏ đến ký tự đầu tiên.

string_ptr.c
C
#include <stdio.h>

// Đếm độ dài chuỗi bằng con trỏ
int my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;
    return (int)(p - s);
}

// Đảo ngược chuỗi
void reverse_str(char *s) {
    char *end = s;
    while (*end) end++;
    end--;
    while (s < end) {
        char tmp = *s;
        *s++ = *end;
        *end-- = tmp;
    }
}

int main() {
    char s[] = "hello";
    printf("Độ dài: %d\n", my_strlen(s));
    reverse_str(s);
    printf("Đảo ngược: %s\n", s);
}
Độ dài: 5 Đảo ngược: olleh
⚠️ Cảnh báo quan trọng

char *s = "hello"; tạo con trỏ đến string literal — bộ nhớ READ-ONLY. Không thể sửa! Dùng char s[] = "hello"; nếu cần chỉnh sửa.

// 06 · Heap

Cấp phát bộ nhớ động

● Trung cấp

Stack có giới hạn và tự giải phóng. Heap thì lớn hơn — nhưng bạn phải tự quản lý bộ nhớ bằng malloc/free.

📚

Stack

Tự động cấp phát/giải phóng. Kích thước cố định tại compile time. Nhanh.

🏗️

Heap

Cấp phát thủ công. Kích thước linh hoạt tại runtime. Phải free() sau khi dùng.

dynamic_alloc.c
C
#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("Nhập số phần tử: ");
    scanf("%d", &n);

    // Cấp phát n ô nhớ kiểu int trên heap
    int *arr = (int*) malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Lỗi cấp phát bộ nhớ!\n");
        return 1;
    }

    for (int i = 0; i < n; i++)
        arr[i] = i * i;   // bình phương

    for (int i = 0; i < n; i++)
        printf("%d ", arr[i]);

    free(arr);   // BẮT BUỘC — tránh memory leak!
    arr = NULL; // Good practice sau khi free
    return 0;
}

Các hàm cấp phát

HàmMô tảKhởi tạo
malloc(size)Cấp phát size bytesKhông (garbage)
calloc(n, size)Cấp phát n×size bytesCó (zeros)
realloc(ptr, size)Thay đổi kích thước
free(ptr)Giải phóng bộ nhớ
🚨 Memory Leak

Nếu quên free(), bộ nhớ bị chiếm mãi đến khi chương trình tắt. Với chương trình lâu dài (server, app...) điều này cực kỳ nguy hiểm — hệ thống sẽ chạy chậm rồi crash.

// 07 · Nâng cao

Con trỏ Hàm

● Nâng cao

Hàm cũng được lưu trong bộ nhớ và có địa chỉ! Con trỏ hàm cho phép truyền hàm như tham số — nền tảng của callback, strategy pattern.

func_ptr.c
C
#include <stdio.h>

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int sub(int a, int b) { return a - b; }

// Hàm nhận con trỏ hàm làm tham số
void apply(int x, int y, int (*op)(int, int), const char *name) {
    printf("%s(%d, %d) = %d\n", name, x, y, op(x, y));
}

int main() {
    // Khai báo mảng con trỏ hàm
    int (*ops[3])(int, int) = {add, mul, sub};
    const char *names[] = {"add", "mul", "sub"};

    for (int i = 0; i < 3; i++)
        apply(6, 3, ops[i], names[i]);

    return 0;
}
add(6, 3) = 9 mul(6, 3) = 18 sub(6, 3) = 3
📚 Ứng dụng thực tế

Con trỏ hàm là nền tảng của: qsort() trong stdlib · Callback trong lập trình hệ thống · Strategy pattern trong design patterns · Event handlers.

// 08 · Advanced

Con trỏ đến Con trỏ (**ptr)

● Nâng cao

Con trỏ cũng là biến — có địa chỉ — nên có thể tạo con trỏ trỏ đến con trỏ. Cực kỳ cần thiết khi làm việc với mảng 2D động và linked list.

ptr_to_ptr.c
C
#include <stdio.h>

int main() {
    int   x   = 42;    // biến int
    int  *p   = &x;    // con trỏ đến int
    int **pp  = &p;    // con trỏ đến con trỏ đến int

    printf("x    = %d\n",  x);
    printf("*p   = %d\n", *p);
    printf("**pp = %d\n", **pp);   // đều là 42

    **pp = 100;   // ghi xuyên qua 2 lớp con trỏ
    printf("x bây giờ = %d\n", x);  // 100
    return 0;
}

Sơ đồ 3 cấp

0x3000
0x2000
pp
0x2000
0x1000
p
0x1000
42
x

Ứng dụng: Mảng 2D động

2d_array.c
C
#include <stdlib.h>
#include <stdio.h>

int main() {
    int rows = 3, cols = 4;

    // Cấp phát mảng con trỏ (mỗi phần tử là 1 hàng)
    int **mat = (int**) malloc(rows * sizeof(int*));
    for (int i = 0; i < rows; i++)
        mat[i] = (int*) malloc(cols * sizeof(int));

    // Điền và in
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            mat[i][j] = i * cols + j;

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++)
            printf("%3d", mat[i][j]);
        printf("\n");
    }

    // Giải phóng theo thứ tự ngược lại
    for (int i = 0; i < rows; i++)
        free(mat[i]);
    free(mat);
    return 0;
}
0 1 2 3 4 5 6 7 8 9 10 11
// 09 · Struct

Con trỏ & Struct — Linked List

● Nâng cao

Con trỏ đến struct là nền tảng của mọi cấu trúc dữ liệu động: linked list, tree, graph. Toán tử -> là cách truy cập thành viên qua con trỏ.

linked_list.c
C
#include <stdio.h>
#include <stdlib.h>

// Định nghĩa Node
typedef struct Node {
    int data;
    struct Node *next;  // con trỏ đến node tiếp theo
} Node;

// Tạo node mới
Node* new_node(int val) {
    Node* n = (Node*) malloc(sizeof(Node));
    n->data = val;   // n->x tương đương (*n).x
    n->next = NULL;
    return n;
}

// Thêm vào đầu
void push(Node **head, int val) {
    Node* n = new_node(val);
    n->next = *head;
    *head = n;
}

// In danh sách
void print_list(Node* h) {
    while (h) {
        printf("%d → ", h->data);
        h = h->next;
    }
    printf("NULL\n");
}

int main() {
    Node* head = NULL;
    push(&head, 30);
    push(&head, 20);
    push(&head, 10);
    print_list(head);
}
10 → 20 → 30 → NULL

✏️ Thử thách

Mở rộng linked list với các hàm:

  • pop(Node **head) — xóa phần tử đầu
  • search(Node *head, int val) — tìm kiếm
  • reverse(Node **head) — đảo ngược danh sách
  • free_list(Node **head) — giải phóng toàn bộ bộ nhớ
// 10 · C++ Modern

Smart Pointers trong C++

● Expert

C++11 giới thiệu smart pointers — con trỏ "thông minh" tự động giải phóng bộ nhớ, loại bỏ memory leak.

🔒

unique_ptr

Sở hữu duy nhất. Không thể copy, chỉ move. Tự giải phóng khi ra khỏi scope.

unique_ptr<int>
🤝

shared_ptr

Nhiều con trỏ cùng sở hữu. Đếm tham chiếu (reference count). Free khi count = 0.

shared_ptr<int>
👀

weak_ptr

Quan sát shared_ptr mà không tăng count. Dùng để tránh circular reference.

weak_ptr<int>
smart_ptr.cpp
C++
#include <memory>
#include <iostream>
using namespace std;

struct Dog {
    string name;
    Dog(string n) : name(n) { cout << "Dog " << name << " born\n"; }
    ~Dog() { cout << "Dog " << name << " died\n"; }
};

int main() {
    // unique_ptr — tự free khi ra scope
    {
        auto d1 = make_unique<Dog>("Rex");
        cout << "Trong scope\n";
    } // Rex tự động bị xóa ở đây!
    cout << "Sau scope\n";

    // shared_ptr — đếm tham chiếu
    auto d2 = make_shared<Dog>("Buddy");
    {
        auto d3 = d2;   // count = 2
        cout << "Refs: " << d2.use_count() << "\n";  // 2
    } // d3 xóa, count = 1
    cout << "Refs: " << d2.use_count() << "\n";    // 1
    return 0;  // Buddy tự xóa
}
Dog Rex born Trong scope Dog Rex died Sau scope Dog Buddy born Refs: 2 Refs: 1 Dog Buddy died
// 11 · Bugs

Những lỗi nguy hiểm nhất

● Expert
1

🔴 Dangling Pointer — Con trỏ trỏ vào bộ nhớ đã free

Sau khi free(p), p vẫn giữ địa chỉ cũ. Truy cập *p = undefined behavior!

2

🔴 Wild Pointer — Con trỏ chưa khởi tạo

int *p; *p = 5;p chứa rác, truy cập vùng nhớ ngẫu nhiên → crash hoặc corruption.

3

🔴 Buffer Overflow — Ghi ngoài vùng cấp phát

int arr[3]; arr[5] = 99; — ghi vào vùng nhớ không thuộc về mình → security vulnerability!

4

🔴 Memory Leak — Quên free

Gọi malloc mà không free → bộ nhớ bị chiếm vĩnh viễn đến khi tắt chương trình.

5

🔴 Double Free — Free cùng một vùng nhớ 2 lần

Gọi free(p) hai lần → undefined behavior, có thể corrupt allocator state.

✅ Checklist phòng tránh

safe_pointer.c — Best Practices
C
// ✅ 1. Luôn khởi tạo con trỏ
int *p = NULL;

// ✅ 2. Kiểm tra NULL trước khi dùng
if (p != NULL) { *p = 5; }

// ✅ 3. Kiểm tra malloc thành công
p = malloc(sizeof(int));
if (p == NULL) { /* xử lý lỗi */ }

// ✅ 4. Sau free, gán NULL ngay
free(p);
p = NULL;  // tránh dangling pointer

// ✅ 5. Dùng const để bảo vệ dữ liệu read-only
void print(const int *arr, int n) { /* không thể sửa arr */ }
🛠️ Công cụ phát hiện lỗi

Valgrind: Phát hiện memory leak, dangling pointer trên Linux · AddressSanitizer: Compile với -fsanitize=address · clang-tidy: Static analysis

// 12 · Capstone

Thực hành tổng hợp

● Expert

Kết hợp tất cả kiến thức: cấp phát động, con trỏ hàm, struct — xây dựng một Generic Sort tương tự qsort trong stdlib.

generic_sort.c — Capstone Project
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ── Student struct ──
typedef struct {
    char name[32];
    float gpa;
} Student;

// ── Comparators (con trỏ hàm) ──
int cmp_by_gpa(const void *a, const void *b) {
    float ga = ((const Student*)a)->gpa;
    float gb = ((const Student*)b)->gpa;
    if (ga < gb) return -1;
    if (ga > gb) return  1;
    return 0;
}

int cmp_by_name(const void *a, const void *b) {
    return strcmp(((const Student*)a)->name,
                  ((const Student*)b)->name);
}

void print_students(Student *s, int n) {
    for (int i = 0; i < n; i++)
        printf("  %-12s GPA: %.1f\n", s[i].name, s[i].gpa);
}

int main() {
    // Cấp phát mảng động
    Student *class = (Student*) malloc(4 * sizeof(Student));
    class[0] = (Student){"Alice",  3.8};
    class[1] = (Student){"Charlie",3.5};
    class[2] = (Student){"Bob",    3.9};
    class[3] = (Student){"Diana",  3.6};

    printf("--- Sắp xếp theo GPA ---\n");
    qsort(class, 4, sizeof(Student), cmp_by_gpa);
    print_students(class, 4);

    printf("--- Sắp xếp theo Tên ---\n");
    qsort(class, 4, sizeof(Student), cmp_by_name);
    print_students(class, 4);

    free(class);
    return 0;
}
--- Sắp xếp theo GPA --- Charlie GPA: 3.5 Diana GPA: 3.6 Alice GPA: 3.8 Bob GPA: 3.9 --- Sắp xếp theo Tên --- Alice GPA: 3.8 Bob GPA: 3.9 Charlie GPA: 3.5 Diana GPA: 3.6

🏆 Thử thách cuối khóa

Mở rộng dự án trên với yêu cầu:

  • Thêm hàm sắp xếp theo cả tên và GPA (multi-key sort)
  • Dùng **Student để lưu mảng con trỏ thay vì copy struct
  • Cài đặt merge sort tùy chỉnh với con trỏ hàm comparator
  • Thêm tính năng filter: chỉ giữ sinh viên GPA ≥ 3.7
  • (C++) Chuyển đổi dùng std::vectorstd::sort

🎯 Quiz tổng kết

Đoạn code sau có vấn đề gì? int *p = malloc(4); free(p); *p = 10;
✓ Sau khi free(p), bộ nhớ được trả lại cho OS. Tiếp tục ghi *p = 10 là truy cập vùng nhớ không còn sở hữu — đây là dangling pointer. Hành vi không xác định (undefined behavior)! Luôn gán p = NULL sau khi free.

📊 Tóm tắt toàn bộ

Chủ đềCú phápMức độ
Khai báoint *p;Cơ bản
Lấy địa chỉ&xCơ bản
Dereference*pCơ bản
Pointer Arithmeticp++, p+nCơ bản
Pass by referencevoid f(int *p)Trung cấp
Dynamic allocationmalloc/freeTrung cấp
Function pointerint (*fp)(int,int)Nâng cao
Double pointerint **ppNâng cao
Struct pointerNode->nextNâng cao
Smart pointerunique_ptr, shared_ptrExpert