Khái niệm về con trỏ thực sự là một trong những khái niệm rất quan trọng trong ngôn ngữ lập trình C. Cho đến bây giờ, chúng ta đã thảo luận về một số khía cạnh của con trỏ trong C. Mở rộng hơn nữa, trong hướng dẫn này, chúng ta sẽ thảo luận thêm một số khái niệm về con trỏ.
Dòng đầu tiên định nghĩa một mảng 'p' có kích thước bằng số ký tự trong dấu ngoặc kép. Nhưng dòng tiếp theo định nghĩa một con trỏ 'p' trỏ tới một hằng chuỗi.
Sự khác biệt ở đây là 'p' đầu tiên là một mảng, bạn có thể dễ dàng sửa đổi hoặc thay đổi nội dung của mảng. Nhưng vì 'p' thứ hai trỏ đến một hằng chuỗi, nên bạn không thể thay đổi nội dung chuỗi.
Ví dụ, đoạn mã sau đây cố gắng sửa đổi một hằng chuỗi:
Và đây là đầu ra do đoạn mã này tạo ra trên hệ thống của tôi:
Lỗi này cho thấy quá trình thực thi chương trình đã kết thúc đột ngột và đó là do chúng ta đã cố gắng thay đổi một thứ gì đó là hằng số.
Ngoài ra, hãy nhớ rằng mặc dù con trỏ 'p' có thể được sử dụng để trỏ đến một chuỗi khác, nhưng bạn không thể thay đổi địa chỉ cơ sở của mảng 'p' (nếu bạn nhớ, chúng ta đã thảo luận về vấn đề này trong một trong những bài trước hướng dẫn).
Bây giờ chuyển sang mảng con trỏ, giống như bạn đã thấy mảng số nguyên, mảng ký tự và một loại mảng khác, cũng có thể có một mảng con trỏ. Ví dụ, chương trình sau định nghĩa một mảng 'arr' gồm các con trỏ số nguyên và gán giá trị cho nó.
Lưu ý rằng các giá trị được gán cho mảng là các địa chỉ. Điều này là do 'arr' là một mảng các con trỏ và các con trỏ không lưu trữ gì ngoài các địa chỉ. Bây giờ, nếu bạn muốn truy cập các giá trị được lưu giữ tại các địa chỉ này, bạn sẽ phải sử dụng *operator.
Ví dụ sau (chỉ là phần mở rộng của ví dụ trước) minh họa điều này:
Đây là output:
Tương tự như mảng con trỏ số nguyên (như mảng chúng ta đã thảo luận ở đây), bạn có thể có các mảng lưu trữ con trỏ ký tự và nhiều hơn nữa.
Bây giờ, chúng ta hãy chuyển sang con trỏ tới con trỏ. Vì chúng ta đã lặp lại nhiều lần cho đến nay, một con trỏ lưu trữ một địa chỉ. Bây giờ, cho đến bây giờ trong loạt bài hướng dẫn lập trình C đang diễn ra này, chúng ta chỉ thấy một con trỏ trỏ tới một biến không phải con trỏ, nhưng thực tế là con trỏ cũng có thể trỏ tới các con trỏ khác.
Điều này có nghĩa là một con trỏ có thể lưu trữ địa chỉ của một con trỏ khác. Ví dụ, sau đây là một con trỏ kép hoặc một con trỏ tới con trỏ:
Đây là một đoạn mã sử dụng con trỏ kép:
Đây là đầu ra:
Đây là một ví dụ về con trỏ kép. Trên các dòng tương tự, bạn có thể có một con trỏ đến một con trỏ đến một con trỏ, được định nghĩa là, ví dụ, int ***ptr. Số lượng tối đa các cấp độ 'con trỏ đến con trỏ đến......' như vậy là cụ thể cho việc triển khai (mặc dù trong một số trường hợp, giới hạn là 12).
Tuy nhiên, trên thực tế, bạn có thể chỉ gặp con trỏ đến con trỏ lên đến cấp độ ba, vì việc có nhiều cấp độ hơn khiến logic phức tạp hơn để hiểu và duy trì.
Con trỏ ký tự, mảng con trỏ và con trỏ tới con trỏ trong C
Chúng ta hãy bắt đầu với con trỏ ký tự bằng các dòng mã sau:
Mã:
char p[] = "Tôi thích HowtoForge"
char *p = "Tôi thích HowToForge"
Sự khác biệt ở đây là 'p' đầu tiên là một mảng, bạn có thể dễ dàng sửa đổi hoặc thay đổi nội dung của mảng. Nhưng vì 'p' thứ hai trỏ đến một hằng chuỗi, nên bạn không thể thay đổi nội dung chuỗi.
Ví dụ, đoạn mã sau đây cố gắng sửa đổi một hằng chuỗi:
Mã:
#include
int main()
{
char *p = "I like HowToForge";
p[0] = 'U';
return 0;
}
Mã:
Segmentationfault
Ngoài ra, hãy nhớ rằng mặc dù con trỏ 'p' có thể được sử dụng để trỏ đến một chuỗi khác, nhưng bạn không thể thay đổi địa chỉ cơ sở của mảng 'p' (nếu bạn nhớ, chúng ta đã thảo luận về vấn đề này trong một trong những bài trước hướng dẫn).
Bây giờ chuyển sang mảng con trỏ, giống như bạn đã thấy mảng số nguyên, mảng ký tự và một loại mảng khác, cũng có thể có một mảng con trỏ. Ví dụ, chương trình sau định nghĩa một mảng 'arr' gồm các con trỏ số nguyên và gán giá trị cho nó.
Mã:
#include
int main()
{
int *arr[3];
int a = 0, b = 1, c = 2;
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
return 0;
}
Ví dụ sau (chỉ là phần mở rộng của ví dụ trước) minh họa điều này:
Mã:
#include
int main()
{
int *arr[3];
int a = 0, b = 1, c = 2;
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
for(int i=0; i < 3; i++)
printf("\n arr[%d] is: %d",i,*(arr[i]));
return 0;
}
Mã:
arr[0] is: 0
arr[1] is: 1
arr[2] is: 2
Bây giờ, chúng ta hãy chuyển sang con trỏ tới con trỏ. Vì chúng ta đã lặp lại nhiều lần cho đến nay, một con trỏ lưu trữ một địa chỉ. Bây giờ, cho đến bây giờ trong loạt bài hướng dẫn lập trình C đang diễn ra này, chúng ta chỉ thấy một con trỏ trỏ tới một biến không phải con trỏ, nhưng thực tế là con trỏ cũng có thể trỏ tới các con trỏ khác.
Điều này có nghĩa là một con trỏ có thể lưu trữ địa chỉ của một con trỏ khác. Ví dụ, sau đây là một con trỏ kép hoặc một con trỏ tới con trỏ:
Mã:
int **ptr;
Mã:
#include
int main()
{
int *ptr;
int **p;
int a = 10;
ptr = &a;
p = &ptr;
printf("\n Con trỏ 'p' trỏ tới con trỏ 'ptr' trỏ tới giá trị: %d", **p);
return 0;
}
Mã:
Pointer'p'pointstopopointer'ptr'whichfurtherpointstovalue:10
Tuy nhiên, trên thực tế, bạn có thể chỉ gặp con trỏ đến con trỏ lên đến cấp độ ba, vì việc có nhiều cấp độ hơn khiến logic phức tạp hơn để hiểu và duy trì.