Con trỏ đến con trỏ (Double Pointer) trong C

Xin chào các bạn nhà lập trình đam mê! Hôm nay, chúng ta sẽ bắt đầu hành trình hấp dẫn vào thế giới của các con trỏ - cụ thể là các con trỏ đến con trỏ. Tôi biết bạn có thể suy nghĩ: "Con trỏ? Double pointer? Đây có lẽ là một cơn ác mộng!" Nhưng đừng lo lắng, tôi hứa sẽ làm cho việc này trở nên thú vị và dễ hiểu nhất có thể. Vậy hãy lấy ly đồ uống yêu thích của bạn, thoải mái ngồi, và hãy bắt đầu!

C - Pointer to Pointer

Con trỏ đôi trong C là gì?

Hãy tưởng tượng bạn đang tham gia vào một cuộc săn kho báu. Bạn có một bản đồ (gọi nó là một con trỏ) dẫn bạn đến một chiếc hộp. Nhưng hãy ngạc nhiên! Trong hộp đó, có một bản đồ khác (một con trỏ khác) dẫn đến kho báu thực tế. Đó chính là điều gì mà một con trỏ đôi là - một con trỏ dẫn đến một con trỏ khác.

Trong lập trình C, một con trỏ đôi chính là như vậy - một con trỏ dẫn đến một con trỏ. Nó là một biến lưu trữ địa chỉ của một con trỏ khác. Điều này có thể có vẻ khó hiểu ban đầu, nhưng đừng lo lắng, chúng ta sẽ phân tích nó bước به bước.

Khai báo Con trỏ đến một Con trỏ

Hãy bắt đầu với cách chúng ta khai báo một con trỏ đôi. Cú pháp rất đơn giản:

int **ptr;

Ở đây, ptr là một con trỏ đến một con trỏ đến một số nguyên. Dấu sao * thứ nhất làm nó trở thành một con trỏ, và dấu sao * thứ hai làm nó trở thành một con trỏ đến một con trỏ.

Ví dụ về Con trỏ đến Con trỏ (Double Pointer)

Hãy xem một ví dụ đơn giản để hiểu rõ hơn:

#include <stdio.h>

int main() {
int x = 5;
int *p = &x;
int **pp = &p;

printf("Giá trị của x: %d\n", x);
printf("Giá trị của x sử dụng p: %d\n", *p);
printf("Giá trị của x sử dụng pp: %d\n", **pp);

return 0;
}

Output:

Giá trị của x: 5
Giá trị của x sử dụng p: 5
Giá trị của x sử dụng pp: 5

Hãy phân tích nó:

  1. Chúng ta khai báo một số nguyên x và khởi tạo nó với 5.
  2. Chúng ta tạo một con trỏ p dẫn đến x.
  3. Chúng ta tạo một con trỏ đôi pp dẫn đến p.
  4. Chúng ta sau đó in giá trị của x ba cách khác nhau:
  • Trực tiếp sử dụng x
  • Sử dụng con trỏ đơn p (chúng ta định tham chiếu nó một lần với *p)
  • Sử dụng con trỏ đôi pp (chúng ta định tham chiếu nó hai lần với **pp)

Tất cả ba cách đều cho chúng ta giá trị: 5. Như thể đạt được kho báu bằng các bản đồ khác nhau!

Con trỏ bình thường trong C hoạt động như thế nào?

Trước khi chúng ta sâu hơn vào con trỏ đôi, hãy nhanh chóng xem lại cách con trỏ bình thường hoạt động:

int y = 10;
int *q = &y;

printf("Giá trị của y: %d\n", y);
printf("Địa chỉ của y: %p\n", (void*)&y);
printf("Giá trị của q: %p\n", (void*)q);
printf("Giá trị được q chỉ: %d\n", *q);

Output:

Giá trị của y: 10
Địa chỉ của y: 0x7ffd5e8e9f44
Giá trị của q: 0x7ffd5e8e9f44
Giá trị được q chỉ: 10

Ở đây, q là một con trỏ lưu trữ địa chỉ của y. Khi chúng ta sử dụng *q, chúng ta đang truy cập giá trị được lưu trữ ở địa chỉ đó.

Con trỏ đôi hoạt động như thế nào?

Bây giờ, hãy mở rộng đến con trỏ đôi:

int z = 15;
int *r = &z;
int **rr = &r;

printf("Giá trị của z: %d\n", z);
printf("Địa chỉ của z: %p\n", (void*)&z);
printf("Giá trị của r: %p\n", (void*)r);
printf("Địa chỉ của r: %p\n", (void*)&r);
printf("Giá trị của rr: %p\n", (void*)rr);
printf("Giá trị được r chỉ: %d\n", *r);
printf("Giá trị được rr chỉ: %p\n", (void*)*rr);
printf("Giá trị được chỉ bởi giá trị được rr chỉ: %d\n", **rr);

