Con trỏ là một trong những thành phần cốt lõi của ngôn ngữ lập trình C. Một con trỏ có thể được sử dụng để lưu giữ địa chỉ bộ nhớ của các biến khác, các hàm, hoặc thậm chí cả con trỏ khác. Việc sử dụng con trỏ cho phép truy cập vào bộ nhớ ở mức độ thấp, cấp phát bộ nhớ động, và nhiều chức năng khác trong ngôn ngữ C.
Trong bài viết này, chúng ta sẽ thảo luận về con trỏ trong ngôn ngữ C chi tiết, các loại con trỏ, cách sử dụng, ưu điểm và nhược điểm cùng với ví dụ minh họa.
Những gì là con trỏ trong ngôn ngữ C?
Một con trỏ được định nghĩa như một kiểu dữ liệu phái sinh có thể lưu giữ địa chỉ của các biến khác trong ngôn ngữ C hoặc một vị trí bộ nhớ. Chúng ta có thể truy cập và thao tác dữ liệu được lưu trữ trong vị trí bộ nhớ đó bằng cách sử dụng con trỏ.
Vì con trỏ trong C lưu trữ các địa chỉ bộ nhớ, kích thước của chúng là độc lập với loại dữ liệu mà chúng đang trỏ đến. Kích thước của con trỏ trong C chỉ phụ thuộc vào kiến trúc hệ thống.
Cú pháp của con trỏ trong ngôn ngữ C
Cú pháp của con trỏ tương tự như khai báo biến trong C, nhưng chúng ta sử dụng toán tử dereferencing (*
) trong khai báo con trỏ.
datatype *ptr;
Trong đó:
ptr
là tên của con trỏ.datatype
là kiểu dữ liệu mà con trỏ đang trỏ tới.
Cú pháp trên được sử dụng để định nghĩa một con trỏ tới một biến. Chúng ta cũng có thể định nghĩa con trỏ tới các hàm, cấu trúc, v.v.
Cách sử dụng con trỏ?
Việc sử dụng con trỏ trong C có thể được chia thành ba bước:
- Khai báo con trỏ
- Khởi tạo con trỏ
- Truy cập giá trị được lưu trữ trong con trỏ
1. Khai báo con trỏ
Trong bước khai báo con trỏ, chúng ta chỉ khai báo con trỏ mà không khởi tạo nó. Để khai báo một con trỏ, chúng ta sử dụng toán tử dereferencing (*
) trước tên của con trỏ.
Ví dụ:
int *ptr;
Con trỏ được khai báo ở đây sẽ trỏ đến một địa chỉ bộ nhớ ngẫu nhiên vì nó chưa được khởi tạo. Các con trỏ như vậy được gọi là wild pointers.
2. Khởi tạo con trỏ
Khởi tạo con trỏ là quá trình gán một giá trị khởi tạo cho biến con trỏ. Chúng ta thường sử dụng toán tử địa chỉ (&
) để lấy địa chỉ bộ nhớ của một biến và gán nó vào biến con trỏ.
Ví dụ:
int var = 10; int *ptr; ptr = &var;
Chúng ta cũng có thể khai báo và khởi tạo con trỏ trong một bước duy nhất. Phương pháp này được gọi là định nghĩa con trỏ vì con trỏ được khai báo và khởi tạo cùng một lúc.
Ví dụ:
int *ptr = &var;
Lưu ý: Để đảm bảo an toàn, nên luôn khởi tạo con trỏ thành một giá trị trước khi sử dụng nó. Nếu không, có thể dẫn đến một số lỗi.
3. Truy cập giá trị trong con trỏ
Việc giải tham chiếu một con trỏ là quá trình truy cập vào giá trị được lưu trữ trong địa chỉ bộ nhớ được chỉ định trong con trỏ. Chúng ta sử dụng cùng toán tử dereferencing (*
) mà chúng ta đã sử dụng trong khai báo con trỏ.
Ví dụ về con trỏ trong ngôn ngữ C
// Chương trình C minh họa việc sử dụng con trỏ #include void geeks() { int var = 10; // khai báo biến int* ptr; // khai báo con trỏ ptr = &var; // gán địa chỉ của biến vào con trỏ // in ra giá trị của con trỏ, biến và giá trị được trỏ tới printf("Giá trị của ptr = %p\n", ptr); printf("Giá trị của var = %d\n", var); printf("Giá trị được trỏ tới bởi ptr = %d\n", *ptr); } // Chương trình chính int main() { geeks(); return 0; }
Kết quả:
Giá trị của ptr = 0x7ffd662303cc Giá trị của var = 10 Giá trị được trỏ tới bởi ptr = 10
Các loại con trỏ trong ngôn ngữ C
Con trỏ trong C có thể được phân loại thành nhiều loại khác nhau dựa trên tham số mà chúng ta đang định nghĩa loại con trỏ. Nếu chúng ta xem xét loại biến được lưu trữ trong vị trí bộ nhớ được con trỏ trỏ đến, thì con trỏ có thể được phân loại thành các loại sau:
1. Con trỏ số nguyên
Như tên gọi, đây là những con trỏ trỏ đến các giá trị số nguyên.
Cú pháp:
int *ptr;
Các con trỏ này được phát âm là "Con trỏ tới Số nguyên".
Tương tự, con trỏ có thể trỏ đến bất kỳ kiểu dữ liệu nguyên thủy nào khác. Chúng có thể trỏ cũng như truy cập vào kiểu dữ liệu dẫn xuất như mảng và kiểu dữ liệu do người dùng định nghĩa như cấu trúc.
2. Con trỏ mảng
Con trỏ và mảng trong C liên quan mật thiết với nhau. Ngay cả tên mảng cũng là con trỏ tới phần tử đầu tiên của mảng. Chúng cũng được gọi là "Con trỏ tới Mảng". Chúng ta có thể tạo một con trỏ tới một mảng bằng cách sử dụng cú pháp sau:
Cú pháp:
char *ptr = &array_name;
Con trỏ tới mảng có một số tính chất thú vị được thảo luận sau.
3. Con trỏ cấu trúc
Con trỏ trỏ đến loại dữ liệu cấu trúc được gọi là con trỏ cấu trúc hoặc con trỏ tới cấu trúc. Chúng có thể được khai báo giống như các kiểu dữ liệu nguyên thủy khác.
Cú pháp:
struct struct_name *ptr;
Trong C, con trỏ cấu trúc được sử dụng trong các cấu trúc dữ liệu như danh sách liên kết, cây, v.v.
4. Con trỏ hàm
Con trỏ hàm trỏ đến các hàm. Chúng khác biệt với các loại con trỏ khác ở chỗ thay vì trỏ đến dữ liệu, chúng trỏ đến mã nguồn của hàm. Hãy xem xét một nguyên mẫu hàm - int func(int, char)
. Con trỏ hàm cho hàm này sẽ là:
Cú pháp:
int (*ptr)(int, char);
Lưu ý: Cú pháp của con trỏ hàm thay đổi theo nguyên mẫu hàm.
5. Con trỏ hai lần
Trong ngôn ngữ lập trình C, chúng ta có thể định nghĩa một con trỏ lưu giữ địa chỉ bộ nhớ của một con trỏ khác. Các con trỏ như vậy được gọi là con trỏ hai lần hoặc con trỏ tới con trỏ. Thay vì trỏ đến một giá trị dữ liệu, chúng trỏ đến một con trỏ.
Cú pháp:
datatype **pointer_name;
Ví dụ:
int **ptr;
Lưu ý: Trong C, chúng ta có thể tạo ra các con trỏ đa cấp với bất kỳ số cấp nào như - ***ptr3
, ****ptr4
, *****ptr5
và cứ tiếp tục.
6. Con trỏ NULL
Con trỏ null là những con trỏ không trỏ đến bất kỳ vị trí bộ nhớ nào. Chúng có thể được tạo bằng cách gán giá trị NULL cho con trỏ. Một con trỏ của bất kỳ kiểu nào cũng có thể được gán giá trị NULL.
Cú pháp:
data_type *pointer_name = NULL;
Nên gán giá trị NULL cho các con trỏ hiện tại không được sử dụng.
7. Con trỏ void
Con trỏ void trong C là con trỏ có kiểu void. Điều này có nghĩa là chúng không có bất kỳ kiểu dữ liệu được liên kết nào. Chúng được gọi là "con trỏ chung" vì chúng có thể trỏ vào bất kỳ kiểu dữ liệu nào và có thể được ép kiểu thành bất kỳ kiểu dữ liệu nào.
Cú pháp:
void *pointer_name;
Một trong những thuộc tính chính của con trỏ void là chúng không thể được giải tham chiếu.
8. Con trỏ hoang
Con trỏ hoang là những con trỏ chưa được khởi tạo bằng một giá trị nào đó. Loại con trỏ C này có thể gây ra vấn đề trong các chương trình của chúng ta và trong tương lai có thể gây ra sự cố. Nếu giá trị được cập nhật bằng con trỏ hoang, chúng có thể gây ra gián đoạn dữ liệu hoặc làm hỏng dữ liệu.
Ví dụ:
int *ptr; char *str;
9. Con trỏ hằng
Trong con trỏ hằng, địa chỉ bộ nhớ được lưu trữ bên trong con trỏ là hằng số và không thể được thay đổi sau khi nó được định nghĩa. Nó luôn chỉ trỏ đến cùng một địa chỉ bộ nhớ.
Cú pháp:
data_type * const pointer_name;
10. Con trỏ tới hằng
Con trỏ trỏ đến một giá trị hằng mà không thể thay đổi được gọi là con trỏ tới hằng. Ở đây, chúng ta chỉ có thể truy cập vào dữ liệu mà con trỏ trỏ đến, nhưng không thể thay đổi nó. Tuy nhiên, chúng ta có thể thay đổi địa chỉ được lưu trữ trong con trỏ đến hằng.
Cú pháp:
const data_type *pointer_name;
Kích thước của con trỏ trong ngôn ngữ C
Kích thước của con trỏ trong C là bằng nhau cho mọi loại con trỏ. Kích thước của con trỏ không phụ thuộc vào kiểu dữ liệu mà chúng đang trỏ đến. Nó chỉ phụ thuộc vào hệ điều hành và kiến trúc CPU. Kích thước của con trỏ trong C là:
- 8 byte cho một Hệ thống 64-bit
- 4 byte cho một Hệ thống 32-bit
Lý do cho cùng một kích thước là con trỏ lưu trữ địa chỉ bộ nhớ, bất kể kiểu dữ liệu mà chúng trỏ đến. Vì không gian cần thiết để lưu trữ các địa chỉ của các vị trí bộ nhớ khác nhau là giống nhau, bộ nhớ cần thiết cho một loại con trỏ sẽ bằng với bộ nhớ cần thiết cho các loại con trỏ khác.
Làm thế nào để tìm kích thước của con trỏ trong C?
Chúng ta có thể tìm kích thước của con trỏ bằng cách sử dụng toán tử sizeof
, như được thể hiện trong ví dụ sau:
// Chương trình C để tìm kích thước các loại con trỏ khác nhau #include // cấu trúc giả struct str { int a; }; // hàm giả void func(int a, int b) {} int main() { // các biến giả int a = 10; char c = 'G'; struct str x; // khai báo con trỏ của các loại khác nhau int* ptr_int = &a; char* ptr_char = &c; struct str* ptr_str = &x; void (*ptr_func)(int, int) = &func; void* ptr_vn = NULL; // in ra kích thước printf("Kích thước của con trỏ số nguyên: %lu byte\n", sizeof(ptr_int)); printf("Kích thước của con trỏ ký tự: %lu byte\n", sizeof(ptr_char)); printf("Kích thước của con trỏ cấu trúc: %lu byte\n", sizeof(ptr_str)); printf("Kích thước của con trỏ hàm: %lu byte\n", sizeof(ptr_func)); printf("Kích thước của con trỏ NULL Void: %lu byte", sizeof(ptr_vn)); return 0; }
Kết quả:
Kích thước của con trỏ số nguyên: 8 byte Kích thước của con trỏ ký tự: 8 byte Kích thước của con trỏ cấu trúc: 8 byte Kích thước của con trỏ hàm: 8 byte Kích thước của con trỏ NULL Void: 8 byte
Con trỏ trong ngôn ngữ C và mảng
Con trỏ và mảng trong ngôn ngữ C liên quan chặt chẽ với nhau. Thậm chí tên mảng cũng là một con trỏ tới phần tử đầu tiên của mảng. Chúng ta có thể tạo một con trỏ tới một mảng bằng cách sử dụng cấu trúc sau:
data_type *ptr = &array_name;
Con trỏ tới mảng có một số đặc điểm thú vị sẽ được thảo luận trong phần sau.
Ví dụ 1: Truy cập phần tử mảng bằng con trỏ với chỉ số mảng
// Chương trình C để truy cập các phần tử mảng bằng con trỏ với chỉ số mảng #include void geeks() { // khai báo mảng int val[3] = {5, 10, 15}; // khai báo con trỏ int* ptr; // gán địa chỉ của mảng vào con trỏ ptr = val; // in ra các phần tử của mảng printf("Các phần tử của mảng là: "); printf("%d, %d, %d", ptr[0], ptr[1], ptr[2]); } // Chương trình chính int main() { geeks(); return 0; }
Kết quả:
Các phần tử của mảng là: 5, 10, 15
Ví dụ 2: Truy cập phần tử mảng bằng con trỏ với phép toán chỉ số mảng
// Chương trình C để truy cập các phần tử mảng bằng con trỏ với phép toán chỉ số mảng #include int main() { // Khai báo mảng int arr[5] = {1, 2, 3, 4, 5}; // Con trỏ tới mảng int* ptr = arr; // In ra các phần tử của mảng printf("Các phần tử của mảng là: "); for (int i = 0; i 5; i++) { printf("%d ", ptr[i]); } return 0; }
Kết quả:
Các phần tử của mảng là: 1 2 3 4 5
Con trỏ trong ngôn ngữ C và số học con trỏ
Số học con trỏ trong ngôn ngữ lập trình C là các phép toán số học hợp lệ có thể thực hiện trên con trỏ. Chúng bao gồm:
- Tăng giá trị con trỏ (con trỏ++)
- Giảm giá trị con trỏ (con trỏ--)
- Cộng số nguyên với con trỏ (con trỏ + số nguyên)
- Trừ số nguyên từ con trỏ (con trỏ - số nguyên)
- Trừ hai con trỏ cùng kiểu
Ví dụ: Số học con trỏ
// Chương trình C để minh họa các phép toán số học con trỏ #include int main() { // Khai báo mảng int arr[5] = {1, 2, 3, 4, 5}; // Khai báo con trỏ int* ptr = arr; // In ra kết quả printf("ptr++ = %p\n", ptr++); printf("ptr-- = %p\n", ptr--); printf("ptr + 2 = %p\n", ptr + 2); printf("ptr - 2 = %p\n", ptr - 2); printf("ptr2 - ptr = %ld\n", ptr2 - ptr); return 0; }
Kết quả:
ptr++ = 0x7ffded6bfb84 ptr-- = 0x7ffded6bfb80 ptr + 2 = 0x7ffded6bfb88 ptr - 2 = 0x7ffded6bfb78 ptr2 - ptr = 1
Tổng kết
Con trỏ là một phần quan trọng trong ngôn ngữ lập trình C. Chúng cho phép chúng ta truy xuất và thao tác dữ liệu lưu trữ trong bộ nhớ. Chúng ta đã thảo luận về các loại con trỏ khác nhau, cú pháp, kích thước và cách sử dụng chúng trong các tình huống khác nhau. Qua ví dụ và giải thích, ta có thể hiểu sâu hơn về cách sử dụng con trỏ trong ngôn ngữ C.
Nhớ rằng việc sử dụng con trỏ trong ngôn ngữ C yêu cầu cẩn thận và hiểu biết. Nếu không được sử dụng đúng cách, con trỏ có thể dẫn đến các lỗi và cú pháp không chính xác trong chương trình của bạn.