Nội dung
Trong bài viết này, bạn sẽ được tìm hiểu các kiến thức liên quan đến biến con trỏ trong C là gì, cách sử dụng và những sai lầm thường gặp khi làm việc với biến con trỏ (lập trình máy tính) thông qua các ví dụ.
1. Biến con trỏ trong C
Các biến con trỏ trong C là một công cụ mạnh mẽ, đặc trưng của lập trình C và C++. Trước khi tìm hiểu về các biến con trỏ, bạn cần hiểu rõ về địa chỉ (address) trong C (ngôn ngữ lập trình).
1.1 Địa chỉ trong lập trình C
Nếu biến var xuất hiện trong lập trình của bạn thì &var sẽ thể hiện địa chỉ của nó trong bộ nhớ.
Tôi từng nhiều lần sử dụng địa chỉ mỗi khi dùng hàm scanf().
scanf(“%d”, &var);
Giá trị do người dùng nhập vào sẽ được lưu trữ lại địa chỉ của biến var. Hãy xem qua ví dụ dưới đây:
#include <stdio.h>
int main()
{
int var = 5;
printf(“var: %d\n”, var);
// Notice the use of & before var
printf(“address of var: %p”, &var);
return 0;
}
Đầu ra
var: 5
address of var: 2686778
Tuy nhiên, bạn cũng có thể nhận được một địa chỉ khác khi chạy đoạn mã trên.
1.2 Biến con trỏ trong C là gì?
Biến con trỏ trong C (còn gọi là biến pointer trong C) là các biến đặc biệt được dùng để lưu trữ địa chỉ, chứ không phải lưu trữ các giá trị bình thường.
1.3 Cách để khai báo biến con trỏ trong C
Cú pháp của biến con trỏ C: int* p;
Trong ví dụ này, tôi đã khai báo một biến con trỏ p kiểu int.
Bạn cũng có thẻ khai báo biến con trỏ theo những cách khác như sau
int *p1;
int * p2;
Dưới đây là một ví dụ khác về cách khai báo biến pointer
int* p1, p2;
Trong ví dụ này, tôi đã khai báo một biến pointer p1 và một biến thông thường p2.
1.4 Gán địa chỉ cho biến con trỏ C
Hãy xem qua ví dụ dưới đây
int* pc, c;
c = 5;
pc = &c;
Trong ví dụ này, 5 được gán cho biến c. Và địa chỉ của c được gán với biến pointer pc.
1.5 Nhận giá trị từ những nơi mà biến pointer trỏ vào
Để nhận giá trị từ những nơi mà biến pointer trỏ vào, tôi sử dụng toán tử *. Ví dụ:
int* pc, c;
c = 5;
pc = &c;
printf(“%d”, *pc); // Output: 5
Trong ví dụ này, địa chỉ của c được gán cho biến pointer pc. Để nhận giá trị được lưu trữ trong địa chỉ này, tôi đã sử dụng *pc.
Tuy nhiên, pc là một biến pointer, chứ không phải là*pc. Do đó, bạn không thể và cũng không nên làm những điều tương tự như *pc = &c;
* được gọi là toán tử tham chiếu (khi nó đi chung với các biến pointer). Nó hoạt động cùng với các biến pointer và cung cấp giá trị được lưu trữ trong biến pointer đó.
1.6 Thay đổi giá trị mà biến pointer trỏ vào
Hãy xem xét ví dụ dưới đây:
int* pc, c;
c = 5;
pc = &c;
c = 1;
printf(“%d”, c); // Output: 1
printf(“%d”, *pc); // Ouptut: 1
Tôi đã gán địa chỉ của c vào biến pointer pc.
Sau đó, tôi đã thay đổi giá trị của c thành 1. Vì pc và địa chỉ của c là giống nhau nên đầu ra của *pc cũng sẽ là 1.
Hãy xem xét một ví dụ khác
int* pc, c;
c = 5;
pc = &c;
*pc = 1;
printf(“%d”, *pc); // Ouptut: 1
printf(“%d”, c); // Output: 1
Tôi đã gàn địa chỉ của c vào biến pointer pc.
Sau đó, tôi thay đổi *pc thành 1 bằng cách sử dụng *pc = 1;. Vì pc và địa chỉ của c là giống nhau nên c cũng sẽ bằng 1.
Thêm một ví dụ nữa.
int* pc, c, d;
c = 5;
d = -15;
pc = &c; printf(“%d”, *pc); // Output: 5
pc = &d; printf(“%d”, *pc); // Ouptut: -15
Ban đầu, địa chỉ của c được gán với biến pointer pc bằng cách sử dụng pc = &c;. Bởi vì c bằng 5 nên pc cũng sẽ bằng 5.
Sau đó, địa chỉ của d được gán với biến pointer pc bằng cách sử dụng pc = &d;. Bởi vì d bằng -15 nên *pc cũng sẽ là -15.
1.7 Ví dụ: Cách thức hoạt động của biến pointer trong C
Cùng xem qua ví dụ dưới đây
#include <stdio.h>
int main()
{
int* pc, c;
c = 22;
printf(“Address of c: %p\n”, &c);
printf(“Value of c: %d\n\n”, c); // 22
pc = &c;
printf(“Address of pointer pc: %p\n”, pc);
printf(“Content of pointer pc: %d\n\n”, *pc); // 22
c = 11;
printf(“Address of pointer pc: %p\n”, pc);
printf(“Content of pointer pc: %d\n\n”, *pc); // 11
*pc = 2;
printf(“Address of c: %p\n”, &c);
printf(“Value of c: %d\n\n”, c); // 2
return 0;
}
Đầu ra
Address of c: 2686784
Value of c: 22
Address of pointer pc: 2686784
Content of pointer pc: 22
Address of pointer pc: 2686784
Content of pointer pc: 11
Address of c: 2686784
Value of c: 2
Giải thích lập trình trên
- int* pc, c;
Một biến pointer pc và một biến thông c thường được tạo ra. Cả 2 biến này đều là kiểu int.
Vì lúc đầu, pc và c không được khởi tạo, nên biến pointer pc sẽ không trỏ tới bất cứ địa chỉ nào hoặc trỏ đến một địa chỉ ngẫu nhiên. Và, biến c sẽ có địa chỉ nhưng chứa các giá trị rác ngẫu nhiên.
- c = 22;
Gán 22 cho biến c đồng nghĩa với việc 22 sẽ được lưu trong vị trí bộ nhớ của biến c.
- pc = & c;
Địa chỉ của biến c được gán cho biến pointer pc.
- c = 11;
Gán 11 cho biến c.
- *pc = 2;
Điều này làm thay đổi giá trị tại vị trí bộ nhớ mà biến pointer pc trỏ vào thành 2.
2. Những lỗi thường gặp khi làm việc với biến pointer
Giả sử bạn muốn biến pointer pc trỏ đến địa chỉ của c.
int c, *pc;
// pc is address but c is not
pc = c; // Error
// &c is address but *pc is not
*pc = &c; // Error
// both &c and pc are addresses
pc = &c;
// both c and *pc values
*pc = c;
Đây là một ví dụ về cú pháp của biến pointer dành cho người mới bắt đầu học về lập trình C.
#include <stdio.h>
int main() {
int c = 5;
int *p = &c;
printf(“%d”, *p); // 5
return 0;
}
Tại sao khi sử dụng int *p = &c;? lại xảy ra lỗi?
Bởi vì
int *p = &c;
tương đương với
int *p:
p = &c;
Trong cả 2 trường hợp trên, tôi đang tạo ra một biến pointer p (chứ không phải *p) và gán &c cho nó.
Để tránh gây ra nhầm lẫn, tôi sử dụng câu lệnh như sau:
int* p = &c;
Chắc hẳn sau khi xem qua các ví dụ nêu trên, bạn đã hiểu rõ về biến con trỏ trong C. Tiếp theo, bạn sẽ được tìm hiểu về vấn đề các biến pointer trong C liên quan như thế nào đến các mảng.
3. Biến pointer trong C và mảng
Trong phần này, bạn sẽ được tìm hiểu về mối quan hệ giữa mảng và biến pointer trong C. Ngoài ra, bạn cũng sẽ được tìm hiểu cách truy cập vào các phần tử mảng thông qua việc sử dụng biến pointer.
Tuy nhiên, trước khi tìm hiểu về mối quan hệ giữa mảng và con trỏ, bạn cần tìm hiểu kỹ 2 chủ đề sau:
- Mảng trong C
- Biến pointer trong C
3.1 Mối quan hệ giữa mảng và các biến pointer trong C
Mảng trong C là một khối dữ liệu tuần tự. Dưới đây là lập trình in ra địa chỉ của các phần tử mảng.
#include <stdio.h>
int main() {
int x[4];
int i;
for(i = 0; i < 4; ++i) {
printf(“&x[%d] = %p\n”, i, &x[i]);
}
printf(“Address of array x: %p”, x);
return 0;
}
Đầu ra
&x[0] = 1450734448
&x[1] = 1450734452
&x[2] = 1450734456
&x[3] = 1450734460
Address of array x: 1450734448
Các phần tử liên tiếp của mảng x chênh nhau 4 byte. Bởi vì kích thước của biến int là 4 byte.
Địa chỉ của &x[0] và x là giống nhau. Bởi vì tên của biến x trỏ vào phần tử đầu tiên của mảng.
Trong ví dụ trên, rõ ràng là &x[0] tương đương với x. Và x[0] tương đương với *x.
Tương tự,
- &x[1] tương đương với x+1 và x[1] tương đương với *(x+1).
- &x[2] tương đương với x+2 và x[2] tương đương với *(x+2).
- …
- Về cơ bản, &x[i] tương đương với x+i và x[i] tương đương với *(x+i).
Ví dụ 1: Biến con trỏ trong C và Mảng
#include <stdio.h>
int main() {
int i, x[6], sum = 0;
printf(“Enter 6 numbers: “);
for(i = 0; i < 6; ++i) {
// Equivalent to scanf(“%d”, &x[i]);
scanf(“%d”, x+i);
// Equivalent to sum += x[i]
sum += *(x+i);
}
printf(“Sum = %d”, sum);
return 0;
}
Khi bạn chạy lập trình này, đầu ra sẽ là
Enter 6 numbers: 2
3
4
4
12
4
Sum = 29
Tôi đã khai báo một mảng x gồm 6 phần tử. Để truy cập vào các phần tử của mảng này, bạn cần sử dụng biến pointer.
Trong hầu hết các ngữ cảnh, tên mảng sẽ phân rã thành biến con trỏ. Nói cách khác, tên mảng được chuyển đổi thành biến pointer.
Đó là lý do vì sao bạn nên sử dụng biến pointer trong C để truy cập vào các phần tử của mảng. Tuy nhiên, bạn cần lưu ý rằng mảng và biến con trỏ C không hề giống nhau.
Có vài trường hợp tên mảng không phân rã thành biến con trỏ C. Xem thông tin chi tiết tại: Khi nào tên mảng không phân rã thành biến pointer?
Ví dụ 2: Mảng và biến pointer trong C
#include <stdio.h>
int main() {
int x[5] = {1, 2, 3, 4, 5};
int* ptr;
// ptr is assigned the address of the third element
ptr = &x[2];
printf(“*ptr = %d \n”, *ptr); // 3
printf(“*(ptr+1) = %d \n”, *(ptr+1)); // 4
printf(“*(ptr-1) = %d”, *(ptr-1)); // 2
return 0;
}
Khi bạn chạy lập trình này, đầu ra sẽ là
*ptr = 3
*(ptr+1) = 4
*(ptr-1) = 2
Trong ví dụ này, &x[2] là địa chỉ của phần tử thứ 3 được gán với biến pointer ptr. Do đó, khi in *ptr thì đầu ra cũng sẽ hiển thị 3 trên màn hình luôn.
Và nếu in *(ptr+1) thì tôi sẽ nhận được phần tử thứ 4. Tương tự, nếu in *(ptr-1) thì tôi sẽ nhận được phần tử thứ 2.
4. Biến con trỏ trong C và hàm
4.1 Truyền địa chỉ và biến con trỏ C
Trong phần này, bạn sẽ học được cách truyền địa chỉ và biến pointer dưới dạng đối số vào các hàm thông qua các ví dụ.
Trong lập trình C, bạn có thể truyền địa chỉ dưới dạng đối số vào các hàm.
Thông qua các biến con trỏ, những địa chỉ này sẽ được chấp nhận vào định nghĩa hàm. Bởi vì biến con trỏ trong C được sử dụng để lưu trữ địa chỉ. Cùng xem qua các ví dụ dưới đây:
Ví dụ: Truyền địa chỉ vào hàm
#include <stdio.h>
void swap(int *n1, int *n2);
int main()
{
int num1 = 5, num2 = 10;
// address of num1 and num2 is passed
swap( &num1, &num2);
printf(“num1 = %d\n”, num1);
printf(“num2 = %d”, num2);
return 0;
}
void swap(int* n1, int* n2)
{
int temp;
temp = *n1;
*n1 = *n2;
*n2 = temp;
}
Khi chạy lập trình, đầu ra sẽ là:
num1 = 10
num2 = 5
Địa chỉ của num1 và num2 được truyền vào hàm swap() bằng cách sử dụng swap(&num1, &num2);.
Biến pointer n1 và n2 chấp nhận truyền những đối số này vào định nghĩa hàm.
void swap(int* n1, int* n2) {
… ..
}
Khi bạn thay đổi *n1 và *n2 trong hàm swap(), num1 và num2 trong hàm main() cũng thay đổi theo.
Trong hàm swap(),*n1 và *n2 được hoán đổi. Do đó, num1 và num2 cũng được hoán đổi theo.
Lưu ý, hàm swap() sẽ không trả về bất cứ giá trị nào bởi kiểu trả về của nó là void.
Ví dụ 2: Truyền biến pointer trong C vào các hàm
#include <stdio.h>
void addOne(int* ptr) {
(*ptr)++; // adding 1 to *ptr
}
int main()
{
int* p, i = 10;
p = &i;
addOne(p);
printf(“%d”, *p); // 11
return 0;
}
Ban đầu, giá trị được lưu trữ tại p, *p là 10.
Sau đó, tôi truyền biến pointer p vào hàm addOne(). Biến pointer ptr sẽ nhận địa chỉ trong hàm addOne().
Bên trong hàm này, tôi tăng giá trị lưu trữ tại ptr thêm 1 bằng cách sử dụng (*ptr)++;. Bởi vì biến pointer ptr và p có chung địa chỉ nên *p bên trong hàm main() cũng sẽ là 11.
5. Cấp phát bộ nhớ trong lập trình C
Trong phần này, bạn sẽ được học cách cấp phát tự động bộ nhớ trong lập trình C bằng cách sử dụng các hàm thư viện tiêu chuẩn: malloc(), calloc(), free() và realloc().
Như chúng ta đã biết, mảng là tập hợp của một số giá trị cố định. Sau khi khai báo xong mảng, bạn không thể thay đổi kích thước của nó.
Đôi khi kích thước mảng mà bạn khai báo có thể không đủ. Để giải quyết vấn đề này, bạn có thể cấp phát bộ nhớ một cách thủ công trong thời gian chạy lập trình. Cách thức này được xem là cấp phát bộ nhớ động trong lập trình C.
Để cấp phát bộ nhớ động, bạn sử dụng các hàm thư viện malloc(), calloc(), realloc() và free(). Các hàm này được xác định trong tệp tiêu đề <stdlib.h>.
5.1 Hàm malloc() trong lập trình C
Tên “malloc” là từ viết tắt của memory allocation (cấp phát bộ nhớ)
Hàm malloc() dự trữ một khối bộ nhớ chứa một lượng byte nhất định. Và, nó sẽ trả về biến pointer void. Biến này có thể được chuyển thành biến con trỏ dưới bất kỳ hình thức nào.
Cú pháp của hàm malloc()
ptr = (castType*) malloc(size);
Ví dụ,
ptr = (float*) malloc(100 * sizeof(float));
Câu lệnh trên cấp phát 400 byte của bộ nhớ. Bởi vì kích thước của float là 4 byte. Và biến pointer ptr giữ địa chỉ của byte đầu tiên trong bộ nhớ được cấp phát.
Nếu không thể cấp phát bộ nhớ thì biểu thức này sẽ có kết quả là biến pointer NULL.
5.2 Hàm calloc() trong lập trình C
Tên “calloc” tượng trưng cho contiguous allocation (cấp phát liên tục)
Hàm malloc cấp phát bộ nhớ và để bộ nhớ ở trạng thái chưa khởi tạo. Trong khi đó, hàm calloc() cấp phát bộ nhớ và khởi tạo tất cả các bit từ 0.
Cú pháp của hàm calloc()
ptr = (castType*)calloc(n, size);
Ví dụ:
ptr = (float *) calloc (25, sizeof (float));
Câu lệnh trên phân bổ không gian liền kề trong bộ nhớ cho 25 phần tử kiểu float.
5.3 Hàm free() trong lập trình C
Bộ nhớ được cấp phát động được tạo bằng hàm calloc() hoặc malloc() sẽ không tự giải phóng được. Bạn phải sử dụng hàm free() để giải phóng không gian.
Cú pháp của hàm free()
free(ptr);
Câu lệnh này giúp giải phóng không gian được cấp phát trong bộ nhớ được chỉ định bởi biến pointer ptr.
Ví dụ 1: hàm malloc() và free()
// Program to calculate the sum of n numbers entered by the user
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n, i, *ptr, sum = 0;
printf(“Enter number of elements: “);
scanf(“%d”, &n);
ptr = (int*) malloc(n * sizeof(int));
// if memory cannot be allocated
if(ptr == NULL)
{
printf(“Error! memory not allocated.”);
exit(0);
}
printf(“Enter elements: “);
for(i = 0; i < n; ++i)
{
scanf(“%d”, ptr + i);
sum += *(ptr + i);
}
printf(“Sum = %d”, sum);
// deallocating the memory
free(ptr);
return 0;
}
Trong ví dụ này, tôi đã cấp phát bộ nhớ cho n số int.
Ví dụ 2: hàm calloc() và free()
// Program to calculate the sum of n numbers entered by the user
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n, i, *ptr, sum = 0;
printf(“Enter number of elements: “);
scanf(“%d”, &n);
ptr = (int*) calloc(n, sizeof(int));
if(ptr == NULL)
{
printf(“Error! memory not allocated.”);
exit(0);
}
printf(“Enter elements: “);
for(i = 0; i < n; ++i)
{
scanf(“%d”, ptr + i);
sum += *(ptr + i);
}
printf(“Sum = %d”, sum);
free(ptr);
return 0;
}
5.4 Hàm realloc() trong lập trình C
Nếu bộ nhớ được cấp phát động không đủ hoặc nhiều hơn yêu cầu, bạn có thể thay đổi kích thước của bộ nhớ được cấp phát trước đó bằng cách sử dụng hàm realloc().
Cú pháp của hàm realloc()
ptr = realloc(ptr, x);
ptr được phân bổ lại với kích thước mới là x.
Ví dụ 3: hàm realloc()
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr, i , n1, n2;
printf(“Enter size: “);
scanf(“%d”, &n1);
ptr = (int*) malloc(n1 * sizeof(int));
printf(“Addresses of previously allocated memory: “);
for(i = 0; i < n1; ++i)
printf(“%u\n”,ptr + i);
printf(“\nEnter the new size: “);
scanf(“%d”, &n2);
// rellocating the memory
ptr = realloc(ptr, n2 * sizeof(int));
printf(“Addresses of newly allocated memory: “);
for(i = 0; i < n2; ++i)
printf(“%u\n”, ptr + i);
free(ptr);
return 0;
}
Khi chạy, lập trình này sẽ trả ra kết quả:
Enter size: 2
Addresses of previously allocated memory:26855472
26855476
Enter the new size: 4
Addresses of newly allocated memory:26855472
26855476
26855480
26855484
Trên đây là tất cả những thông tin bản chất của biến con trỏ trong C là gì. Hy vọng qua bài viết, bạn đã hiểu rõ được con trỏ là gì, biến con trỏ hay biến pointer trong C khởi tạo, khai báo như thế nào,..
Nếu bạn còn nhiều thắc mắc, hãy liên hệ tới Ironhack để giải đáp chuyên sâu bởi các chuyên gia IT lành nghề. Chúc bạn thành công!