Tại sao a[i][j] tương đương với ((a+i)+j) trong C/C++
Nếu bạn đã quen thuộc với các kiến thức nền tảng, bạn có thể nhảy thẳng tới Chương ba.
Khám phá ban đầu: Các phép toán +, -, ++, – đối với con trỏ
Ảnh hưởng của phép tăng tự động đến địa chỉ mà con trỏ đang trỏ tới
Hãy biên dịch đoạn mã sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3;
int main() {
int a[M] = {1, 2, 3};
int *p = a;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới
cout << *p << endl;
// Thực hiện phép tăng tự động cho p
p++;
// In ra địa chỉ mà con trỏ p đang trỏ tới sau khi tăng
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới sau khi tăng
cout << *p << endl;
}
|
Kết quả chạy (địa chỉ đầu ra sẽ thay đổi tùy theo tình huống thực tế):
1
2
3
4
|
0x3a2f7ff93c
1
0x3a2f7ff940
2
|
Chúng ta có thể thấy rằng, đầu ra hiển thị giá trị tương ứng với a[0]
và a[1]
, cũng như hai địa chỉ. Hai địa chỉ bắt đầu bằng 0x
, cho thấy chúng là số thập lục phân. Tính toán 3a2f7ff940 - 3a2f7ff93c
trong hệ thập phân sẽ cho kết quả là 4, và 4 Byte chính xác là kích thước của kiểu dữ liệu int
trong môi trường biên dịch của tôi (môi trường: x86_64-w64-mingw32, phiên bản gcc: 13.2.0).
Từ đây, chúng ta dễ dàng rút ra kết luận: phép tăng tự động trên con trỏ sẽ làm tăng địa chỉ bộ nhớ mà nó đang trỏ tới một số x. Số này x chính là kích thước của kiểu dữ liệu mà con trỏ đang trỏ tới.
Tương tự, chúng ta cũng có thể suy ra ảnh hưởng của các phép toán +, -, – đối với địa chỉ mà con trỏ đang trỏ tới.
Nghiên cứu sâu hơn: Mảng và con trỏ
Mảng một chiều
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3;
int main() {
int a[M] = {1, 2, 3};
int *p = a;
// In ra địa chỉ mà mảng a đang trỏ tới
cout << a << endl;
// In ra giá trị mà mảng a đang trỏ tới
cout << *a << endl;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới
cout << *p << endl;
}
|
Kết quả chạy (địa chỉ đầu ra sẽ thay đổi tùy theo tình huống thực tế):
1
2
3
4
|
0x2fafbffd9c
1
0x2fafbffd9c
1
|
Từ đoạn mã trên, chúng ta có thể nhận thấy rằng mặc dù biến a
không phải là một con trỏ, nhưng trong nhiều trường hợp, tên của mảng a
có thể được sử dụng như một con trỏ trỏ tới phần tử đầu tiên của mảng. Mỗi phần tử của mảng a
lưu trữ dữ liệu kiểu int
, và biến p
là kiểu int *
(int *
biểu thị một con trỏ trỏ tới biến kiểu int
).
Mảng hai chiều
Tiếp theo, hãy thử biên dịch đoạn mã sau:
1
2
3
4
5
6
7
8
9
10
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int *p = a;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
return 0;
}
|
Bạn sẽ nhận được lỗi sau:
1
2
3
4
5
6
|
Đường dẫn tập tin\Tên tập tin.cpp: In function 'int main()':
Đường dẫn tập tin\Tên tập tin.cpp:8:14: error: cannot convert 'int (*)[4]' to 'int*' in initialization
8 | int *p = a;
| ^
| |
| int (*)[4]
|
Phiên bản đúng có thể biên dịch:
1
2
3
4
5
6
7
8
9
10
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[N] = a;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
return 0;
}
|
Rõ ràng, mỗi phần tử của mảng a
lưu trữ dữ liệu kiểu “mảng gồm 4 phần tử kiểu int”, và biến p
là kiểu int (*)[4]
(int (*)[4]
biểu thị một con trỏ trỏ tới “mảng gồm 4 phần tử kiểu int”). Việc gán a
cho int *
sẽ không qua được biên dịch do vấn đề về kiểu dữ liệu.
Một số bạn có thể nghĩ rằng nếu a
có thể được gán cho biến kiểu int (*)[4]
, thì *a
hẳn có thể được gán cho biến kiểu int *
. Chúng ta thử biên dịch đoạn mã sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[N] = a;
int *p2 = *a;
int v = **a;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới
cout << *p << endl;
// In ra địa chỉ mà con trỏ p2 đang trỏ tới
cout << p2 << endl;
// In ra giá trị mà con trỏ p2 đang trỏ tới
cout << *p2 << endl;
// In ra giá trị của biến v
cout << v << endl;
return 0;
}
|
Kết quả chạy (địa chỉ đầu ra sẽ thay đổi tùy theo tình huống thực tế):
1
2
3
4
5
|
0xd5e61ffc20
0xd5e61ffc20
0xd5e61ffc20
1
1
|
Như vậy, suy đoán của các bạn là đúng, đoạn mã đã biên dịch và chạy thành công.
Vậy phép tăng tự động trên mảng hai chiều hoạt động như thế nào? Thực tế, từ kết luận ở Chương một, chúng ta có thể suy ra kết quả thực tế. Hãy kiểm chứng kết luận này.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <bits/stdc++.h>
using namespace std;
int main() {
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int(*p)[4] = a;
// In ra địa chỉ mà con trỏ p đang trỏ tới
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới
cout << *p << endl;
// Thực hiện phép tăng tự động cho p
p++;
// In ra địa chỉ mà con trỏ p đang trỏ tới sau khi tăng
cout << p << endl;
// In ra giá trị mà con trỏ p đang trỏ tới sau khi tăng
cout << *p << endl;
}
|
Kết quả chạy (địa chỉ đầu ra sẽ thay đổi tùy theo tình huống thực tế):
1
2
3
4
|
0xb9ff5ff6b0
0xb9ff5ff6b0
0xb9ff5ff6c0
0xb9ff5ff6c0
|
Ta có thể thấy rằng địa chỉ mà con trỏ đang trỏ tới đã tăng lên b9ff5ff6c0 - b9ff5ff6b0
bằng 16 trong hệ thập phân. Điều này phù hợp với kết luận ở Chương một: phép tăng tự động trên con trỏ sẽ làm tăng địa chỉ bộ nhớ mà nó đang trỏ tới một số x. Số này x chính là kích thước của kiểu dữ liệu mà con trỏ đang trỏ tới.
(int (*)[4]
biểu thị một con trỏ trỏ tới “mảng gồm 4 phần tử kiểu int”) Trong ví dụ này, biến p
là kiểu int (*)[4]
, tức là nó trỏ tới dữ liệu kiểu “mảng gồm 4 phần tử kiểu int”. Kích thước của kiểu dữ liệu này là 4×kích thước của kiểu int
, tức là 4×4Byte = 16Byte. Kết quả thực tế cũng cho thấy tăng thêm 16.
Phân tích: Tại sao a[i][j] tương đương với ((a+i)+j)
Trong chương này, tôi sẽ giải thích chi tiết cách hoạt động của *(*(a+i)+j)
thông qua việc phân tích từng bước trong đoạn mã dưới đây:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include <bits/stdc++.h>
using namespace std;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int i = 2, j = 3;
// In ra giá trị của a[i][j]
cout << a[i][j] << endl; // Nên là 12
// In ra giá trị của *(*(a+i)+j)
cout << *(*(a + i) + j) << endl; // Nên là 12
// Phân tích *(*(a+i)+j)
// Gán giá trị cho p để thực hiện các phép toán
int(*p)[N] = a;
// Bước một
p += i; // Tương ứng với a+i bên trong
// Bước hai
int *q = *p; // Tương ứng với phép toán *( ) của *(a+i)
// Bước ba
q += j; // Tương ứng với phép toán +j của *(a+i)+j
// Bước bốn
int res = *q; // Tương ứng với phép toán *( ) ngoài cùng của *(*(a+i)+j)
// In ra kết quả của phép toán
cout << res << endl; // Nên là 12
return 0;
}
|
Sau đây, tôi sẽ bổ sung giải thích chi tiết hơn dựa trên đoạn mã trên. Vui lòng chú ý tới các chú thích trong mã.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int M = 3, N = 4;
int main() {
int a[M][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int i = 2, j = 3;
cout << boolalpha; // Đặt chế độ in trực tiếp true hoặc false thay vì 1 hoặc 0
// In ra giá trị của a[i][j]
cout << a[i][j] << endl; // Nên là 12
// In ra giá trị của *(*(a+i)+j)
cout << *(*(a + i) + j) << endl; // Nên là 12
// Phân tích *(*(a+i)+j)
// Gán giá trị cho p để thực hiện các phép toán
int(*p)[N] = a;
// Tại đây p trỏ tới a[0]
int(*temp)[N] = p; // Biến tạm, dùng để lưu địa chỉ mà p đang trỏ tới
// Bước một
p += i; // Tương ứng với a+i bên trong
// Tại đây p sau khi tăng sẽ trỏ tới a[i]
// Địa chỉ mà p trỏ tới sau khi tăng sẽ tăng thêm i×(N×kích thước của int)
// Kiểm tra giá trị tăng của địa chỉ mà p trỏ tới có phải là i×(N×kích thước của int) không
cout << (ull(p) - ull(temp) == i * (N * sizeof(int))) << endl; // Nên là true
// Bước hai
int *q = *p; // Tương ứng với phép toán *( ) của *(a+i)
// Tại đây q trỏ tới a[i][0]
// Kiểm tra giá trị mà q trỏ tới có phải là a[i][0] không
cout << (*q == a[i][0]) << endl; // Nên là true
int *temp2 = q; // Biến tạm, dùng để lưu địa chỉ mà q đang trỏ tới
// Bước ba
q += j; // Tương ứng với phép toán +j của *(a+i)+j
// Tại đây q sau khi tăng sẽ trỏ tới a[i][j]
// Địa chỉ mà q trỏ tới sau khi tăng sẽ tăng thêm j×kích thước của int
// Kiểm tra giá trị tăng của địa chỉ mà q trỏ tới có phải là j×kích thước của int không
cout << (ull(q) - ull(temp2) == j * sizeof(int)) << endl; // Nên là true
// Kiểm tra giá trị mà q trỏ tới có phải là a[i][j] không
cout << (*q == a[i][j]) << endl; // Nên là true
// Bước bốn
int res = *q; // Tương ứng với phép toán *( ) ngoài cùng của *(*(a+i)+j)
// In ra kết quả của phép toán
cout << res << endl; // Nên là 12
return 0;
}
|
Kết quả chạy:
1
2
3
4
5
6
7
|
12
12
true
true
true
true
12
|
Tổng kết
- Trong hầu hết các ngữ cảnh, biểu thức kiểu mảng sẽ trải qua chuyển đổi ngầm thành con trỏ trỏ tới phần tử đầu tiên của mảng. Chi tiết xem tại mục Chuyển đổi mảng sang con trỏ.
- Tham khảo thêm:
- cppreference.com: Chuyển đổi mảng sang con trỏ
- cppreference.com: Toán tử số học - Toán tử cộng tính - Toán học con trỏ
- cppreference.com: Toán tử truy cập thành viên
Theo định nghĩa, toán tử chỉ mục E1[E2]
hoàn toàn tương đương với *((E1)+(E2))
. Nếu biểu thức con trỏ là một biểu thức mảng, nó sẽ bị chuyển đổi từ giá trị trái sang giá trị phải và trở thành con trỏ trỏ tới phần tử đầu tiên của mảng.
Do định nghĩa của phép cộng giữa con trỏ và số nguyên, kết quả là phần tử trong mảng có chỉ số bằng kết quả của biểu thức số nguyên (hoặc, nếu biểu thức con trỏ trỏ tới phần tử thứ i của một mảng, thì kết quả có chỉ số bằng i cộng với kết quả của biểu thức số nguyên).
Lời kết
Khả năng của tôi có hạn, bài viết có thể chứa sai sót. Rất hoan nghênh mọi người góp ý, sửa chữa và thảo luận trong phần bình luận. Nếu bạn thấy bài viết hữu ích, đừng quên để lại lời nhắn khích lệ tôi nhé!
Ghi chú: Bài viết này đã được kiểm tra kỹ lưỡng để đảm bảo không có bất kỳ ký tự tiếng Trung nào tồn đọng.