Con trỏ là một khái niệm rất quan trọng trong lập trình C/C++. Khi bạn sử dụng các ngôn ngữ lập trình cao cấp hơn như Java, .NET... thì sẽ không còn thấy bóng dáng của con trỏ đâu nữa vì tất cả đã được hỗ trợ bởi ngôn ngữ lập trình.
Hiểu được con trỏ sẽ giúp chúng ta hiểu được bản chất của các loại cấu trúc dữ liệu (CTDL). Sử dụng con trỏ thành thạo và linh hoạt sẽ giúp chúng ta làm chủ được mọi tình huống phát sinh trong lập trình CTDL.
Nội dung bên dưới đây được trích dẫn từ cuốn sách "Giáo trình Kỹ thuật lập trình C Căn bản & Nâng cao" được xuất bản năm 2009 bởi Nhà xuất bản Hồng Đức. Các tác giả của cuốn sách bao gồm GS. Phạm Văn Ất (chủ biên) - Ths. Nguyễn Hiếu Cường, Ths. Đỗ Văn Tuấn, Lê Trường Thông.
1. Địa chỉ
Liên quan đến một biến ta đã có khái niệm:
▸Tên biến.
▸Kiểu biến.
▸Giá trị của biến.
Ví dụ câu lệnh:
float alpha = 30.5;
Câu lệnh trên xác định một biến có tên là alpha có kiểu float và có giá trị 30.5. Ta cũng đã biết, theo khai báo trên, máy sẽ cấp phát cho biến alpha một khoảng nhớ gồm 4 byte liên tiếp. Địa chỉ của biến là số thứ tự của byte đầu tiên trong một dãy các byte liên tiếp mà máy tính dành cho biến (các byte được đánh từ số 0).
Một điều cần chú ý là mặc dù địa chỉ của biến là một số nguyên nhưng không được đánh đồng nó với các số nguyên thông thường dùng trong các phép tính.
Rõ ràng địa chỉ của hai biến kiểu int liên tiếp cách nhau khoảng 2 byte, địa chỉ của hai biến kiểu float liên tiếp cách nhau 4 byte,... Nên máy sẽ phân biệt các kiểu địa chỉ: địa chỉ kiểu int, kiểu float, kiểu double,...
Lưu ý: Phép toán &x cho ta địa chỉ của biến x.
2. Con trỏ
Con trỏ là một biến dùng để chứa địa chỉ. Vì có nhiều loại địa chỉ nên cũng có nhiều kiểu con trỏ tương ứng. Con trỏ kiểu int dùng để chứa địa chỉ các biến kiểu int. Tương tự, ta có con trỏ kiểu float, double... Cũng như đối với bất kỳ một biến nào khác, một con trỏ cần phải được khai báo trước khi sử dụng. Việc khai báo biến con trỏ được thực hiện theo mẫu sau:
kiểu *tên_con_trỏ;
Ví dụ câu lệnh:
int x, y, *px, *c;
Câu lệnh trên khai báo hai biến int x, y và hai con trỏ kiểu int là px và c.
Tương tự câu lệnh:
float *t, *d;
Khai báo hai con trỏ kiểu float t và d.
Khi đã có các khai báo trên thì các câu lệnh:
c = &y;
px = &x;
Sẽ hoàn toàn xác định. Câu lệnh thứ nhất gán địa chỉ của y cho con trỏ c và câu lệnh thứ hai gán địa chỉ của biến x cho con trỏ px. Như vậy trong con trỏ c chứa địa chỉ của biến y và trong con trỏ px chứa địa chỉ của biến x. Chú ý rằng nếu viết:
t = &y;
Thì bạn sẽ nhận được một câu lệnh sai, vì: t là con trỏ kiểu float, nó chỉ chứa được địa chỉ của các biến float. Câu lệnh trên nhằm gán địa chỉ của biến nguyên y cho con trỏ t kiểu số thực là không thể chấp nhận được.
3. Quy tắc sử dụng con trỏ trong các biểu thức
Ta có thể sử dụng tên con trỏ hoặc dạng khai báo của nó trong các biểu thức. Ví dụ đối với con trỏ px, ta có thể sử dụng các cách viết: px (tên con trỏ) và *px (dạng khai báo của con trỏ).
▸Cách 1: Sử dụng tên con trỏ. Con trỏ cũng là một biến nên khi tên của nó xuất hiện trong một biểu thức thì giá trị của nó sẽ được sử dụng trong biểu thức này. Chỉ có một điều cần lưu ý ở đây: giá trị của một con trỏ là địa chỉ của một biến nào đó. Khi tên con trỏ đứng ở bên trái của một toán tử gán thì giá trị của biểu thức bên phải (để gán cho con trỏ phải là địa chỉ).
Ta hãy xem các câu lệnh sau làm gì?
float a, *p, *q;
p = &a;
q = p;
Câu lệnh thứ nhất khai báo một biến kiểu float (biến a) và hai con trỏ p và q kiểu float. Câu lệnh thứ hai sẽ gán địa chỉ của biến a cho con trỏ p và câu lệnh thứ ba sẽ gán giá trị của p cho q. Kết quả là con trỏ q chứa địa chỉ của biến a. Nói một cách nôm na dễ hiểu hơn đó là con trỏ p trỏ về ô nhớ chứa biến a, và con trỏ q bằng con trỏ p nên con trỏ q cũng trỏ về ô nhớ chứa biến a, hay nói cách khác, p và q cùng trỏ về một ô nhớ.
Cũng giống như các biến khác, nội dung của con trỏ có thể thay đổi. Ta có thể sử dụng quy tắc này để biến đổi địa chỉ. Chẳng hạn, nếu con trỏ p chứa địa chỉ của phần tử mảng a[i], thì sau khi thực hiện phép toán ++p nó sẽ chưa địa chỉ của phần tử a[i + 1].
▸Cách 2: Sử dụng dạng khai báo của con trỏ. Như đã biết sau khi thực hiện các câu lệnh:
float x, y, z, *px, *py;
px = &x;
py = &y;
Thì px trỏ tới con trỏ x, py trỏ tới con trỏ y. Bây giờ thì có thể bàn đến ý nghĩa của các cách viết *px và *py.
Điều này được phát biểu trong một nguyên lý rất ngắn gọn như sau: Nếu con trỏ px trỏ tới biến x thì các cách viết: x và *px là tương đương trong mọi ngữ cảnh.
Theo nguyên lý này thì ba câu lệnh sau đều có hiệu lực như nhau:
y = 3*x + z;
*py = 3*x + z;
*py = 3*(px) +z;
Từ đây có thể rút ra một kết luận quan trọng là: khi biết được địa chỉ của một biến thì chẳng những chúng ta có thể sử dụng giá trị của nó mà còn có thể gán cho nó một giá trị mới (làm thay đổi nội dung của nó). Điều này sẽ được áp dụng như một phương pháp chủ yếu để nhận kết quả của hàm thông qua đối số.
4. Hàm có đối con trỏ
Điều đầu tiên cần ghi nhớ ở đây là: Nếu đối của hàm là con trỏ kiểu int (hoặc float, double...) thì tham số thực tương ứng phải là địa chỉ của phần tử mảng kiểu int (hoặc float, double...). Khi đó địa chỉ của biến được truyền cho đối con trỏ tương ứng. Do đã biết địa chỉ của biến, nên ta có thể gán cho nó các giá trị mới bằng cách sử dụng các câu lệnh thích hợp trong thân hàm. Bây giờ bằng cách dùng đối con trỏ ta có thể xây dựng hàm hoán vị hai biến kiểu float như sau:
#include "stdio.h"
// ví dụ đúng
void hoan_vi(float *px, float *py) {
float z; // cấp phát cho biến cục bộ z một khoảng nhớ 4 byte
z = *px;
*px = *py;
*py = z;
}
int main() {
float a = 7.6;
float b = 13.5;
hoan_vi(&a, &b);
putchar('\n');
printf("a = %0.2f, b = %0.2f", a, b);
return 0;
}
Kết quả sau khi thực hiện chương trình là: a = 13.50, b = 7.60
Ta hãy xem hàm hoan_vi làm việc như thế nào? Như đã biết, chương trình bắt đầu từ câu lệnh đầu tiên trong hàm main. Kết quả là biến a nhận giá trị 7.6 và biến b nhận giá trị 13.5. Tiếp đó là lời gọi hàm hoan_vi. Máy sẽ gán giá trị của các tham số thực cho đối tương ứng. Như vậy địa chỉ của a (&a) được gán cho con trỏ px, địa chỉ của b (&b) được gán cho con trỏ py. Sau đó máy lần lượt xét đến các câu lệnh trong thân hàm hoan_vi. Câu lệnh thứ nhất sẽ cấp phát cho biến cục bộ z một khoảng nhớ 4 byte. Theo quy tắc về sử dụng con trỏ thì 3 câu lệnh tiếp theo sẽ tương đương với các câu lệnh:
z = a;
a = b;
b = z;
Như vậy a sẽ nhận giá trị của b và ngược lại. Tiếp đó, máy trở về hàm main và in ra những dòng kết quả như đã chỉ ra ở trên.
5. Khi nào thì chúng ta sử dụng đối số dạng con trỏ?
Trong số các đối của hàm, ta có thể chia ra làm 2 loại. Loại thứ nhất gồm các đối dùng để chứa các giá trị đã biết, ta gọi chúng là các đối vào. Loại thứ hai gồm các đối dùng để chứa các kết quả mới nhận được, gọi là các đối ra.
Ví dụ cần lập môt hàm giải phương trình bậc hai ax² + bx + c = 0. Đối với hàm này thì a, b, c là các đối vào, còn các nghiệm x1, x2 là các đối ra. Ngoài ra có thể thiết kế để hàm nhận giá trị bằng:
▸0 khi a = 0
▸1 khi a != 0 và delta >= 0
▸-1 khi a != 0 và delta < 0
Ta trở lại với câu hỏi khi nào sử dụng đối con trỏ? Câu trả lời như sau: các đối ra phải là con trỏ. Bảng sau đây chỉ ra mối quan hệ giữa tham số thực và đối tương ứng.
+------------------+------------------+
| Tham số thực | Đối tương ứng |
+------------------+------------------+
| Giá trị kiểu int | Biến kiểu int |
| (float, double) | (float, double) |
| Địa chỉ kiểu int | Con trỏ kiểu int |
| (float, double) | (float, double) |
+------------------+------------------+
Bây giờ việc xây dựng hàm giải phương trình bậc hai đã trở nên dễ dàng. Chương trình dưới đây sẽ minh họa các điều nói trên.
#include "studio.h"
#include "math.h"
int ptb2(float a, float b, float c, float *x1, float *x2) {
float delta;
if (a == 0) {
return 0;
}
delta = b * b - 4 * a * c;
if (delta < 0) {
return -1;
}
*x1 = (-b - sqrt(delta)) / (2 * a);
*x2 = (-b + sqrt(delta)) / (2 * a);
return 1;
}
int main() {
int s, ch;
float a, b, c, x1, x2;
print("Nhap a: ");
scanf("%f", &a);
print("Nhap b: ");
scanf("%f", &b);
print("Nhap c: ");
scanf("%f", &c);
s = ptb2(a, b, c, &x1, &x2);
if (s == 0) {
printf("\na = 0");
} else if (s == -1) {
printf("\n delta < 0");
} else {
printf("\n x1 = %0.2f, x2 = %0.2f", x1, x2);
}
return 0;
}