Lập trình C: Con trỏ (Pointer)
Tổng quan
Mỗi biến trong ngôn ngữ C đều có một tên, và tương ứng với nó là một vùng nhớ dùng để chứa giá trị của nó. Tuỳ theo kiểu dữ liệu của biến mà vùng nhớ dành cho biến có kích thước khác nhau. Ðịa chỉ của biến là số thứ tự của Byte đầu tiên tương ứng với biến đó. Ðịa chỉ của các biến có kiểu khác nhau là khác nhau, chẳng hạn như địa chỉ của hai biến kiểu int liên tiếp cách nhau 2 Byte, biến kiểu float là 4 Byte, biến kiểu double là 8 Byte.
Con trỏ có thể dùng để chứa địa chỉ của biến khác hoặc có thể chứa địa chỉ của hàm (con trỏ hàm). Do có nhiều loại địa chỉ nên cũng có nhiều loại biến con trỏ, ví dụ như con trỏ kiểu int dùng để chứa địa chỉ của biển kiểu int, con trỏ kiểu float dùng để chứa địa chỉ của biến kiểu float.
Muốn sử dụng được con trỏ (pointer), trước tiên phải có được địa chỉ của biến mà ta cần quan tâm bằng phép toán lấy địa chỉ & (đặt & trước tên biến). Kết quả của phép lấy địa chỉ & là con trỏ sẽ chứa địa chỉ của biến gán. Khi đó nếu biến con trỏ thay đổi giá trị thì biến đó cũng sẽ thay đổi giá trị.
Sau đây là một số tình huống phổ biến mà con trỏ được dùng tới: trả về nhiều hơn một giá trị từ một hàm, dùng con trỏ để truyền mảng giữa các hàm (sẽ thuận tiện hơn), làm việc với các phần tử của mảng thông qua con trỏ thay vì truy xuất trực tiếp tới chúng, cấp phát và truy xuất vùng nhớ động.
Cú pháp khai báo con trỏ
, lúc này, *Tên_con_trỏ gọi là biến con trỏ (hay biến trỏ), nó có tác dụng như một biến thông thường, tức là nó dùng để lưu giá trị, còn Tên_con_trỏ chính là con trỏ và nó được dùng để lưu địa chỉ của biến khác. Ví dụ:
int *p;
, thì *p là biến trỏ, còn p là con trỏ.
Cú pháp sử dụng con trỏ
, lúc này ta hoàn toàn có thể sử dụng biến trỏ *Tên_con_trỏ thay cho Tên_biến, mọi thao tác trên biến trỏ *Tên_con_trỏ đều tương đương với thao tác trên Tên_biến.
Ví dụ 1:
, lúc này *pnum cũng có giá trị là 5.
Hai câu lệnh sau đây là tương đương:
Ví dụ 2:
#include<stdio.h> main() { int a, *pa; a=5; pa=&a; //cho pa trỏ tới biến a *pa=a; //phép gán đúng printf("\nDia chi va gia tri cua bien a la %d va %d", &a, a); printf("\nSau khi pa tro toi a thi pa = %d va *pa = %d", pa, *pa); return 0; }
Kết quả demo:
→ Như vậy, khi có lệnh gán pa=&a; → pa sẽ chứa địa chỉ của biến a, còn *pa sẽ chứa giá trị của biến a.
Tính toán trên biến con trỏ
Hai biến con trỏ cùng kiểu có thể gán cho nhau.
Ví dụ 1:
#include<stdio.h> main() { int a, *p, *q; float *f; a = 5; p=&a; q=p; //đúng f=p; //sẽ đưa ra cảnh báo f=(float*)p; //đúng, nhờ ép kiểu con trỏ nguyên về kiểu float return 0; }
Ví dụ 2:
#include<stdio.h> main() { int a; char *c; c=&a; //xuất hiện cảnh báo c=(char*)a; //đúng, nhờ ép kiểu return 0; }
Một con trỏ có thể được cộng, trừ với một số nguyên (int, long) để cho kết quả là một con trỏ.
Ví dụ 3:
#include<stdio.h> main() { int a, *p, * p10; a=5; p=&a; p10 = p + 10; //đúng return 0; }
Ví dụ 4:
#include<stdio.h> main() { int V[10]; //mảng 10 phần tử int *pV; int i; pV=&V[0]; //gán địa chỉ của phần tử đầu tiên của mảng V cho con trỏ pV for(i=0;i<10;i++){ *pV=i; //gán giá trị i cho phần tử mà p đang trỏ đến pV++; //p được tăng lên 1 để chỉ đến phần tử kế tiếp } return 0; }
→ Kết quả: V[0]=0, V[1]=1, ..., V[9]=9
Chú ý:
• Phép trừ 2 con trỏ cho kết quả là một số int biểu thị khoảng cách (số phần tử) giữa 2 pointer đó.
• Phép cộng 2 pointer là không hợp lệ, pointer không được nhân chia với 1 số nguyên hoặc nhân chia vơi nhau.
• p=NULL → Con trỏ p không trỏ đến đâu cả.
Con trỏ và mảng
Con trỏ và mảng là tương đương nhau, nghĩa là con trỏ có thể thay thế cho mảng và ngược lại, mảng cũng có thể thay thế cho con trỏ.
Mảng một chiều tương ứng với con trỏ có 1 sao (*), ví dụ: a[] tương ứng với *a; mảng 2 chiều tương ứng với con trỏ có 2 sao (**), ví dụ: a[][] tương ứng với **a.
Chú ý: Không nên sử dụng con trỏ khi nó chưa được khởi gán vì khi đó nó sẽ trỏ đến vị trí bất kỳ, đơn giản nhất trong trường hợp này là bạn có thể gán NULL cho con trỏ.
Ví dụ:
int a, *p; scanf ("%d", p); //không nên
→ Ta nên thay bằng các lệnh:
int a, *p; p=&a; //hoặc p=NULL scanf ("%d", p); //đúng