Trong C++, con trỏ là công cụ cho phép làm việc trực tiếp với địa chỉ bộ nhớ. Nó là nền tảng cho việc truy cập hiệu quả tài nguyên hệ thống, quản lý động vùng nhớ, triển khai cấu trúc dữ liệu linh hoạt và tương tác với phần cứng ở mức thấp. Hiểu rõ con trỏ là yêu cầu bắt buộc khi làm việc với bất kỳ ngôn ngữ lập trình hệ thống nào.

Con trỏ là một biến có khả năng lưu trữ địa chỉ của một biến khác. Nếu int x = 10;
, thì &x
là địa chỉ bộ nhớ nơi giá trị 10 được lưu. Ta có thể khai báo con trỏ như sau:
int x = 10;
int* p = &x;
Ở đây, p
không chứa giá trị 10, mà là địa chỉ của biến x
. Dấu *
trong khai báo thể hiện kiểu “con trỏ đến int”. Khi dùng lại *p
, ta lấy ra giá trị mà con trỏ đang trỏ tới:
cout << *p; // in ra 10
Dấu *
trong khai báo là một phần của kiểu dữ liệu, còn trong biểu thức là toán tử dereference. Tương tự, dấu &
là toán tử lấy địa chỉ, không liên quan đến toán học.
Mối quan hệ giữa biến, địa chỉ và giá trị là cốt lõi:
int a = 42;
int* ptr = &a;
cout << a; // 42
cout << &a; // địa chỉ a
cout << ptr; // địa chỉ a
cout << *ptr; // 42
Con trỏ có thể trỏ đến bất kỳ kiểu dữ liệu nào. Ta cần khai báo đúng kiểu để dereference cho kết quả hợp lệ:
double d = 3.14;
double* pd = &d;
Tuy nhiên, C++ cũng cho phép khai báo con trỏ void*
– có thể trỏ đến bất kỳ kiểu nào, nhưng không thể dereference trực tiếp mà cần ép kiểu.
Một trong những ứng dụng thiết yếu của con trỏ là truyền tham chiếu qua hàm. Khi truyền con trỏ, hàm có thể thay đổi trực tiếp nội dung tại vùng nhớ gốc:
void tang(int* p) {
(*p)++;
}
int x = 5;
tang(&x); // x giờ là 6
Con trỏ cũng được dùng để cấp phát bộ nhớ động. Khi không biết trước kích thước mảng, cần dùng new
để tạo vùng nhớ:
int* arr = new int[100];
Vùng nhớ này được cấp phát từ heap, tồn tại cho đến khi ta gọi delete[] arr;
. Nếu không gọi, sẽ gây rò rỉ bộ nhớ (memory leak). Quản lý bộ nhớ động là trách nhiệm của người viết mã khi dùng con trỏ.

Con trỏ có thể được dùng để thao tác mảng, vì như đã trình bày, tên mảng thực chất là một con trỏ hằng:
int a[3] = {1, 2, 3};
int* p = a;
cout << *(p + 2); // in 3
Toán tử []
chỉ là cú pháp của *(base + index)
. Do đó, truy cập mảng qua con trỏ hoặc chỉ số đều tương đương. Điều này giúp ta viết các vòng lặp linh hoạt hơn:
for (int* p = a; p < a + 3; p++) {
cout << *p << " ";
}
Ngoài ra, con trỏ còn hỗ trợ tạo ra các cấu trúc dữ liệu động như danh sách liên kết, cây nhị phân, đồ thị… Trong những cấu trúc đó, mỗi phần tử chứa một con trỏ tới phần tử tiếp theo.
Một khái niệm quan trọng là con trỏ NULL, tức là con trỏ không trỏ đến đâu cả:
int* p = nullptr;
Luôn khởi tạo con trỏ về nullptr
nếu chưa có giá trị rõ ràng. Truy cập con trỏ chưa khởi tạo sẽ dẫn đến lỗi runtime nghiêm trọng (segmentation fault).
Một lỗi phổ biến là dangling pointer – con trỏ trỏ tới vùng nhớ đã bị giải phóng:
int* foo() {
int x = 10;
return &x; // sai, x bị hủy sau khi hàm kết thúc
}
Tương tự, nếu gọi delete
mà vẫn giữ con trỏ cũ, rồi dereference nó sau đó, cũng gây ra lỗi không xác định.
Cuối cùng, với new
phải đi kèm delete
, với new[]
phải đi kèm delete[]
. Dùng sai có thể dẫn tới lỗi nghiêm trọng trong chương trình lớn.
Hiểu và sử dụng con trỏ đúng cách là bước chuyển từ lập trình căn bản sang lập trình hệ thống. Đó là nơi người viết phải hiểu bản chất bộ nhớ, stack, heap, quyền sở hữu dữ liệu, và trách nhiệm giải phóng tài nguyên.
Sign up