Chào các bạn học viên đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.
Trong bài học trước, chúng ta tạm dừng sau khi tìm hiểu những khái niệm cơ bản nhất khi sử dụng con trỏ trong C/C++, vẫn còn rất nhiều thứ cần phải nói khi nhắc đến con trỏ.
Một câu hỏi đặt ra là các phép toán khi sử dụng cho con trỏ có gì khác so với sử dụng các phép toán với các biến thông thường hay không?
Về mặt bản chất, giá trị lưu trữ bên trong vùng nhớ của con trỏ là địa chỉ, địa chỉ của một biến (hoặc vùng nhớ) có kiểu unsigned int (số nguyên không dấu), do đó, chúng ta có thể thực hiện các phép toán trên con trỏ. Nhưng kết quả của các phép toán thực hiện trên con trỏ sẽ khác các phép toán số học thông thường về giá trị và cả ý nghĩa.
Ngôn ngữ C/C++ định nghĩa cho chúng ta 4 toán tử toán học có thể sử dụng cho con trỏ: ++, --, +, và -.
Trước khi tìm hiểu về các toán tử toán học dùng cho con trỏ, chúng ta khai báo trước một biến thông thường và một biến con trỏ (có kiểu dữ liệu phù hợp để trỏ tới biến thông thường vừa được khai báo):
int value = 0;
int *ptr = &value;
Increment operator (++)
Như các bạn đã được học, increment operator (++) được dùng để tăng giá trị bên trong vùng nhớ của biến lên 1 đơn vị. Increment operator (++) là toán tử một ngôi, có thể đặt trước tên biến, hoặc đặt sau tên biến.
Bây giờ, chúng ta sử dụng toán tử (++) cho con trỏ ptr để xem kết quả:
cout << "Before increased: " << ptr << endl;
ptr++;
cout << " After increased: " << ptr << endl;
Kết quả:
-
Before increased: 0x00F9FEFC (heximal) tương đương 16383740 (decimal)
-
After increased: 0x00F9FF00 (heximal) tương đương 16383744 (decimal)
Địa chỉ mới của ptr lúc này là 16383744, giá trị này lớn hơn giá trị cũ 4 đơn vị. Đúng bằng kích thước của kiểu dữ liệu int mà mình dùng để khai báo cho biến value.
Như vậy, increment operator (++) sẽ làm con trỏ trỏ đến địa chỉ tiếp theo trên bộ nhớ ảo. Khoảng cách của 2 địa chỉ này đúng bằng kích thước của kiểu dữ liệu được khai báo cho con trỏ.
Giả sử cũng với địa chỉ ban đầu là 16383740, nếu con trỏ được khai báo là char *ptr;
thì khi sử dụng toán tử (++), địa chỉ mới của con trỏ lúc này sẽ là 16383741.
Decrement operator (–)
Ngược lại so với increment operator (++), decrement operator (–) sẽ giảm giá trị bên trong vùng nhớ của biến thông thường đi 1 đơn vị. Đối với biến con trỏ, khi sử dụng decrement operator (–), nó sẽ làm thay đổi địa chỉ của con trỏ đang trỏ đến, giá trị địa chỉ mới sẽ bằng giá trị địa chỉ cũ trừ đi kích thước của kiểu dữ liệu mà con trỏ đang trỏ đến.
Để dễ hình dung, mình lấy lại ví dụ trên:
int value = 0;
int *ptr = &value;
cout << "Before decreased: " << ptr << endl;
ptr--;
cout << " After decreased: " << ptr << endl;
Kết quả:
-
Before increased: 0x0051FC24 (heximal) tương đương 5372964 (decimal)
-
After increased: 0x0051FC20 (heximal) tương đương 5372960 (decimal)
Như chúng ta thấy, địa chỉ mới nhỏ hơn 4 (bytes) so với địa chỉ ban đầu, 4 bytes này chính là kích thước kiểu dữ liệu int mà con trỏ được khai báo.
Giả sử cũng với địa chỉ ban đầu là 5372964, nếu con trỏ được khai báo double *ptr;
thì sau khi sử dụng toán tử (–), địa chỉ mới của con trỏ sẽ là 5372956.
Addition operator (+)
Sử dụng increment operator (++) cho con trỏ chỉ có thể làm con trỏ trỏ đến địa chỉ tiếp theo trên bộ nhớ ảo bắt đầu từ địa chỉ ban đầu mà con trỏ đang nắm giữ. Trong khi đó, toán tử addition (+) cho phép chúng ta trỏ đến vùng nhớ bất kỳ phía sau địa chỉ mà con trỏ đang nắm giữ.
Xét đoạn chương trình sau:
int value = 0;
int *ptr = &value;
cout << ptr << endl;
ptr = ptr + 5;
cout << ptr << endl;
Kết quả:
-
Before added 5: 0x0087FE48 (heximal) tương đương 8912456.
-
After added 5: 0x0087FE5C (heximal) tương đương 8912476.
8912476 - 8912456 = 20 (bytes)
Như vậy, con trỏ ptr đã trỏ đến địa chỉ mới đứng sau địa chỉ ban đầu 20 bytes (tương đương với 5 lần kích thước kiểu int)
Chúng ta có thể sử dụng dereference operator để truy xuất trực tiếp giá trị bên trong các vùng nhớ ảo bất kỳ khi sử dụng toán tử (+).
int value = 0;
int *ptr = &value;
cout << ptr << " => " << *ptr << endl;
cout << ptr + 10 << " => " << *(ptr + 10) << endl;
cout << ptr + 50 << " => " << *(ptr + 50) << endl;
Kết quả của đoạn chương trình này là:
Giá trị 0 ban đầu là của biến value đang nắm giữ, những giá trị rác phía sau là của các vùng nhớ khác nắm giữ, chúng ta không cần thông qua tên biến nhưng vẫn có thể truy xuất giá trị của chúng thông qua dereference operator.
Những giá trị này có thể do chương trình khác đang sử dụng, nhưng những vùng nhớ này chưa được truy xuất bởi các chương trình khác hoặc không phải vùng nhớ hệ thống quan trọng, nên chương trình của chúng ta vẫn có thể truy xuất đến giá trị bên trong những địa chỉ này. Nếu có 2 chương trình cùng truy cập đến một vùng nhớ, hệ thống sẽ xảy ra xung đột.
Lưu ý: Toán tử (+) chỉ cho phép thực hiện với số nguyên.
Subtraction operator (-)
Ngược lại so với toán tử (+).
-
Before subtracted 5: 0x002CF7E0 (heximal) tương đương 2947040
-
After subtracted 5: 0x002CF7CC (heximal) tương đương 2947020
2947040 - 2947020 = 20 (bytes)
Như vậy, con trỏ ptr đã trỏ đến địa chỉ mới đứng trước địa chỉ ban đầu 20 bytes (tương đương với 5 lần kích thước kiểu int).
Chúng ta có thể sử dụng dereference operator để truy xuất trực tiếp giá trị bên trong các vùng nhớ ảo bất kỳ khi sử dụng toán tử (-).
int value = 0;
int *ptr = &value;
cout << ptr << " => " << *ptr << endl;
cout << ptr - 5 << " => " << *(ptr - 5) << endl;
cout << ptr - 10 << " => " << *(ptr - 10) << endl;
Kết quả của đoạn chương trình này là:
Giải thích tương tự khi sử dụng toán tử (+).
Lưu ý: Toán tử (-) chỉ cho phép thực hiện với số nguyên.
So sánh hai con trỏ
Ngoài các toán tử toán học, chúng ta còn có thể áp dụng các toán tử quan hệ khi sử dụng con trỏ. Giả sử chúng ta khai báo 2 con trỏ p1 và p2 như sau:
int value1, value2;
int *p1;
int *p2;
p1 = &value1;
p2 = &value2;
Con trỏ p1 trỏ đến value1 và con trỏ p2 trỏ đến value2. Chúng ta thực hiện lần lượt 6 phép so sánh:
cout << "Is p1 less than p2? " << (p1 < p2) << endl;
cout << "Is p1 greater than p2? " << (p1 > p2) << endl;
cout << "Is p1 less than or equal p2? " << (p1 <= p2) << endl;
cout << "Is p1 greater than or equal p2? " << (p1 >= p2) << endl;
cout << "Is p1 equal p2? " << (p1 == p2) << endl;
cout << "Is p1 not equal p2? " << (p1 != p2) << endl;
Kết quả chúng ta được như sau:
Trong đó, phép so sánh bằng (==) sẽ kiểm tra xem 2 con trỏ này có trỏ đến cùng một địa chỉ hay không.
Một số lưu ý khi sử dụng các toán tử dùng cho con trỏ
Vì các toán tử dùng cho con trỏ có ý nghĩa hoàn toàn khác so với việc áp dụng các toán tử lên giá trị hoặc biến thông thường. Chúng ta cần có cách sử dụng hợp lý để tránh gây nhầm lẫn hoặc gây rối mắt.
Lấy đoạn chương trình sau để làm ví dụ:
int n = 5;
int *p = &n; //p point to n
*p++;
p++;
int n2 = *p*n;
Đây là một số cách sử dụng các toán tử toán học cho con trỏ gây khó hiểu cho người đọc.
-
Lệnh
*p++;
sẽ thực hiện hai bước, đầu tiên là sử dụng toán tử dereference để truy xuất đến vùng nhớ tại địa chỉ mà con trỏ p đang nắm giữ, bước thứ hai là trỏ đến địa chỉ tiếp theo (đứng sau n). -
Sau đó, chúng ta bắt gặp lệnh
p++;
có nghĩa là cho con trỏ p trỏ đến địa chỉ tiếp theo lớn hơn địa chỉ ban đầu 4 bytes (kích thước của kiểu int). -
Dòng cuối cùng, chúng ta có phép gán giá trị của phép nhân *p và n cho biến n2.
Để chương trình rõ ràng hơn, chúng ta nên thêm các cặp dấu ngoặc vào chương trình tương tự như thế này:
int n = 5;
int *p = &n; //p point to n
(*p)++;
p++;
int n2 = (*p) * n;
Những cặp dấu ngoặc sẽ giúp phân biệt lúc nào chúng ta sử dụng giá trị là địa chỉ lưu trong con trỏ, lúc nào chúng ta sử dụng giá trị trong vùng nhớ mà con trỏ đang trỏ đến.
Tổng kết
Việc sử dụng toán các toán tử toán học cho biến con trỏ mà không có mục đích rõ ràng có thể gây xung đột vùng nhớ, có thể dẫn đến crash chương trình. Chúng ta thường sử dụng các toán tử toán học khi con trỏ trỏ đến mảng một chiều, vì mảng một chiều lưu trữ trên bộ nhớ ảo là một vùng nhớ mà những phần tử có địa chỉ liên tiếp nhau. Chúng ta sẽ tìm hiểu vấn đề này trong các bài học sau.
Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.
Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn.