1.Con trỏ là gì?

1.1 Con trỏ là gì?

Đầu tiên, ta có thể hiểu rằng khi khai báo biến, các biến đó đều có một vùng chứa nhất định trong bộ nhớ hay còn gọi là địa chỉ trong bộ nhớ, xét ví dụ dưới đây tôi khai báo 2 biến a và biến b sau đó kiểm tra xem chúng ở địa chỉ nào trong bộ nhớ bằng cách printf 2 biến a, b ra màn hình sau đó sử dụng ký hiệu &a, &b và sử dụng kiểu %x (thập lục phân) trong việc tìm ra địa chỉ của 2 biến trong bộ nhớ

#include <stdio.h>
int main(){
    int a = 10;
    int b[10];
    printf("Dia chi cua a trong bo nho la: %x",&a);
    printf("\nDia chi cua b trong bo nho la: %x",&b);
    return 0;
}
Dia chi cua a trong bo nho la: 62fe1c

Dia chi cua b trong bo nho la: 62fdf0

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả 62fe1c và 62fdf0 chỉ là kết quả minh họa.

Như vậy con trỏ có liên quan gì đến bộ nhớ và các biến? Cùng xem các định nghĩa dưới đây về con trỏ:

  • Con trỏ là một biến, nó chứa địa chỉ ô nhớ 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 này được gọi là con trỏ trỏ đến biến thứ hai
  • Con trỏ cung cấp phương thức truy xuất gián tiếp đến giá trị của một phần tử dữ liệu
  • Các con trỏ có thể trỏ đến các biến có kiểu dữ liệu cơ bản như int, char, double, hay dữ liệu tập hợp như mảng hoặc cấu trúc.

1.2 Con trỏ được sử dụng làm gì?

  • Các tình huống con trỏ có thể được sử dụng:
  • Để trả về nhiều hơn một giá trị từ một hàm
  • Để truyền mảng và chuỗi từ một hàm đến một hàm khác thuận tiện hơn
  • Để làm việc với các phần tử của mảng thay vì truy xuất trực tiếp vào các phần tử này
  • Để cấp phát bộ nhớ và truy xuất bộ nhớ (Cấp phát bộ nhớ trực tiếp)

2.Sử dụng con trỏ trong C

2.1 Khai báo biến con trỏ

Cú pháp khai báo biến con trỏ như sau:

type *name

Trong đó:

  • Type là kiểu dữ liệu (int, float, double, char…..)
  • Name là tên của con trỏ
  • Dấu * (dấu sao) trước tên là thành phần bắt buộc để khai báo rằng đó là biến con trỏ

Ví dụ dưới đây tôi khai báo con trỏ với một số kiểu dữ liệu thường gặp

#include <stdio.h>
int main(){
    int *a;
    float *b;
    double *c;
    char *d;
}

2.2 Các toán tử con trỏ

Trong con trỏ có 2 toán tử đặc biệt được sử dụng là toán từ (&) và (*)

  • (&) là toán tử trả về ô nhớ của biến
  • (*) là toán tử trả về giá trị chứa trong vùng nhớ được trỏ đến bởi biến con trỏ

Ví dụ dưới đây tôi có 2 biến đó là: a và biến con trỏ *p, tôi sử dụng toán tử (&) để trả về ô nhớ của biến a bằng cách khai báo biến con trỏ *p và gán biến con trỏ p = &a

#include <stdio.h>
int main(){
    //Khai bao bien a
    int a = 10;
    // Khai bao bien con tro *p
    int *p;
    // Gan con tro *p = &a
    p = &a;
    printf("Dia chi o nho cua bien la: %x",p);
}
Dia chi o nho cua bien la: 62fe14

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Giả sử tôi muốn lấy giá trị của con trỏ *p ở trên thì sao??

Như ở trên tôi đã đề cập đến toán tử (*) để lấy ra giá trị của con trỏ:

#include <stdio.h>
int main(){
    //Khai bao bien a
    int a = 10;
    // Khai bao bien con tro *p
    int *p;
    // Gan con tro *p = &a
    p = &a;
    printf("Dia chi o nho cua bien la: %x",p);
    // Hien thi gia tri con tro bang cach su dung toan tu *p
    printf("\nGia tri cua con tro p la: %d",*p);
}
Dia chi o nho cua bien la: 62fe14

