Chúng ta đã biết các biến được chứa trong bộ nhớ. Mỗi vị trí các biến được chứa trong bộ nhớ thì được gán cho một con số duy nhất gọi là địa chỉ (address). Thông qua địa chỉ, chúng ta có thể biết được biến đó lưu trữ ở đâu trong bộ nhớ. Tương tự như vậy mỗi phần tử của mảng đều có một địa chỉ riêng. Con trỏ là một dạng biến để chứa loại địa chỉ này.
1. Khái niệm
Pointer (con trỏ) là một kiểu dữ liệu đặc biệt dùng đê quản lý địa chỉ của các ô nhớ. Một con trỏ quản lý các địa chỉ mà dữ liệu tại các địa chỉ này có kiểu T thì con trỏ đó được gọi là con trỏ kiểu T. Con trỏ kiểu T chỉ được dùng để chứa địa chỉ của biến kiểu T. Nghĩa là con trỏ kiểu int chỉ được dùng để chứa biến kiểu int, con trỏ kiểu char chỉ được dùng chứa biến kiểu char.
Pointer là một phần cơ bản quan trọng của ngôn ngữ lập trình C. Nó là cách duy nhất để thể hiện một số thao tác truy xuất và dữ liệu; nó tạo ra mã code đơn giản, hiệu quả, là một công cụ thực thi mạnh mẽ.
2. Khai báo con trỏ
Cú pháp khai báo biến con trỏ:
Kiểu * Tên biến con trỏ;
Ví dụ:
// x là biến kiểu int, còn px là con trỏ kiểu int.
int x, *px;
px được khai báo là một con trỏ kiểu int, nó chứa địa chỉ của biến kiểu dữ liệu số nguyên. Dấu * không phải là một phần của biến, int * có nghĩa là con trỏ kiểu int.
Đặt tên biến con trỏ giống như tên của các biến khác. Để gán địa chỉ vào con trỏ chúng ta cần phải gán giá trị cho biến như sau:
int a = 10;
int *p;
p = &a;// giá trị p chứa địa chỉ của biến a
Ví dụ trên được hiểu như sau:
- a là biến kiểu int được khởi tạo bằng 10.
- p là biến con trỏ kiểu int, chứa địa chỉ của kiểu dữ liệu int, lúc này nó không chứa giá trị (hay chứa giá trị NULL).
- Câu lệnh p = &a có nghĩa là “gán địa chỉ của a vào p”. Biến con trỏ này bây giờ chứa địa chỉ của biến a.
- Giả sử địa chỉ của biến a và p trong bộ nhớ là fff0 và ff12. Câu lệnh p = &a để gán địa chỉ của a vào p. Dấu ‘&’ viết phía trước biến a được gọi là phép toán địa chỉ (address). Vì thế biến con trỏ này chứa giá trị fff0. Mặc dù chúng ta khai báo biến con trỏ với dấu ‘*’ ở phía trước, nhưng bộ nhớ chỉ gán cho p chứ không phải *p.
3. Toán tử địa chỉ và toán tử nội dung
Toán tử địa chỉ (&): Biến được khai báo là x thì &x là địa chỉ của x. Kết quả của phép lấy địa chỉ (&) là một con trỏ, do đó có thể dùng để gán cho một biến pointer.
Ví dụ:
a) int *px, num;
// px là một pointer chỉ đến biến kiểu int là num.
px = #
//xuất địa chỉ của biến num dạng số hệ 16 (hệ hexa)
printf(“%x”, &num);
b) int *px, num;
px = &(num +4); // SAI vì ( num+4) không phải là
một biến cụ thể
Lưu ý: Chúng ta thấy cú pháp lệnh nhập dữ liệu scanf trong ngôn ngữ lập trình C luôn có dấu & trước biến cần nhập. Điều này xác định cần đưa dữ liệu vào con trỏ chứa địa chỉ của biến tương ứng.
Toán tử nội dung (*): Toán tử lấy nội dung của một địa chỉ được kí hiệu là dấu * trước một pointer, dùng để lấy giá trị của biến mà con trỏ đang trỏ đến.
Xét lại ví dụ trên, ta có: px là một pointer chỉ đến biến num như ví dụ phần a, thì * px là giá trị của biến num.
Ví dụ:
a) //num là biến được khai báo và gán giá trị là 10.
int num = 10 ;
int *px; // px là một con trỏ chỉ đến kiểu int
px= &num ; //px là địa chỉ của biến num.
/*giá trị của *px (tức là num) cộng thêm 3, gán cho
k. Sau đó *px thực hiện lệnh tăng 1 đơn vị (++)*/
int k = (* px)++ + 3 ;
// Sau câu lệnh trên num = 11, k = 13
b) int num1 = 2, num2, *pnt;
pnt = &num1
num2 = *pnt;
Trong ví dụ trên, biến num1 được gán bằng 2. Dòng pnt = &num1 nghĩa là biến con trỏ pnt chứa địa chỉ của biến num1. Phép gán num2 = *pnt, dấu ‘*’ được đặt ở phía trước biến con trỏ, thì giá trị trả về của biến này l giá trị của biến được trỏ tới bởi con trỏ pnt. Do đó, num2 có giá trị là 2.
Lưu ý : NULL là hằng khi pointer mang ý nghĩa không chứa một địa chỉ nào cả. Ta gọi là pointer rỗng.
4. Tính toán trên Pointer
Một biến pointer có thể được cộng trừ với một số nguyên (int, long) để cho kết quả là một pointer chỉ đến một vùng nhớ khác.
Ví dụ:
int *ptr, *p1;
int num = 10;
ptr = #
p1 = ptr + 2;
Việc cộng hoặc trừ pointer với một số nguyên n thì pointer sẽ chỉ đến một địa chỉ mới hay nói cách khác là chỉ đến một biến khác nằm cách biến đó n vị trí.
Ví dụ:
int v[10]; // mảng 10 phần tử lin tiếp .
int * p ; // Biến pointer chỉ đến một số int .
p= & v[0]; // p là địa chỉ phần tử đầu tiên của mảng
for( i =0; i<10 ; i++)
{
*p= i * i; // gán cho phần tử mà p đang chỉ đến
p ++ ;// p được tăng lên để chỉ đến phần tử kế tiếp
}
Do cộng một pointer với một giá trị nguyên cho ta một pointer, nên phép trừ hai pointer vẫn được coi là hợp lệ, kết quả cho ta một giá trị 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ệ.
- Không thể nhân, chia , hoặc lấy dư của một pointer với bất kì một số nào.
- Đối với các phép toán khác, pointer được xử lý như một biến bình thường (gán, so sánh …), các toán hạng phải cùng kiểu pointer và cùng kiểu đối tượng của chúng. Mỗi sự chuyển kiểu tự động luôn được cân nhắc và xác nhận từ trình biên dịch.
Địa chỉ của một biến được xem là một pointer hằng và ngôn ngữ lập trình C cho phép thực hiện các phép toán mà pointer chấp nhận trên nó, trừ phép gán lại và phép tăng giảm vì ta không thể gán lại một giá trị hằng bằng một giá trị khác được.
5. Cấp phát và giải phóng vùng nhớ cho biến con trỏ
Cấp phát vùng nhớ cho biến con trỏ:
Trước khi sử dụng biến con trỏ, ta nên cấp phát vùng nhớ cho biến con trỏ này quản lý địa chỉ. Việc cấp phát được thực hiện nhờ các hàm malloc(), calloc() trong thư viện alloc.h. Cú pháp các hàm:
void *malloc(size_t size): Cấp phát vùng nhớ có kích thước size.
void *calloc(size_t nitems, size_t size): Cấp phát vùng
nhớ có kích thước là nitems*size.
Ví dụ:
int a, *pa, *pb;
pa = (int*)malloc(sizeof(int)); /* Cấp phát vùng nhớ
có kích thước bằng với kích thước của một số
nguyên */
pb= (int*)calloc(10, sizeof(int)); /* Cấp phát vùng
nhớ có thể chứa được 10 số nguyên*/
Lưu ý: Khi sử dụng hàm malloc() hay calloc(), ta phải ép kiểu vì nguyên mẫu các hàm này trả về con trỏ kiểu void.
Cấp phát lại vùng nhớ cho biến con trỏ:
Trong quá trình thao tác trên biến con trỏ, nếu ta cần cấp phát thêm vùng nhớ có kích thước lớn hơn vùng nhớ đã cấp phát, ta sử dụng hàm realloc(). Cú pháp:
Trong quá trình thao tác trên biến con trỏ, nếu ta cần cấp phát thêm vùng nhớ có kích thước lớn hơn vùng nhớ đã cấp phát, ta sử dụng hàm realloc(). Cú pháp:
void *realloc(void *block, size_t size)
Ý nghĩa:
- Cấp phát lại 1 vùng nhớ cho con trỏ block quản lý, vùng nhớ này có kích thước mới là size; khi cấp phát lại thì nội dung vùng nhớ trước đó vẫn tồn tại.
- Kết quả trả về của hàm là địa chỉ đầu tiên của vùng nhớ mới. Địa chỉ này có thể khác với địa chỉ được chỉ ra khi cấp phát ban đầu.
Giải phóng vùng nhớ cho biến con trỏ:
Một vùng nhớ đã cấp phát cho biến con trỏ, khi không còn sử dụng nữa, ta sẽ thu hồi lại vùng nhớ này nhờ hàm free().
Một vùng nhớ đã cấp phát cho biến con trỏ, khi không còn sử dụng nữa, ta sẽ thu hồi lại vùng nhớ này nhờ hàm free().
Cú pháp:
void free(void *block)
Như vậy, thông qua bài học này, mình đã giới thiệu đến các bạn con trỏ trong C. Cảm ơn các bạn đã đọc.