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:
- Tìm kiếm địa chỉ ô nhớ của biến
- Truy xuất hoặc thiết lập giá trị tại địa chỉ đó
Ví dụ:
int main() {
char ch = 'x';
int a = 7;
}Memory Layout
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ ... │ │ │
│ 0x10 │ ch │ 'x' │
│ 0x11 │ │ │
│ 0x12 │ │ │
│ 0x13 │ │ │
│ 0x14 │ a │ 7 │
│ 0x15 │ a │ │
│ 0x16 │ a │ │
│ 0x17 │ a │ │
│ ... │ │ │
└────────┴──────────┴──────────┘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.
&<tên_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ỉ đó.
*(<địa_chỉ>)Ví dụ:
int value = 3200;┌────────┬──────────┬──────────┐
│ Đị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.
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
<kiểu_dữ_liệu> *<tên_biến_con_trỏ>;Ví dụ:
char char1;
int *ptrI;
float *ptrF;┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x10 │ ptrF │ ? │
│ ... │ │ │
│ 0x50 │ char1 │ ? │
│ ... │ │ │
│ 0x80 │ ptrI │ ? │
└────────┴──────────┴──────────┘3.2. Khởi Tạo Con Trỏ
Cú pháp:
<kiểu_dữ_liệu> *<tên_con_trỏ> = &<tên_biến>;Ví dụ đúng:
int a;
int *ptr = &a; // ĐÚNG: Con trỏ int trỏ tới biến int
┌────────┬──────────┬──────────┐
│ Địa chỉ│ Biến │ Giá trị │
├────────┼──────────┼──────────┤
│ 0x34 │ a │ ? │
│ ... │ │ │
│ 0x90 │ ptr │ 0x34 │
└────────┴──────────┴──────────┘Ví dụ sai:
double a;
int *ptr = &a; // SAI: Không khớp kiểu!
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))= 31995. Phép Gán Con Trỏ
5.1. Gán Con Trỏ Cho Con Trỏ
int *p1, *p2;
p2 = p1; // p2 trỏ tới nơi mà p1 đang trỏ
Ví dụ:
int x = 27, y = 5;
int *p1 = &x;
int *p2 = &y;
p1 = p2; // p1 bây giờ trỏ tới y
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ỏ
*p2 = *p1; // Gán giá trị trỏ bởi p1 cho giá trị trỏ bởi p2
Ví dụ:
int x = 27, y = 5;
int *p1 = &x;
int *p2 = &y;
*p1 = *p2; // x = y = 5
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
└────────┴──────┘ └────────┴──────┘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:
int *ptr = NULL;
if (ptr == NULL) {
cout << "Con tro chua tro toi bien nao";
}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:
sizeof(<kiểu_dữ_liệu>)
sizeof(<tên_biến>)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*) = 48. Từ Khóa const và Con Trỏ
8.1. Con Trỏ Thường
int x;
int *p1 = &x; // Non-const pointer to non-const int
- Có thể thay đổi địa chỉ:
p1 = &y;✓ - Có thể thay đổi giá trị:
*p1 = 10;✓
8.2. Con Trỏ Tới Hằng
const int *p2 = &x; // Non-const pointer to const int
- Có thể thay đổi địa chỉ:
p2 = &y;✓ - KHÔNG thể thay đổi giá trị:
*p2 = 10;✗
8.3. Con Trỏ Hằng
int * const p3 = &x; // Const pointer to non-const int
- 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
const int * const p4 = &x; // Const pointer to const int
- 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;
*pa ≡ a // Cùng chỉ nội dung của biến a
pa ≡ &a // Cùng chỉ địa chỉ của biến a
10.2. Lỗi Thường Gặp
Lỗi 1: Sử dụng con trỏ chưa khởi tạo
int *pa;
*pa = 1904; // SAI! pa chưa trỏ vào đâu
Đúng:
int a;
int *pa = &a;
*pa = 1904; // ĐÚNG
Lỗi 2: Không khớp kiểu
double a;
int *ptr = &a; // SAI! 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
int main() {
int x, *p;
x = 10;
*p = x; // LỖI: p chưa khởi tạo!
return 0;
}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;
}