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:
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ỉ:
1.2. Mảng và Hằng Con Trỏ¤
Tên mảng là một hằng con 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 │
└────────┴─────────┴──────────┘
↑
arr = 0x10
Lưu ý
Tên mảng là hằng con trỏ, không thể thay đổi:
2. Phép Toán Số Học Trên Con Trỏ¤
2.1. Phép Cộng¤
Quy tắc:
Ví dụ:
┌────────┬─────────┬──────────┐
│ 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:
2.2. Phép Trừ¤
Quy tắc:
Ví dụ:
┌────────┬─────────┬──────────┐
│ 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 │
└────────┴─────────┴──────────┘
2.3. Khoảng Cách Giữa Hai Con Trỏ¤
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
Phép toán không hợp lệ
KHÔNG thể thực hiện: *, /, %
3. Con Trỏ và Mảng Một Chiều¤
3.1. Gán Con Trỏ Cho Mảng¤
┌────────┬─────────┬──────────┐
│ 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ệ:
Lấy địa chỉ:
Ví 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ác lưu ý
- Không thực hiện phép nhân, chia, lấy phần dư trên con trỏ
- Tăng/giảm con trỏ n đơn vị = tăng/giảm
n × sizeof(kiểu_dữ_liệu)bytes - Không thể tăng/giảm biến mảng (vì là hằng con trỏ)
- Khi truyền mảng cho hàm, tham số mảng không phải hằng con trỏ
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
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)
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:
Ví dụ:
┌────────┬──────────┐
│ 0x34 │ ? │ ← Vùng nhớ được cấp phát
├────────┼──────────┤
│ 0x90 │ 0x34 │ ptr
└────────┴──────────┘
Sử dụng:
2.2. Khởi Tạo Giá Trị¤
2.3. Kiểm Tra Cấp Phát Thành Công¤
3. Toán Tử delete¤
3.1. Giải Phóng Bộ Nhớ¤
Cú pháp:
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
Con trỏ lạc (Dangling Pointer)
Sau khi delete, con trỏ vẫn trỏ tới vùng nhớ cũ nhưng vùng nhớ đã được giải phóng!
Giải pháp: Gán p = NULL sau khi delete
3.2. Ví Dụ Con Trỏ Lạc¤
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:
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¤
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;
Quan trọng
- Cấp phát bằng
new→ Giải phóng bằngdelete - Cấp phát bằng
new[]→ Giải phóng bằngdelete[]
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ỏ¤
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:
7. Bài Tập Bắt Buộc¤
Bài 1: Mảng động
Nhập một dãy số hữu tỉ tùy ý (dùng cấp phát động), xuất ra dãy gồm các số < 1, tính tổng và tích.
Bài 2: Sao chép mảng
Viết hàm nhập dãy số thực A (cấp phát động). Viết hàm sao chép A sang dãy B (cấp phát lại).
Bài 3: Làm lại bài tập mảng
Làm lại các bài tập về mảng 1 chiều và 2 chiều sử dụng cấp phát động.
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?