Bỏ qua để đến nội dung

Con trỏ và mảng

Mảng và con trỏ có mối quan hệ rất chặt chẽ trong C++ - hiểu mối quan hệ này giúp giải thích nhiều hành vi của mảng mà bạn đã thấy ở các bài trước.

Khi dùng tên của một mảng trong hầu hết ngữ cảnh, nó tự động chuyển thành con trỏ trỏ đến phần tử đầu tiên của mảng:

#include <iostream>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
std::cout << numbers << std::endl; // Địa chỉ của phần tử đầu tiên
std::cout << &numbers[0] << std::endl; // Cũng chính là địa chỉ đó
int* ptr = numbers; // Không cần dấu &, vì numbers đã "là" một địa chỉ
std::cout << *ptr << std::endl; // 10 (giá trị của phần tử đầu tiên)
return 0;
}
int numbers[5] = {10, 20, 30, 40, 50};
int* ptr = numbers;
std::cout << *ptr << std::endl; // 10 (numbers[0])
std::cout << *(ptr + 1) << std::endl; // 20 (numbers[1])
std::cout << *(ptr + 2) << std::endl; // 30 (numbers[2])

Khi cộng một số nguyên vào con trỏ, C++ tự động “nhảy” theo đúng kích thước của kiểu dữ liệu, không phải theo byte:

int numbers[3] = {10, 20, 30};
int* ptr = numbers;
std::cout << ptr << std::endl; // Địa chỉ, ví dụ 0x1000
std::cout << (ptr + 1) << std::endl; // 0x1004 (nhảy 4 byte, vì sizeof(int) == 4)

Điều này giải thích vì sao numbers[i]*(numbers + i) cho kết quả giống hệt nhau - thực chất arr[i] chính là cú pháp rút gọn của *(arr + i):

int numbers[3] = {10, 20, 30};
std::cout << numbers[1] << std::endl; // 20
std::cout << *(numbers + 1) << std::endl; // 20 - hoàn toàn tương đương
#include <iostream>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int* ptr = numbers;
for (int i = 0; i < 5; i++) {
std::cout << *(ptr + i) << " ";
}
// 1 2 3 4 5
return 0;
}

Sự khác biệt quan trọng: mảng không phải là con trỏ

Phần tiêu đề “Sự khác biệt quan trọng: mảng không phải là con trỏ”

Dù mảng “phân rã” thành con trỏ trong hầu hết ngữ cảnh, chúng không hoàn toàn giống nhau:

int numbers[5] = {1, 2, 3, 4, 5};
int* ptr = numbers;
std::cout << sizeof(numbers) << std::endl; // 20 (5 phần tử x 4 byte = kích thước TOÀN BỘ mảng)
std::cout << sizeof(ptr) << std::endl; // 8 (kích thước của MỘT con trỏ trên hệ 64-bit)

sizeof trên mảng cho biết kích thước toàn bộ mảng, nhưng sizeof trên con trỏ chỉ cho biết kích thước của bản thân con trỏ (thường là 8 byte trên hệ 64-bit) - đây là lý do tại sao khi mảng “phân rã” thành con trỏ khi truyền vào hàm, hàm đó không thể tự biết kích thước mảng.

  • Tên mảng, khi dùng trong biểu thức, tự động chuyển thành con trỏ trỏ đến phần tử đầu tiên
  • arr[i]*(arr + i) là hai cách viết tương đương để truy cập phần tử thứ i
  • Phép cộng trên con trỏ tự động “nhảy” theo kích thước của kiểu dữ liệu, không phải theo byte
  • Mảng và con trỏ không hoàn toàn giống nhau: sizeof cho kết quả khác nhau giữa hai loại