Gia tri cua con tro p la: 10

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Ngoài ra tôi cũng có thể gán lại giá trị cho biến a bằng cách gán thông qua con trỏ *p

#include <stdio.h>
int main(){
    //Khai bao bien a ban dau
    int a = 10;
    printf("Gia tri bien a ban dau la: %d",a);
    // Khai bao bien con tro *p
    int *p;
    // Gan con tro *p = &a
    p = &a;
    //Gan gia tri moi cho bien a thong qua con tro *p
    *p = 20;
    printf("\nGia tri cua bien a sau khi duoc gan lai: %d",a);
}
Gia tri bien a ban dau la: 10

Gia tri cua bien a sau khi duoc gan lai: 20

2.3 Phép toán con trỏ

Con trỏ chỉ có thể thực hiện đươc hai phép toán đó là phép cộng và trừ trên con trỏ. Tôi có một ví dụ sau

int a, 
*p; 
p = &a; 
a = 500; 
p++; 

Giả sử biến a được lưu trữ tại địa chỉ 1000 , sau khi gán p = &a thì p lưu giá trị 1000. Sau biểu thức “p++;” p sẽ có giá trị là 1004 do số nguyên có kích thước là 4 bytes nên khi tăng p lên một đơn vị sẽ là 1000 + 4 = 1004

Tất cả con trỏ sẽ tăng hoặc giảm trị theo kích thước của kiểu dữ liệu mà chúng đang trỏ đến ví dụ kiểu int ở trên kích thước là 4bytes nên tăng kích thước là +4 và giảm kích thước là -4. Các bạn có thể đọc lại bài các kiểu dữ liệu căn bản trong C để hiểu rõ thêm một số kích thước của một số kiểu dữ liệu khác

Xét a và p trên ví dụ trên tôi có một số phép toán con trỏ như sau:

Phép toán Ý nghĩa
++p hoặc p++ Trỏ đến số nguyên được lưu trữ kế tiếp sau a
–p hoặc p — Trỏ đến số nguyên được lưu trữ liền trước a
p + i Trỏ đến số nguyên được lưu trữ i vị trí sau a
p – i Trỏ đến số nguyên được lưu trữ i vị trí trước a
(*p)++ Tăng giá trị của a lên 1

Chú ý vào phép toán p++ và p + i, vì đây là 2 phép toán sẽ được sử dụng nhiều khi thao tác với con trỏ. (Đặc biệt là thao tác con trỏ với mảng ở bài sau)

Xét ví dụ dưới đây cho các phép toán:

Phép toán: ++p hoặc p++

#include <stdio.h>
int main(){
    int a;
    int *p; 
    p = &a; 
    a = 500; 
    printf("Dia chi o nho ban dau la: %x", p);
    p++; 
    printf("\nDia chi o nho khi p++ la: %x", p);
}
Dia chi o nho ban dau la: 62fe14

Dia chi o nho khi p++ la: 62fe18

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Phép toán: –p hoặc p –

#include <stdio.h>
int main(){
    int a;
    int *p; 
    p = &a; 
    a = 500; 
    printf("Dia chi o nho ban dau la: %x", p);
    p--; 
    printf("\nDia chi o nho khi p++ la: %x", p);
}
Dia chi o nho ban dau la: 62fe14

Dia chi o nho khi p++ la: 62fe10

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Phép toán: p + i (ở đây tôi dùng vòng lặp để p cộng với i = 2 lần, nghĩa là p + 2)

#include <stdio.h>
int main(){
    int a;
    int *p; 
    p = &a; 
    a = 500; 
    printf("Dia chi o nho ban dau la: %x", p);
    //Dung vong lap de cong p voi i lan
    for (int i = 0; i <= 2; i++){
        printf("\nDia chi o nho khi p + %d la: %x",i , p);
        p++;
    }
}
Dia chi o nho ban dau la: 62fe10

Dia chi o nho khi p + 0 la: 62fe10

Dia chi o nho khi p + 1 la: 62fe14

Dia chi o nho khi p + 2 la: 62fe18

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Phép toán: p – i (ở đây tôi dùng vòng lặp để p trừ với i = 2 lần, nghĩa là p – 2)

#include <stdio.h>
int main(){
    int a;
    int *p; 
    p = &a; 
    a = 500; 
    printf("Dia chi o nho ban dau la: %x", p);
    //Dung vong lap de tru p voi i lan
    for (int i = 0; i <= 3; i++){
        p--;
        printf("\nDia chi o nho khi p - %d la: %x",i , p);
    }
}
Dia chi o nho ban dau la: 62fe10

