L10. Con Trỏ và Mảng Một Chiều
1. Mảng Một Chiều và Địa Chỉ
1.1. Lấy Địa Chỉ Phần Tử Mảng
Cho mảng:
int arr[6] = {5, 6, 9, 4, 1, 2};Bộ nhớ:
┌────────┬─────────┬──────────┐
│ Địa chỉ│ Phần tử │ Giá trị │
├────────┼─────────┼──────────┤
│ 0x10 │ arr[0] │ 5 │
│ 0x14 │ arr[1] │ 6 │
│ 0x18 │ arr[2] │ 9 │
│ 0x22 │ arr[3] │ 4 │
│ 0x26 │ arr[4] │ 1 │
│ 0x30 │ arr[5] │ 2 │
└────────┴─────────┴──────────┘Lấy địa chỉ:
&arr[0] // 0x10
&arr[1] // 0x14
&arr[2] // 0x18
&arr[3] // 0x22
&arr[4] // 0x26
&arr[5] // 0x30
1.2. Mảng và Hằng Con Trỏ
Tên mảng là một hằng con trỏ:
arr == &arr[0] // ĐÚNG: arr là địa chỉ đầu tiên
┌────────┬─────────┬──────────┐
│ 0x10 │ arr[0] │ 5 │
│ 0x14 │ arr[1] │ 6 │
│ 0x18 │ arr[2] │ 9 │
│ 0x22 │ arr[3] │ 4 │
│ 0x26 │ arr[4] │ 1 │
│ 0x30 │ arr[5] │ 2 │
└────────┴─────────┴──────────┘
↑
arr = 0x102. Phép Toán Số Học Trên Con Trỏ
2.1. Phép Cộng
Quy tắc:
<con_trỏ> + n → <con_trỏ> + n × sizeof(<kiểu_dữ_liệu>)Ví dụ:
int arr[6] = {5, 6, 9, 4, 1, 2};
int *p = &arr[2]; // p trỏ tới arr[2] (địa chỉ 0x18)
┌────────┬─────────┬──────────┐
│ 0x10 │ arr[0] │ 5 │
│ 0x14 │ arr[1] │ 6 │
│ 0x18 │ arr[2] │ 9 │ ← p
│ 0x22 │ arr[3] │ 4 │ ← p+1
│ 0x26 │ arr[4] │ 1 │ ← p+2
│ 0x30 │ arr[5] │ 2 │
└────────┴─────────┴──────────┘p // 0x18
p + 1 // 0x18 + 1×4 = 0x22
p + 2 // 0x18 + 2×4 = 0x26
*(p + 1) // 4 (giá trị tại 0x22)
*(p + 2) // 1 (giá trị tại 0x26)
Toán tử gộp:
p += 1; // p = p + 1
p++; // p = p + 1
2.2. Phép Trừ
Quy tắc:
<con_trỏ> - n → <con_trỏ> - n × sizeof(<kiểu_dữ_liệu>)Ví dụ:
int arr[6] = {5, 6, 9, 4, 1, 2};
int *p = &arr[4]; // p trỏ tới arr[4] (địa chỉ 0x26)
┌────────┬─────────┬──────────┐
│ 0x10 │ arr[0] │ 5 │
│ 0x14 │ arr[1] │ 6 │
│ 0x18 │ arr[2] │ 9 │ ← p-2
│ 0x22 │ arr[3] │ 4 │ ← p-1
│ 0x26 │ arr[4] │ 1 │ ← p
│ 0x30 │ arr[5] │ 2 │
└────────┴─────────┴──────────┘p // 0x26
p - 1 // 0x26 - 1×4 = 0x22
p - 2 // 0x26 - 2×4 = 0x18
2.3. Khoảng Cách Giữa Hai Con Trỏ
<con_trỏ_1> - <con_trỏ_2> // Kết quả: số phần tử
Ví dụ:
int arr[6] = {5, 6, 9, 4, 1, 2};
int *p1 = &arr[1]; // 0x14
int *p2 = &arr[5]; // 0x30
p2 - p1 // (0x30 - 0x14) / 4 = 4 phần tử
p1 - p2 // -4
2.4. Phép So Sánh
==, !=, >, >=, <, <=So sánh địa chỉ (thứ tự ô nhớ)
int arr[6] = {5, 6, 9, 4, 1, 2};
int *p1 = &arr[1];
int *p2 = &arr[5];
p1 < p2 // true (0x14 < 0x30)
p1 == p2 // false
3. Con Trỏ và Mảng Một Chiều
3.1. Gán Con Trỏ Cho Mảng
int arr[6] = {5, 6, 9, 4, 1, 2};
int *parr;
// Cách 1
parr = arr;
// Cách 2
parr = &arr[0];┌────────┬─────────┬──────────┐
│ 0x10 │ arr[0] │ 5 │
│ 0x14 │ arr[1] │ 6 │
│ 0x18 │ arr[2] │ 9 │
│ 0x22 │ arr[3] │ 4 │
│ 0x26 │ arr[4] │ 1 │
│ 0x30 │ arr[5] │ 2 │
├────────┼─────────┼──────────┤
│ 0x90 │ parr │ 0x10 │ ← Trỏ tới arr[0]
└────────┴─────────┴──────────┘3.2. Truy Xuất Qua Con Trỏ
Với chỉ số i hợp lệ:
arr[i] ≡ *(arr + i) ≡ parr[i] ≡ *(parr + i)Lấy địa chỉ:
&arr[i] ≡ arr + i ≡ &parr[i] ≡ parr + iVí dụ:
int arr[6] = {5, 6, 9, 4, 1, 2};
int *parr = arr;
// Tất cả đều truy xuất arr[2]
arr[2] // 9
*(arr + 2) // 9
parr[2] // 9
*(parr + 2) // 9
// Tất cả đều lấy địa chỉ arr[2]
&arr[2] // 0x18
arr + 2 // 0x18
&parr[2] // 0x18
parr + 2 // 0x18
3.3. Bảng Tương Đương
| Lấy giá trị | Lấy địa chỉ |
|---|---|
arr[i] | &arr[i] |
*(arr+i) | arr+i |
parr[i] | &parr[i] |
*(parr+i) | parr+i |
4. Truyền Mảng Cho Hàm
4.1. Đặc Điểm
Mảng truyền cho hàm không phải hằng con trỏ:
int main() {
int a[] = {1, 2, 3, 4, 5, 6};
// SAI: a là hằng con trỏ
for (int i = 0; i < 6; i++)
printf("%d", *(a++));
}void xuat(int *a, int n) {
// ĐÚNG: a là tham số, không phải hằng
for (int i = 0; i < n; i++)
printf("%d", *(a++));
}
int main() {
int a[] = {1, 2, 3, 4, 5, 6};
xuat(a, 6);
}4.2. Các Cách Khai Báo Tham Số
void xuat(int a[], int n); // Cách 1
void xuat(int a[100], int n); // Cách 2 (số không quan trọng)
void xuat(int *a, int n); // Cách 3 (khuyến nghị)
Ví dụ đầy đủ:
void nhap(int *a, int n) {
for (int i = 0; i < n; i++) {
cout << "Nhap a[" << i << "] = ";
cin >> *(a + i); // Hoặc cin >> a[i];
}
}
void xuat(int *a, int n) {
for (int i = 0; i < n; i++) {
cout << *(a + i) << " "; // Hoặc cout << a[i];
}
}
int main() {
int arr[100], n;
cout << "Nhap n: "; cin >> n;
nhap(arr, n);
xuat(arr, n);
}5. Bài Tập
Bài Tập 1: Nhóm Biểu Thức
Cho mảng int a[] và con trỏ int *p = a. Nhóm các biểu thức sau thành 2 nhóm:
Nhóm lấy giá trị:
a[i]*(a + i)p[i]*(p + i)
Nhóm lấy địa chỉ:
&a[i]a + i&p[i]p + i
Bài Tập 2: Gán Giá Trị Qua Con Trỏ
int a[10];
int *p = a;
// Gán giá trị 100 cho phần tử thứ 5
*(p + 5) = 100; // Cách 1
p[5] = 100; // Cách 2
a[5] = 100; // Cách 3
Bài Tập 3: Nhập/Xuất Mảng
#include <iostream>
using namespace std;
const int n = 10;
int main() {
int a[n], *p = a;
// Nhập mảng
for (int i = 0; i < n; i++) {
cin >> *(p + i);
}
// Xuất mảng
for (int i = 0; i < n; i++) {
cout << *(p + i) << " ";
}
}Bài Tập 4: Chuyển Chuỗi Sang Chữ Hoa
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char str[20] = "hello class";
char *p = str;
int n = strlen(str);
for (int i = 0; i < n; i++)
p[i] = toupper(p[i]);
cout << p; // Output: HELLO CLASS
}6. Lưu Ý Quan Trọng
Cấp Phát Động (Dynamic Memory Allocation)
1. Cấp Phát Bộ Nhớ Tĩnh vs Động
1.1. Cấp Phát Tĩnh
Đặc điểm:
- Khai báo biến, mảng với kích thước cố định
- Phải biết trước cần bao nhiêu bộ nhớ
- Không thay đổi được kích thước
- Tốn bộ nhớ nếu khai báo quá lớn
int a[1000]; // Luôn tốn 4000 bytes, dù chỉ dùng 10 phần tử
1.2. Cấp Phát Động
Đặc điểm:
- Cấp phát khi cần, giải phóng khi không dùng
- Không cần biết trước kích thước
- Có thể thay đổi kích thước
- Sử dụng vùng nhớ HEAP (và bộ nhớ ảo)
int n;
cin >> n;
int *a = new int[n]; // Cấp phát đúng n phần tử cần dùng
1.3. Cấu Trúc Chương Trình C++ Trong Bộ Nhớ
┌─────────────────────────┐
│ STACK │ ← Biến cục bộ, tham số hàm
│ (Last-In First-Out) │ Tự động quản lý
├─────────────────────────┤
│ │
│ Vùng nhớ trống │
│ │
├─────────────────────────┤
│ HEAP │ ← Cấp phát động
│ (Dùng new/delete) │ RAM + Bộ nhớ ảo
├─────────────────────────┤
│ Biến toàn cục/tĩnh │ ← Kích thước cố định
├─────────────────────────┤
│ Mã chương trình │ ← Lệnh và hằng số
└─────────────────────────┘2. Toán Tử new
2.1. Cấp Phát Cho Biến Đơn
Cú pháp:
<kiểu> *<tên_con_trỏ> = new <kiểu>;Ví dụ:
int *ptr = new int; // Cấp phát 1 biến int
┌────────┬──────────┐
│ 0x34 │ ? │ ← Vùng nhớ được cấp phát
├────────┼──────────┤
│ 0x90 │ 0x34 │ ptr
└────────┴──────────┘Sử dụng:
*ptr = 3199;
cout << *ptr; // 3199
2.2. Khởi Tạo Giá Trị
int *p = new int(99); // Khởi tạo = 99
cout << *p; // 99
2.3. Kiểm Tra Cấp Phát Thành Công
int *p = new int;
if (p == NULL) {
cout << "Error: Khong du bo nho.\n";
exit(1);
}
*p = 3199;3. Toán Tử delete
3.1. Giải Phóng Bộ Nhớ
Cú pháp:
delete <tên_con_trỏ>;Ví dụ:
int *p = new int(100);
cout << *p; // 100
delete p; // Giải phóng bộ nhớ
p = NULL; // Tránh con trỏ lạc
3.2. Ví Dụ Con Trỏ Lạc
int *p1, *p2;
p1 = new int;
*p1 = 30;
p2 = p1;
*p2 = 40;
p1 = new int;
*p1 = 50;Truy vết:
Bước 1: p1 = new int
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └────┘
│
↓
[?]
Bước 2: *p1 = 30
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └────┘
│
↓
[30]
Bước 3: p2 = p1
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └─┬──┘
│ │
└──┬───┘
↓
[30]
Bước 4: *p2 = 40
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └─┬──┘
│ │
└──┬───┘
↓
[40]
Bước 5: p1 = new int
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └─┬──┘
│ │
↓ ↓
[?] [40] ← Bị mất tham chiếu (memory leak)
Bước 6: *p1 = 50
┌────┐ ┌────┐
│ p1 │ │ p2 │
└─┬──┘ └─┬──┘
│ │
↓ ↓
[50] [40]4. Mảng Động Một Chiều
4.1. Cấp Phát Mảng Động
Cú pháp:
<kiểu> *<tên_con_trỏ> = new <kiểu>[<số_phần_tử>];Ví dụ:
int n;
cout << "Nhap n: ";
cin >> n;
int *a = new int[n]; // Cấp phát mảng n phần tử
// Sử dụng như mảng thường
for (int i = 0; i < n; i++) {
a[i] = i + 1;
}4.2. Xóa Mảng Động
delete[] <tên_con_trỏ>;Ví dụ:
int *a = new int[10];
// Sử dụng mảng
for (int i = 0; i < 10; i++) {
a[i] = i;
}
// Giải phóng
delete[] a; // Chú ý: delete[]
a = NULL;4.3. Hàm Trả Về Mảng Động
Cách 1: Trả về con trỏ
int* NhapMang(int n) {
int *p = new int[n];
for (int i = 0; i < n; i++) {
cin >> p[i];
}
return p;
}
int main() {
int n;
cin >> n;
int *arr = NhapMang(n);
// Sử dụng arr
delete[] arr; // Nhớ giải phóng
}Cách 2: Truyền tham chiếu
void NhapMang(int*& p, int n) {
p = new int[n];
for (int i = 0; i < n; i++) {
cin >> p[i];
}
}
int main() {
int *arr, n;
cin >> n;
NhapMang(arr, n);
// Sử dụng arr
delete[] arr;
}5. Mảng Động Hai Chiều
5.1. Cấp Phát Ma Trận
int m, n;
cin >> m >> n;
// Cấp phát mảng con trỏ
int **a = new int*[m];
// Cấp phát từng dòng
for (int i = 0; i < m; i++) {
a[i] = new int[n];
}Minh họa:
a → [ptr0] → [a00][a01][a02]...[a0n]
[ptr1] → [a10][a11][a12]...[a1n]
[ptr2] → [a20][a21][a22]...[a2n]
...
[ptrm] → [am0][am1][am2]...[amn]5.2. Sử Dụng Ma Trận
// Nhập
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cin >> a[i][j];
}
}
// Xuất
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}5.3. Giải Phóng Ma Trận
// Giải phóng từng dòng
for (int i = 0; i < m; i++) {
delete[] a[i];
}
// Giải phóng mảng con trỏ
delete[] a;
a = NULL;5.4. Chương Trình Hoàn Chỉnh
#include <iostream>
using namespace std;
int main() {
int m, n;
cout << "Nhap so dong, so cot: ";
cin >> m >> n;
// Cấp phát
int **a = new int*[m];
for (int i = 0; i < m; i++) {
a[i] = new int[n];
}
// Nhập
cout << "Nhap ma tran:\n";
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cin >> a[i][j];
}
}
// Xuất
cout << "Ma tran vua nhap:\n";
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
// Giải phóng
for (int i = 0; i < m; i++) {
delete[] a[i];
}
delete[] a;
return 0;
}6. Typedef Với Con Trỏ
typedef int* IntPtr;
IntPtr p; // int *p;
IntPtr p1, p2; // int *p1, *p2;
Ví dụ sử dụng:
typedef int* IntPointer;
void Input(IntPointer temp) {
*temp = 20;
cout << "Trong ham: *temp = " << *temp << endl;
}
int main() {
IntPointer p = new int;
*p = 10;
cout << "Truoc khi goi: *p = " << *p << endl;
Input(p);
cout << "Sau khi goi: *p = " << *p << endl;
delete p;
}Output:
Truoc khi goi: *p = 10
Trong ham: *temp = 20
Sau khi goi: *p = 207. Bài Tập Bắt Buộc
Bây giờ đã đầy đủ hơn! Còn thiếu phần Tập tin (File I/O). Tôi tiếp tục bổ sung không?