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.
Bộ nhớ máy tính hoạt động thế nào?
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:
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.
Con trỏ là gì?
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
#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; }
Sơ đồ bộ nhớ minh họa
con trỏ p tại địa chỉ 0x2000 lưu giá trị 0x1000 — chính là địa chỉ của x
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
int a = 10; int *q = &a; — giá trị của *q là?Khai báo & Toán tử
| Cú pháp | Ý nghĩa | Ví dụ |
|---|---|---|
| type *name | Khai báo con trỏ | int *p; |
| &variable | Lấy địa chỉ của biến | p = &x; |
| *pointer | Dereference (đọc/ghi giá trị) | *p = 99; |
| NULL / nullptr | Con trỏ rỗng (không trỏ đâu) | p = NULL; |
Ghi qua con trỏ
#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; }
Pointer Arithmetic — Cộng trừ địa chỉ
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]
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:
Con trỏ & Mảng
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 và &arr[0] có cùng giá trị!
#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; }
Sơ đồ — Mảng trong bộ nhớ
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)
Pass by Reference — Con trỏ & Hàm
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
#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; }
Trả về nhiều giá trị qua con trỏ
// 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); }
✏️ 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ỏ.
Con trỏ & Chuỗi
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.
#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); }
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.
Cấp phát bộ nhớ động
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.
#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àm | Mô tả | Khởi tạo |
|---|---|---|
| malloc(size) | Cấp phát size bytes | Không (garbage) |
| calloc(n, size) | Cấp phát n×size bytes | Có (zeros) |
| realloc(ptr, size) | Thay đổi kích thước | — |
| free(ptr) | Giải phóng bộ nhớ | — |
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.
Con trỏ Hàm
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.
#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; }
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.
Con trỏ đến Con trỏ (**ptr)
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.
#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
Ứng dụng: Mảng 2D động
#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; }
Con trỏ & Struct — Linked List
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ỏ.
#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); }
✏️ Thử thách
Mở rộng linked list với các hàm:
pop(Node **head)— xóa phần tử đầusearch(Node *head, int val)— tìm kiếmreverse(Node **head)— đảo ngược danh sáchfree_list(Node **head)— giải phóng toàn bộ bộ nhớ
Smart Pointers trong C++
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>
#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 }
Những lỗi nguy hiểm nhất
🔴 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!
🔴 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.
🔴 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!
🔴 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.
🔴 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
// ✅ 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 */ }
Valgrind: Phát hiện memory leak, dangling pointer trên Linux · AddressSanitizer: Compile với -fsanitize=address · clang-tidy: Static analysis
Thực hành tổng hợp
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.
#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; }
🏆 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::vectorvàstd::sort
🎯 Quiz tổng kết
int *p = malloc(4); free(p); *p = 10;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áp | Mức độ |
|---|---|---|
| Khai báo | int *p; | Cơ bản |
| Lấy địa chỉ | &x | Cơ bản |
| Dereference | *p | Cơ bản |
| Pointer Arithmetic | p++, p+n | Cơ bản |
| Pass by reference | void f(int *p) | Trung cấp |
| Dynamic allocation | malloc/free | Trung cấp |
| Function pointer | int (*fp)(int,int) | Nâng cao |
| Double pointer | int **pp | Nâng cao |
| Struct pointer | Node->next | Nâng cao |
| Smart pointer | unique_ptr, shared_ptr | Expert |