Output:

Giá trị của z: 15
Địa chỉ của z: 0x7ffd5e8e9f48
Giá trị của r: 0x7ffd5e8e9f48
Địa chỉ của r: 0x7ffd5e8e9f50
Giá trị của rr: 0x7ffd5e8e9f50
Giá trị được r chỉ: 15
Giá trị được rr chỉ: 0x7ffd5e8e9f48
Giá trị được chỉ bởi giá trị được rr chỉ: 15

Điều này có thể có vẻ quá khó khăn, nhưng hãy phân tích nó:

  1. z là một số nguyên có giá trị 15.
  2. r là một con trỏ lưu trữ địa chỉ của z.
  3. rr là một con trỏ đôi lưu trữ địa chỉ của r.
  4. *r cho chúng ta giá trị của z (15).
  5. *rr cho chúng ta giá trị của r (địa chỉ của z).
  6. **rr cho chúng ta giá trị của z (15).

Hãy tưởng tượng điều này như thế này: rr chỉ đến r, r chỉ đến z. Vì vậy **rr như nói "theo dõi con trỏ đầu tiên, sau đó theo dõi con trỏ thứ hai, và đưa cho tôi giá trị ở đó".

Một Con trỏ đôi hoạt động như một Con trỏ bình thường

Dưới đây là một bí mật nhỏ: một con trỏ đôi chỉ là một con trỏ, nhưng thay vì chỉ đến một int hoặc float, nó chỉ đến một con trỏ khác. Điều này có nghĩa là chúng ta có thể làm tất cả những điều tương tự với con trỏ đôi như chúng ta có thể làm với con trỏ bình thường.

Ví dụ, chúng ta có thể sử dụng con trỏ đôi với các mảng:

int main() {
char *fruits[] = {"Apple", "Banana", "Cherry"};
char **ptr = fruits;

for(int i = 0; i < 3; i++) {
printf("%s\n", *ptr);
ptr++;
}

return 0;
}

Output:

Apple
Banana
Cherry

Trong ví dụ này, fruits là một mảng con trỏ (mỗi con trỏ chỉ đến một chuỗi), và ptr là một con trỏ đến con trỏ đến một ký tự (có thể chỉ đến các phần tử của fruits).

Cấp độ nhiều của Con trỏ trong C (Con trỏ ba có thể không?)

Đúng vậy, bạn có thể có con trỏ ba, con trỏ bốn, và nhiều hơn! Không có giới hạn lý thuyết về cấp độ của chỉ tham chiếu bạn có thể có. Tuy nhiên, trong thực tế, điều này khó xảy ra hơn là con trỏ đôi.

Dưới đây là một ví dụ về con trỏ ba:

int x = 5;
int *p = &x;
int **pp = &p;
int ***ppp = &pp;

printf("Giá trị của x: %d\n", ***ppp);

Output:

Giá trị của x: 5

Nhưng nhớ rằng, chỉ vì bạn có thể thì không có nghĩa là bạn nên. Nhiều cấp độ của chỉ tham chiếu có thể làm cho mã trở nên khó đọc và khó bảo trì. Như lời khuyên lập trình cổ điển: "Tất cả các vấn đề trong khoa học máy tính có thể được giải quyết bằng một cấp độ thêm của chỉ tham chiếu... trừ khi vấn đề là quá nhiều cấp độ của chỉ tham chiếu!"

Kết luận

Xin chúc mừng! Bạn đã vượt qua những khó khăn của con trỏ đôi trong C. Nhớ rằng, như nhiều khái niệm trong lập trình, con trỏ đến con trỏ có thể có vẻ khó hiểu ban đầu, nhưng với thực hành, chúng sẽ trở nên tự nhiên như bất kỳ thứ gì khác.

Dưới đây là bảng tóm tắt các điểm chính chúng ta đã đi qua:

Khái niệm Cú pháp Mô tả
Con trỏ bình thường int *p; Chỉ đến một số nguyên
Con trỏ đôi int **pp; Chỉ đến một con trỏ đến một số nguyên
Định tham chiếu *p Truy cập giá trị được chỉ bởi p
Định tham chiếu đôi **pp Truy cập giá trị được chỉ bởi con trỏ mà pp chỉ
Toán tử địa chỉ &x Lấy địa chỉ của x

Tiếp tục thực hành, giữ được sự tham vọng và nhớ rằng - mọi chuyên gia đều từng là người mới bắt đầu. Chúc bạn có những giờ lập trình thú vị!

Credits: Image by storyset