Bài viết trước bạn đã được học về mảng. Bài viết này sẽ tiếp tục giới thiệu với bạn về một kiểu dữ liệu phức tạp tiếp theo, cũng khá quan trọng. Đó chính là con trỏ.
Lý thuyết cơ bản về con trỏ trong lập trình C
Con trỏ là gì?
Con trỏ chính là một biến, chứa địa chỉ của một biến khác.
Nếu một biến chứa địa chỉ của một biến khác, thì biến đó được gọi là trỏ đến địa chỉ của biến còn lại.
Con trỏ cung cấp một cách gián tiếp để truy cập vào giá trị của 1 biến mà nó trỏ vào.
Con trỏ được sử dụng trong những trường hợp nào?
Có một vài trường hợp mà bạn có thể sử dụng con trỏ là:
- Để trả về nhiều hơn một giá trị với hàm
- Để truyền mảng hay string từ một hàm này sang hàm khác
- Để truy cập vào mảng một cách dễ dàng hơn bằng cách dịch chuyển con trỏ
Cách khai báo con trỏ
Cú pháp khai báo con trỏ là:
data_type* name;
Trong đó:
- data_type là kiểu dữ liệu của con trỏ (int, double, float, char,...)
- name là tên của biến con trỏ
Ví dụ, để khai báo một biến con trỏ kiểu int:
int* pointer;
Những toán tử với con trỏ
Có 2 toán tử thường dùng với con trỏ là: toán tử (&) và toán tử (*)
Toán tử (&) là toán tử một ngôi, trả về địa chỉ của một biến.
Ví dụ:
int x = 3;
int* p = &x; // Con trỏ p trỏ vào địa chỉ của biến x
Toán tử (*) là toán tử một ngôi, trả về giá trị của biến mà con trỏ trỏ tới.
Ví dụ:
int x = 3;
int* p = &x;
printf("%d", *p); // In ra giá trị của biến mà con trỏ trỏ tới, là biến x
Khởi tạo giá trị cho con trỏ
Có 2 cách khởi tạo giá trị cho con trỏ.
Cách 1: Sử dụng toán tử (&)
Ví dụ, gán giá trị cho con trỏ p là địa chỉ của biễn x:
int x = 3;
int* p = &x; // Con trỏ p trỏ vào địa chỉ của biến x
Cách 2: Sử dụng thông qua một con trỏ khác
Ví dụ, gán giá trị cho con trỏ p thông qua con trỏ q:
int x = 3;
int* q = &x; // Con trỏ q trỏ vào địa chỉ của biến x
int* p = q; // Con trỏ p trỏ vào địa chỉ giống với con trỏ q
Ngoài ra, có thể thay đổi giá trị của biến mà con trỏ đang trỏ vào, ví dụ:
int x = 3;
int* p = &x; // Con trỏ p trỏ vào địa chỉ của biến x
*p = 10; // Gán trị mới cho biến mà con trỏ p đang chỉ vào,
// tức sau câu lệnh này, giá trị của x = 10
Phép toán số học với con trỏ
Đối với con trỏ, chỉ được sử dụng phép cộng với số nguyên hoặc phép trừ với số nguyên.
Vì bản chất, con trỏ trỏ vào địa chỉ của một biến. Khi thực hiện phép cộng với số nguyên, con trỏ sẽ được dịch lên. Khi thực hiện phép trừ với số nguyên thì con trỏ sẽ dịch xuống.
Ví dụ:
int x = 3;
int* p = &x; // Con trỏ p trỏ vào địa chỉ của biến x
p = p + 1;
p = p - 2;
Trong ví dụ trên, ban đầu con trỏ p trỏ vào địa chỉ của biến x, giả sử là 1000.
Khi thực hiện p = p + 1 thì con trỏ p sẽ dịch lên 1 đơn vị (đơn vị chính là kích thước của kiểu dữ liệu con trỏ). Cụ thể, kiểu int có kích thước 4 bytes, nên lúc này con trỏ p trỏ vào địa chỉ mới là 1000 + 4 = 1004.
Tương tự khi thực hiện, p = p - 2 thì con trỏ p sẽ dịch về 2 đơn vị, tức dịch về 8 bytes. Nên con trỏ p sẽ trỏ vào địa chỉ là 1004 - 8 = 996.
Phép toán so sánh với con trỏ
Có thể so sánh 2 con trỏ với sau sử dụng các toán tử so sánh.
Giả sử có 2 con trỏ p_a và p_b lần lượt trỏ vào địa chỉ 2 biến a và b, khi đó:
- p_a < p_b trả về true nếu p_a trỏ vào vùng nhớ trước p_b
- p_b < p_a trả về true nếu p_a trỏ vào vùng nhớ sau p_b
- p_a <= p_b trả về true nếu p_a trỏ vào vùng nhớ trước p_b hoặc giống p_b
- p_a >= p_b trả về true nếu p_a trỏ vào vùng nhớ sau p_b hoặc giống p_b
- p_a == p_b trả về true nếu p_a và p_b trỏ vào cùng một vùng nhớ
- p_a != p_b trả về true nếu p_a và p_b trỏ vào 2 vùng nhớ khác nhau
- p_a == NULL nếu p_a gán giá trị NULL
Mối quan hệ giữa con trỏ và mảng một chiều
Giả sử cho mảng một chiều như sau:
int a[11];
Lúc này, tên mảng (a) được hiểu là một con trỏ, trỏ vào vị trí đầu tiên của mảng. Do đó, có 2 cách để biểu diễn địa chỉ của mỗi phần tử trong mảng.
Cách 1: Sử dụng toán tử (&). Với cách này, địa chỉ các phần tử của mảng a là: &a[0], &a[1],..., &a[9]
Cách 2: Sử dụng chỉ số. Với cách này, địa chỉ các phần tử của mảng a là: a, a + 1, a + 2,..., a + 9
Đối với mảng 2 chiều, ví dụ:
int b[2][3];
Mảng 2 chiều trên có thể biểu diễn dưới dạng:
int* b[2]
Con trỏ và string
Trong bài viết trước, bạn đã được học về 2 hàm xử lý string liên quan đến con trỏ là: strchr() và strstr().
Trong đó:
- Hàm strchr(str, a): trả về con trỏ ứng với vị trí xuất hiện đầu tiên của kí tự a trong chuỗi str
- Hàm strstr(str, substr): trả về con trỏ ứng với vị trí xuất hiện đầu tiên của chuỗi substr trong chuỗi str
Ví dụ:
#include <stdio.h>
#include <string.h>
int main ()
{
char a, str[81], *ptr;
printf("\nEnter a sentence:");
gets(str);
printf("\nEnter character to search for:");
a = getchar();
ptr = strchr(str, a);
/* return pointer to char*/
printf("\nString starts at address: %u", str);
printf("\nFirst occurrence of the character is at address: %u", ptr);
printf("\n Position of first occurrence(starting from 0)is: %d", ptr - str);
return 0;
}
Cách cấp phát bộ nhớ động cho con trỏ
Hàm malloc()
Hàm malloc() dùng để cấp phát bộ nhớ động, với cú pháp:
void* p = malloc(unsigned int size);
Trong cú pháp trên:
- size là số lượng byte cần cấp phát.
- Hàm trên trả về kiểu con trỏ void*, nghĩa là tùy thuộc vào kiểu dữ kiệu mong muốn, bạn cần kép kiểu về kiểu dữ liệu mong muốn đó.
Ví dụ cấp phát động mảng số nguyên (int) N phần tử:
int* p = (int*)malloc(N * sizeof(int));
Ví dụ cấp phát động mảng số nguyên (int) N phần tử rồi nhập vào giá trị cho các phần tử:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main ()
{
// Nhap vao N
int N;
printf("Nhap N: ");
scanf("%d", &N);
// Cap phat dong mang int gom N phan tu
int* a = (int*) malloc(N * sizeof(int));
// Nhap gia tri cho cac phan tu mang
int i;
for(i = 0; i < N; i += 1) {
printf("Nhap vao phan tu %d la: ", i);
scanf("%d", &a[i]);
}
// In ra gia tri da nhap
for(i = 0; i < N; i += 1) {
printf("%d ", a[i]);
}
return 0;
}
Hàm calloc()
Hàm calloc() có chức năng tương tự với hàm malloc() chỉ khác là bộ nhớ được cấp phát động với hàm calloc() có giá trị mặc định là 0.
Cú pháp hàm calloc() là:
void* p = calloc(unsigned int num, unsigned int size);
Trong đó:
- num: số lượng phần tử
- size: kích thước của mỗi phần tử
- Hàm trên trả về kiểu con trỏ void*, nghĩa là tùy thuộc vào kiểu dữ kiệu mong muốn, bạn cần kép kiểu về kiểu dữ liệu mong muốn đó.
Ví dụ cấp phát động mảng số nguyên (int) N phần tử:
int* p = (int*)calloc(N, sizeof(int));
Ví dụ cấp phát động mảng số nguyên (int) N phần tử rồi nhập vào giá trị cho các phần tử:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main ()
{
// Nhap vao N
int N;
printf("Nhap N: ");
scanf("%d", &N);
// Cap phat dong mang int gom N phan tu
int* a = (int*) calloc(N, sizeof(int));
// Nhap gia tri cho cac phan tu mang
int i;
for(i = 0; i < N; i += 1) {
printf("Nhap vao phan tu %d la: ", i);
scanf("%d", &a[i]);
}
// In ra gia tri da nhap
for(i = 0; i < N; i += 1) {
printf("%d ", a[i]);
}
return 0;
}
Hàm realloc()
Giả sử bạn đã cấp phát động với kích thước N bytes cho một mảng động. Tuy nhiên, sau đó bạn muốn thêm các phần tử mới vào mảng.
Có 2 cách xử lý:
- Copy tất cả dữ liệu mảng vào một mảng trung gian lớn hơn, đủ để add thêm các phần tử vào mảng
- Bạn dùng hàm realloc() để mở rộng phạm vi của một biến
Cú pháp khai báo với hàm realloc() là:
void* realloc(void *p, unsigned int size);
Trong đó:
- p: là con trỏ ban đầu
- size: là kích thước mới của con trỏ p mà bạn muốn cấp phát
- Hàm trên trả về kiểu con trỏ void*, nghĩa là tùy thuộc vào kiểu dữ kiệu mong muốn, bạn cần kép kiểu về kiểu dữ liệu mong muốn đó.
Chú ý: Nếu size = 0 thì tương đương với việc giải phóng bộ nhớ
Ví dụ 1:
// Ban đầu cấp phát động mảng gồm 5 phần tử
int *a = (int*)calloc(5, sizeof(int));
// Sau đó bạn muốn cấp phát thêm 2 phần tử thành 7 phần tử
a = (int*)realloc(a, 7*sizeof(int));
Ví dụ 2:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int i;
ptr = (int*)calloc(5, sizeof(int));
if(ptr != NULL) {
*ptr = 1;
*(ptr+1) = 2;
ptr[2] = 4;
ptr[3] = 8;
ptr[4] = 16;
ptr = (int*)realloc(ptr, 7*sizeof(int));
if(ptr != NULL) {
printf("Now allocating more memory... \n");
ptr[5] = 32; /* now it's legal! */
ptr[6] = 64;
for(i=0; i<7; i++) {
printf("ptr[%d] holds %d\n", i, ptr[i]);
}
realloc(ptr, 0); /* same as free(ptr); - just fancier! */
return 0;
} else {
printf("Not enough memory - realloc failed.\n");
return 1;
}
} else {
printf("Not enough memory - calloc failed.\n");
return 1;
}
}
Hàm free()
Đối với biến auto thông thường, vùng nhớ sẽ được giải phóng sau khi ra ngoài block. Tuy nhiên, với biến được cấp phát động thì nó sẽ tồn tại cho đến hết chương trình.
Vì vậy, khi cấp phát bộ nhớ động bạn cần chú ý giải phóng bộ nhớ sau khi sử dụng xong để tránh bị leak memory.
Để giải phóng bộ nhớ được cấp phát động khi sử dụng các hàm malloc(), calloc() hay realloc(), bạn có thể dùng hàm free() với cú pháp như sau:
void free(void *ptr);
Ví dụ 1:
// cấp phát với malloc, calloc, realloc
int *p1 = (int*)malloc(5*sizeof(int));
int *p2 = (int*)calloc(5, sizeof(int));
int *p3 = (int*)realloc(p2, 7*sizeof(int));
// giải phóng với free
free(p1);
free(p2);
free(p3);
Ví dụ 2:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = NULL;
int i = 0;
while(1) {
if (ptr != NULL) {
free(ptr);
}
if (i == 1024*1024) {
scanf("%d", &i);
if (i == 0) break;
}
printf("loop at %d\n", i++);
ptr = (int*)calloc(5000, sizeof(int));
// do some thing
}
}
Trong ví dụ trên, nếu bạn không giải phóng bộ nhớ với hàm free() ở đầu vòng lặp while thì việc leak memory xảy ra làm cho bộ nhớ sử dụng tăng lên liên tục và rất nhiều.
Bài tập thực hành về con trỏ trong lập trình C
Bài 1
Nhập vào mảng số nguyên gồm N phần tử (0 < N <= 50). Sử dụng cấp phát động và con trỏ để:
- In ra mảng theo thứ tự từ đầu đến cuối
- In ra mảng theo thứ tự từ cuối lên đầu
#include <stdio.h>
#include <stdlib.h>
int main() {
int N, kiemTra = 0, i;
int *p = NULL; // Khoi tao gia tri NULL cho con tro p
printf("Nhap N: ");
kiemTra = scanf("%d", &N);
if (kiemTra == 0 || N <= 0 || N > 50) {
printf("Ban da nhap sai");
return 1;
}
p = (int*) calloc(N, sizeof(int));
if (p == NULL) {
printf("Khong du bo nho");
return 1;
}
printf("Nhap vao %d phan tu la: ", N);
for(i = 0; i < N; i++) {
scanf("%d", p + i);
}
printf("In ra mang tu dau den cuoi: ");
for(i = 0; i < N; i++) {
printf("%d ", *(p + i));
}
printf("\n");
printf("In ra mang tu cuoi len dau: ");
for(i = N-1; i >= 0; i--) {
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
Bài 2
Nhập vào mảng gồm N phần tử (0 < N <= 50). Sử dụng cấp phát động và con trỏ để:
- Tính và in ra giá trị trung bình của các phần tử chẵn
- Tìm và in ra giá trị lẻ lớn nhất
#include <stdio.h>
#include <stdlib.h>
int main() {
int N, kiemTra = 0, i, tongChan = 0, cntChan = 0;
int maxLe;
int *p = NULL; // Khoi tao gia tri NULL cho con tro p
printf("Nhap N: ");
kiemTra = scanf("%d", &N);
if (kiemTra == 0 || N <= 0 || N > 50) {
printf("Ban da nhap sai");
return 1;
}
p = (int*) calloc(N, sizeof(int));
if (p == NULL) {
printf("Khong du bo nho");
return 1;
}
printf("Nhap vao %d phan tu la: ", N);
for(i = 0; i < N; i++) {
scanf("%d", p + i);
}
// Tim phan tu chan
for(i = 0; i < N; i++) {
if (*(p + i) % 2 == 0) {
tongChan += *(p + i);
cntChan++;
}
}
printf("Gia tri trung binh cua cac phan tu chan: %g\n", (float)tongChan/cntChan);
// Tim gia tri le lon nhat
maxLe = *(p + 0);
for(i = 1; i < N; i++) {
if (*(p + i) % 2 != 0 && *(p + i) > maxLe) {
maxLe = *(p + i);
}
}
printf("Gia tri le lon nhat: %d\n", maxLe);
return 0;
}
Bài 3
Nhập vào chuỗi có độ dài tối đa 20 phần tử. Tính độ dài của chuỗi bằng 2 cách:
- Cách 1: sử dụng kiến thức về mảng
- Cách 2: sử dụng con trỏ
#include <stdio.h>
int main() {
char str[21];
int i;
printf("Nhap vao chuoi toi da 20 phan tu: ");
scanf("%s", str);
// Tinh do dai chuoi dung mang
i = 0;
while(str[i] != '\0') {
i++;
}
printf("Do dai chuoi nhap vao la: %d\n", i);
// Tinh do dai chuoi dung mang
i = 0;
while(*(str + i) != '\0') {
i++;
}
printf("Do dai chuoi nhap vao la: %d\n", i);
return 0;
}