Dia chi o nho khi p – 0 la: 62fe0c

Dia chi o nho khi p – 1 la: 62fe08

Dia chi o nho khi p – 2 la: 62fe04

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Phép toán: (*p)++ (tăng giá trị của a lên 1 đơn vị)

#include <stdio.h>
int main(){
    int a;
    int *p; 
    p = &a;
    a= 500; 
    printf("Gia tri a ban dau la: %d", a);
    //Tang gia tri cua a len 1
    (*p)++;
    printf("\nGia tri a sau khi (*p)++ la: %d", a);
}
Gia tri a ban dau la: 500

Gia tri a sau khi (*p)++ la: 501

2.3 So sánh hai con trỏ

Hai con trỏ có thể được so sánh trong một biểu thức quan hệ nếu chúng trỏ đến các biến có cùng kiểu dữ liệu

Giả sử ptr_a và ptr_b là hai biến con trỏ trỏ đến các phần tử dữ liệu a và b. Trong trường hợp này, các phép so sánh sau là có thể:

ptr_a < ptr_b Trả về giá trị true nếu a được lưu trữ ở vị trí trước b
ptr_a > ptr_b Trả về giá trị true nếu a được lưu trữ ở vị trí sau b
ptr_a <= ptr_b Trả về giá trị true nếu a được lưu trữ ở vị trí trước b hoặc

ptr_a và ptr_b trỏ đến cùng một vị trí

ptr_a >= ptr_b Trả về giá trị true nếu a được lưu trữ ở vị trí sau b hoặc

ptr_a và ptr_b trỏ đến cùng một vị trí

ptr_a == ptr_b Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ

đến cùng một phần tử dữ liệu.

ptr_a != ptr_b Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ

đến các phần tử dữ liệu khác nhau nhưng có cùng kiểu

dữ liệu.

Tôi sẽ đưa ra một ví dụ mẫu về việc so sánh con trỏ ptr_a < ptr_b, các phép so sánh sau các bạn có thể tự mình thực hiện.

#include <stdio.h>
int main(){
    int a;
    int b;
    int *ptr_a;
    int *ptr_b; 
    ptr_a = &a;
    ptr_b = &b;
    printf("ptr_a la: %x \n",ptr_a);
    printf("ptr_b la: %x ",ptr_b);
    //Ket qua so sanh bang 1 nghia la ptr_a be hon ptr_b nguoc lai la ptr_a khong be hon ptr_b
    printf("\nKet qua phep so sanh ptr_a < ptr_b la: %d", ptr_a < ptr_b);
}
ptr_a la: 62fe0c

ptr_b la: 62fe08

Ket qua phep so sanh ptr_a < ptr_b la: 0

Kết quả bằng 0 vậy nên ptr_a không bé hơn ptr_b

Các bạn có thể tự mình kiểm tra các phép so sánh khác ở trên.

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

3. Con trỏ NULL

Con trỏ NULL nghĩa là việc ta gán giá trị cho con trỏ đó bằng NULL (rỗng).

Lưu ý rằng NULL ở đây không giống với số 0, nếu gán con trỏ bằng số 0 thì con trỏ đó không phải là con trỏ NULL mà ngược lại con trỏ được gán bằng số 0 cũng giống như mọi con trỏ bình thường được gán bởi các giá trị khác như 1,2,3,.v.v

#include <stdio.h>
int main ()
{
   int  *p = NULL;
   printf("Dia chi cua con tro p la: %x\n", &p);
   return 0;
}
Dia chi cua con tro p la: 62fe18

Chú ý: Con trỏ Null cũng có một địa chỉ trong bộ nhớ, tuy nhiên giá trị ở địa chỉ đó bằng rỗng (hay còn hiểu là NULL)

Để kiểm tra con trỏ có phải là con trỏ NULL hay không ta sử dụng câu lệnh dưới đây:

p == NULL Trả về giá trị true (hay số 1) nếu p được gán giá trị NULL
#include <stdio.h>
int main ()
{
   int  *p = NULL;
   printf("Ket qua kiem tra con tro NULL la: %d", p == NULL);
   return 0;
}
Ket qua kiem tra con tro NULL la: 1