L9. Con Trỏ Cơ Bản (Pointer)¤
1. Biến và Vùng Nhớ¤
1.1. Bộ Nhớ Máy Tính¤
Cấu trúc bộ nhớ RAM: - Bộ nhớ RAM chứa rất nhiều ô nhớ - Mỗi ô nhớ có kích thước 1 byte - Mỗi ô nhớ có địa chỉ duy nhất - Địa chỉ được đánh số từ 0 trở đi
Memory Layout (bytes)
┌────────┬──────────┐
│ Địa chỉ│ Giá trị │
├────────┼──────────┤
│ 0 │ │
│ 1 │ │
│ 2 │ │
│ 3 │ │
│ 4 │ │
│ 5 │ │
│ 6 │ │
│ 7 │ │
│ ... │ ... │
└────────┴──────────┘
1.2. Biến và Vùng Nhớ¤
Khi khai báo biến: - Máy tính dành riêng một vùng nhớ để lưu biến đó - Kích thước vùng nhớ phụ thuộc vào kiểu dữ liệu
Khi gọi tên biến: 1. Tìm kiếm địa chỉ ô nhớ của biến 2. Truy xuất hoặc thiết lập giá trị tại địa chỉ đó
Ví dụ:
Memory Layout
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ ... │ │ │
│ 0x10 │ ch │ 'x' │
│ 0x11 │ │ │
│ 0x12 │ │ │
│ 0x13 │ │ │
│ 0x14 │ a │ 7 │
│ 0x15 │ a │ │
│ 0x16 │ a │ │
│ 0x17 │ a │ │
│ ... │ │ │
└────────┴──────────┴──────────┘
Lưu ý
charchiếm 1 byte (0x10)intchiếm 4 bytes (0x14-0x17)
1.3. Toán Tử & và *¤
Toán tử & (Address-of Operator):
Đặt trước tên biến và cho biết địa chỉ của vùng nhớ của biến.
Toán tử * (Dereferencing/Indirection Operator):
Đặt trước một địa chỉ và cho biết giá trị lưu trữ tại địa chỉ đó.
Ví dụ:
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x50 │ value │ 3200 │
└────────┴──────────┴──────────┘
cout << "value = " << value;
// Output: value = 3200
cout << "&value = " << &value;
// Output: &value = 0x50
cout << "*(&value) = " << *(&value);
// Output: *(&value) = 3200
2. Khái Niệm Con Trỏ¤
2.1. Định Nghĩa¤
Con trỏ (Pointer):
Là một biến lưu trữ địa chỉ của một địa chỉ bộ nhớ. Địa chỉ này thường là địa chỉ của một biến khác.
graph LR
A[Biến con trỏ x] -->|chứa địa chỉ| B[Biến y]
A -.->|"ta nói x 'trỏ tới' y"| B
Ví dụ minh họa:
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x34 │ y │ 100 │
│ ... │ │ │
│ 0x90 │ x │ 0x34 │ ← x trỏ tới y
└────────┴──────────┴──────────┘
2.2. Phân Loại Con Trỏ¤
Con trỏ được phân loại theo kiểu dữ liệu mà nó trỏ tới:
- Con trỏ kiểu
int: Trỏ tới biến kiểuint - Con trỏ kiểu
float: Trỏ tới biến kiểufloat - Con trỏ kiểu
char: Trỏ tới biến kiểuchar - Con trỏ kiểu
double: Trỏ tới biến kiểudouble
3. Khai Báo Con Trỏ¤
3.1. Cú Pháp Khai Báo¤
Ví dụ:
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x10 │ ptrF │ ? │
│ ... │ │ │
│ 0x50 │ char1 │ ? │
│ ... │ │ │
│ 0x80 │ ptrI │ ? │
└────────┴──────────┴──────────┘
Lưu ý
Con trỏ chưa khởi tạo có giá trị không xác định!
3.2. Khởi Tạo Con Trỏ¤
Cú pháp:
Ví dụ đúng:
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x34 │ a │ ? │
│ ... │ │ │
│ 0x90 │ ptr │ 0x34 │
└────────┴──────────┴──────────┘
Ví dụ sai:
Quy tắc quan trọng
Kiểu của con trỏ phải khớp với kiểu của biến mà nó trỏ tới!
4. Con Trỏ và Toán Tử & , *¤
4.1. Sử Dụng Toán Tử *¤
Truy xuất giá trị:
int a = 1000;
int *ptr = &a;
cout << ptr; // 0x34 (địa chỉ của a)
cout << *ptr; // 1000 (giá trị của a)
Thay đổi giá trị:
*ptr = 3200; // Thay đổi giá trị của a
cout << a; // 3200
cout << *ptr; // 3200
(*ptr)++; // Tăng giá trị của a lên 1
cout << a; // 3201
Trước: Sau:
┌────────┬──────┐ ┌────────┬──────┐
│ 0x34 │ 1000 │ a │ 0x34 │ 3201 │ a
├────────┼──────┤ ├────────┼──────┤
│ 0x90 │ 0x34 │ ptr │ 0x90 │ 0x34 │ ptr
└────────┴──────┘ └────────┴──────┘
4.2. Ví Dụ Truy Vết¤
#include <iostream>
using namespace std;
int main() {
int a;
int *ptr;
int value;
a = 3200;
ptr = &a;
value = --(*ptr);
}
Truy vết:
Bước 1: a = 3200
┌────────┬──────┐
│ 0x34 │ 3200 │ a
├────────┼──────┤
│ 0x90 │ ? │ ptr
├────────┼──────┤
│ 0x50 │ ? │ value
└────────┴──────┘
Bước 2: ptr = &a
┌────────┬──────┐
│ 0x34 │ 3200 │ a
├────────┼──────┤
│ 0x90 │ 0x34 │ ptr
├────────┼──────┤
│ 0x50 │ ? │ value
└────────┴──────┘
Bước 3: value = --(*ptr)
┌────────┬──────┐
│ 0x34 │ 3199 │ a ← Giảm 1
├────────┼──────┤
│ 0x90 │ 0x34 │ ptr
├────────┼──────┤
│ 0x50 │ 3199 │ value ← Gán giá trị mới
└────────┴──────┘
Các giá trị:
value = 3199
ptr = 0x34
a = 3199
&value = 0x50
&ptr = 0x90
&a = 0x34
*ptr = 3199
&(*ptr) = 0x34
*(*ptr) = ERROR! (3199 không phải địa chỉ)
*(&(*ptr))= 3199
5. Phép Gán Con Trỏ¤
5.1. Gán Con Trỏ Cho Con Trỏ¤
Ví dụ:
Trước: Sau:
┌────────┬──────┐ ┌────────┬──────┐
│ addr_x │ 27 │ x │ addr_x │ 27 │ x
├────────┼──────┤ ├────────┼──────┤
│ addr_y │ 5 │ y │ addr_y │ 5 │ y
├────────┼──────┤ ├────────┼──────┤
│ │addr_x│ p1 │ │addr_y│ p1 ← Thay đổi
├────────┼──────┤ ├────────┼──────┤
│ │addr_y│ p2 │ │addr_y│ p2
└────────┴──────┘ └────────┴──────┘
5.2. Gán Giá Trị Qua Con Trỏ¤
Ví dụ:
Trước: Sau:
┌────────┬──────┐ ┌────────┬──────┐
│ addr_x │ 27 │ x │ addr_x │ 5 │ x ← Thay đổi
├────────┼──────┤ ├────────┼──────┤
│ addr_y │ 5 │ y │ addr_y │ 5 │ y
├────────┼──────┤ ├────────┼──────┤
│ │addr_x│ p1 │ │addr_x│ p1
├────────┼──────┤ ├────────┼──────┤
│ │addr_y│ p2 │ │addr_y│ p2
└────────┴──────┘ └────────┴──────┘
Phân biệt
p1 = p2: Gán con trỏ (thay đổi địa chỉ)*p1 = *p2: Gán giá trị (thay đổi nội dung)
6. Con Trỏ NULL¤
6.1. Khái Niệm¤
Con trỏ NULL:
Con trỏ không trỏ vào đâu cả.
int n;
int *p1 = &n; // p1 trỏ tới n
int *p2; // p2 chưa khởi tạo (nguy hiểm!)
int *p3 = NULL; // p3 không trỏ vào đâu (an toàn)
Sử dụng:
Khuyến nghị
- Luôn khởi tạo con trỏ = NULL nếu chưa sử dụng
- Kiểm tra con trỏ != NULL trước khi dùng
6.2. Con Trỏ Chưa Khởi Tạo vs NULL¤
int *p1; // unreferenced local variable (nguy hiểm)
int *p2 = NULL; // an toàn, biết rằng chưa trỏ vào đâu
7. Kích Thước Con Trỏ¤
Toán tử sizeof:
Ví dụ:
int a;
double b;
char c;
int *pa;
double *pb;
char *pc;
// Kích thước biến
sizeof(a) = 4 sizeof(int) = 4
sizeof(b) = 8 sizeof(double) = 8
sizeof(c) = 1 sizeof(char) = 1
// Kích thước con trỏ
sizeof(pa) = 4 sizeof(int*) = 4
sizeof(pb) = 4 sizeof(double*) = 4
sizeof(pc) = 4 sizeof(char*) = 4
Quan trọng
Mọi con trỏ đều có cùng kích thước (4 bytes trên hệ 32-bit, 8 bytes trên 64-bit) vì chúng chỉ lưu địa chỉ!
8. Từ Khóa const và Con Trỏ¤
8.1. Con Trỏ Thường¤
- Có thể thay đổi địa chỉ:
p1 = &y;✓ - Có thể thay đổi giá trị:
*p1 = 10;✓
8.2. Con Trỏ Tới Hằng¤
- Có thể thay đổi địa chỉ:
p2 = &y;✓ - KHÔNG thể thay đổi giá trị:
*p2 = 10;✗
8.3. Con Trỏ Hằng¤
- KHÔNG thể thay đổi địa chỉ:
p3 = &y;✗ - Có thể thay đổi giá trị:
*p3 = 10;✓
8.4. Con Trỏ Hằng Tới Hằng¤
- KHÔNG thể thay đổi địa chỉ:
p4 = &y;✗ - KHÔNG thể thay đổi giá trị:
*p4 = 10;✗
Bảng tổng hợp:
| Loại | Thay đổi địa chỉ | Thay đổi giá trị |
|---|---|---|
int *p |
✓ | ✓ |
const int *p |
✓ | ✗ |
int * const p |
✗ | ✓ |
const int * const p |
✗ | ✗ |
9. Con Trỏ và Hàm¤
9.1. Truyền Tham Số Bằng Con Trỏ¤
Cách 1: Truyền giá trị (không thay đổi)
int NhapGiaTri() {
int b;
cout << "Nhap gia tri vao: ";
cin >> b;
return b;
}
int main() {
int a;
a = NhapGiaTri();
cout << a;
}
Cách 2: Truyền con trỏ (có thể thay đổi)
void NhapGiaTri(int *b) {
cout << "Nhap gia tri vao: ";
cin >> *b;
}
int main() {
int a;
NhapGiaTri(&a);
cout << a;
}
Cách 3: Truyền tham chiếu
void NhapGiaTri(int &b) {
cout << "Nhap gia tri vao: ";
cin >> b;
}
int main() {
int a;
NhapGiaTri(a);
cout << a;
}
9.2. Hàm Hoán Vị¤
Sử dụng con trỏ:
void Swap(int *a, int *b) {
int temp = *b;
*b = *a;
*a = temp;
}
int main() {
int x = 7, y = 8;
Swap(&x, &y);
cout << "x = " << x << ", y = " << y;
// Output: x = 8, y = 7
}
Sử dụng tham chiếu:
void Swap(int &a, int &b) {
int temp = b;
b = a;
a = temp;
}
int main() {
int x = 7, y = 8;
Swap(x, y);
cout << "x = " << x << ", y = " << y;
// Output: x = 8, y = 7
}
10. Một Số Lưu ݤ
10.1. Quy Tắc Quan Trọng¤
Với int a, *pa = &a;
10.2. Lỗi Thường Gặp¤
Lỗi 1: Sử dụng con trỏ chưa khởi tạo
Đúng:
Lỗi 2: Không khớp kiểu
10.3. Khuyến Nghị¤
- Không dùng con trỏ khi chưa khởi tạo
- Khởi tạo con trỏ = NULL nếu chưa dùng
- Kiểm tra con trỏ != NULL trước khi dùng
- Đảm bảo kiểu con trỏ khớp với kiểu biến
11. Bài Tập¤
Bài Tập 1: Tìm Lỗi¤
Sửa:
int main() {
int x, *p;
x = 10;
p = &x; // Khởi tạo p trước
*p = 20; // Thay đổi giá trị x
return 0;
}
Bài Tập 2: Truy Vết¤
#include <iostream>
using namespace std;
int main() {
int i = 12;
int *p1;
p1 = &i;
*p1 = 24;
cout << *p1 << " " << i << endl;
}
Output: 24 24
Bài Tập 3: Hoán Vị Hai Số¤
Đề bài: Viết hàm hoán đổi giá trị của 2 tham số
Giải:
// Cách 1: Con trỏ
void Swap(int *a, int *b) {
int temp = *b;
*b = *a;
*a = temp;
}
int main() {
int x = 7, y = 8;
Swap(&x, &y);
cout << "x = " << x << ", y = " << y;
}
// Cách 2: Tham chiếu
void Swap(int &a, int &b) {
int temp = b;
b = a;
a = temp;
}
int main() {
int x = 7, y = 8;
Swap(x, y);
cout << "x = " << x << ", y = " << y;
}
Tổng kết Con Trỏ Cơ Bản
Khái niệm:
- Con trỏ lưu địa chỉ của biến
- "Trỏ tới" nghĩa là chứa địa chỉ
Toán tử:
&: Lấy địa chỉ*: Lấy giá trị tại địa chỉ
Khai báo:
int *p;- Khai báo con trỏp = &a;- Khởi tạo con trỏ*p = 10;- Thay đổi giá trị
Con trỏ NULL:
- Không trỏ vào đâu
- Luôn khởi tạo = NULL nếu chưa dùng
const và con trỏ:
const int *p: Con trỏ tới hằngint * const p: Con trỏ hằng
Truyền cho hàm:
- Cho phép hàm thay đổi biến gốc
- Tiết kiệm bộ nhớ với dữ liệu lớn
Chương tiếp theo: Con trỏ và Mảng