Làm thế nào để con trỏ đến con trỏ làm việc trong C? Khi nào bạn sẽ sử dụng chúng?
Làm thế nào để con trỏ đến con trỏ làm việc trong C? Khi nào bạn sẽ sử dụng chúng?
Câu trả lời:
Giả sử một máy tính 8 bit có địa chỉ 8 bit (và do đó chỉ có 256 byte bộ nhớ). Đây là một phần của bộ nhớ đó (các số ở trên cùng là địa chỉ):
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| | 58 | | | 63 | | 55 | | | h | e | l | l | o | \0 | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
Những gì bạn có thể thấy ở đây, là tại địa chỉ 63, chuỗi "xin chào" bắt đầu. Vì vậy, trong trường hợp này, nếu đây là lần xuất hiện duy nhất của "xin chào" trong bộ nhớ thì,
const char *c = "hello";
... định nghĩa c
là một con trỏ tới chuỗi (chỉ đọc) "hello" và do đó chứa giá trị 63. c
phải được lưu trữ ở đâu đó: trong ví dụ trên tại vị trí 58. Tất nhiên chúng ta không thể chỉ trỏ đến các ký tự , mà còn cho con trỏ khác. Ví dụ:
const char **cp = &c;
Bây giờ cp
chỉ vào c
, nghĩa là, nó chứa địa chỉ của c
(là 58). Chúng ta có thể đi xa hơn nữa. Xem xét:
const char ***cpp = &cp;
Bây giờ cpp
lưu trữ địa chỉ của cp
. Vì vậy, nó có giá trị 55 (dựa trên ví dụ ở trên) và bạn đoán nó: chính nó được lưu trữ tại địa chỉ 60.
Về lý do tại sao một người sử dụng con trỏ để con trỏ:
t
, tham chiếu đến mảng có kiểu t *
. Bây giờ hãy xem xét một mảng các mảng kiểu t
: tự nhiên một tham chiếu đến mảng 2D này sẽ có kiểu (t *)*
= t **
, và do đó là một con trỏ tới một con trỏ.char **
.f
sẽ cần chấp nhận một đối số của kiểu t **
nếu nó là để thay đổi một biến kiểu t *
.Làm thế nào để con trỏ đến con trỏ làm việc trong C?
Đầu tiên một con trỏ là một biến, giống như bất kỳ biến nào khác, nhưng nó giữ địa chỉ của một biến.
Một con trỏ tới một con trỏ là một biến, giống như bất kỳ biến nào khác, nhưng nó giữ địa chỉ của một biến. Biến đó chỉ là một con trỏ.
Khi nào bạn sẽ sử dụng chúng?
Bạn có thể sử dụng chúng khi bạn cần trả về một con trỏ tới một số bộ nhớ trên heap, nhưng không sử dụng giá trị trả về.
Thí dụ:
int getValueOf5(int *p)
{
*p = 5;
return 1;//success
}
int get1024HeapMemory(int **p)
{
*p = malloc(1024);
if(*p == 0)
return -1;//error
else
return 0;//success
}
Và bạn gọi nó như thế này:
int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5
int *p;
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap
Cũng có những cách sử dụng khác, như đối số main () của mọi chương trình C có một con trỏ tới một con trỏ cho argv, trong đó mỗi phần tử chứa một mảng ký tự là các tùy chọn dòng lệnh. Bạn phải cẩn thận mặc dù khi bạn sử dụng con trỏ của con trỏ để trỏ đến mảng 2 chiều, tốt hơn là sử dụng một con trỏ đến mảng 2 chiều thay thế.
Tại sao nó nguy hiểm?
void test()
{
double **a;
int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)
double matrix[ROWS][COLUMNS];
int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}
Dưới đây là một ví dụ về một con trỏ tới mảng 2 chiều được thực hiện đúng:
int (*myPointerTo2DimArray)[ROWS][COLUMNS]
Bạn không thể sử dụng một con trỏ tới một mảng 2 chiều mặc dù nếu bạn muốn hỗ trợ một số phần tử khác nhau cho ROWS và COLUMNS. Nhưng khi bạn biết trước khi sử dụng, bạn sẽ sử dụng một mảng 2 chiều.
Tôi thích ví dụ mã "thế giới thực" này của việc sử dụng con trỏ đến con trỏ, trong Git 2.0, cam kết 7b1004b :
Linus từng nói:
Tôi thực sự muốn nhiều người hiểu loại mã hóa cấp thấp thực sự cốt lõi. Không phải là thứ lớn, phức tạp như tra cứu tên không khóa, mà đơn giản là sử dụng tốt các con trỏ tới con trỏ, v.v.
Ví dụ, tôi đã thấy quá nhiều người xóa mục nhập danh sách liên kết đơn bằng cách theo dõi mục "trước" , và sau đó để xóa mục, làm một cái gì đó như
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
và bất cứ khi nào tôi thấy mã như vậy, tôi chỉ đi "Người này không hiểu con trỏ". Và thật đáng buồn là nó khá phổ biến.
Những người hiểu con trỏ chỉ cần sử dụng " con trỏ tới con trỏ mục nhập " và khởi tạo nó bằng địa chỉ của list_head. Và sau đó khi họ duyệt qua danh sách, họ có thể xóa mục nhập mà không cần sử dụng bất kỳ điều kiện nào, chỉ bằng cách thực hiện
*pp = entry->next
Áp dụng đơn giản hóa đó cho phép chúng ta mất 7 dòng từ chức năng này ngay cả khi thêm 2 dòng nhận xét.
- struct combine_diff_path *p, *pprev, *ptmp;
+ struct combine_diff_path *p, **tail = &curr;
Chris chỉ ra trong các bình luận cho video năm 2016 " Vấn đề con trỏ kép của Linus Torvalds " của Philip Buuck .
kumar chỉ ra trong các bình luận bài đăng trên blog " Linus về Tìm hiểu con trỏ ", nơi Grisha Trubetskoy giải thích:
Hãy tưởng tượng bạn có một danh sách liên kết được xác định là:
typedef struct list_entry {
int val;
struct list_entry *next;
} list_entry;
Bạn cần lặp lại từ đầu đến cuối và loại bỏ một phần tử cụ thể có giá trị bằng giá trị của to_remove.
Cách rõ ràng hơn để làm điều này sẽ là:
list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;
while (entry) { /* line 4 */
if (entry->val == to_remove) /* this is the one to remove ; line 5 */
if (prev)
prev->next = entry->next; /* remove the entry ; line 7 */
else
head = entry->next; /* special case - first entry ; line 9 */
/* move on to the next entry */
prev = entry;
entry = entry->next;
}
Những gì chúng tôi đang làm ở trên là:
- Lặp lại danh sách cho đến khi có mục
NULL
, nghĩa là chúng ta đã đến cuối danh sách (dòng 4).- Khi chúng tôi đi qua một mục chúng tôi muốn xóa (dòng 5),
- chúng ta gán giá trị của con trỏ tiếp theo hiện tại cho con trỏ trước đó,
- do đó loại bỏ phần tử hiện tại (dòng 7).
Có một trường hợp đặc biệt ở trên - khi bắt đầu lặp lại không có mục trước (
prev
làNULL
), và vì vậy để loại bỏ mục đầu tiên trong danh sách, bạn phải sửa đổi chính đầu (dòng 9).Điều Linus đã nói là đoạn mã trên có thể được đơn giản hóa bằng cách biến phần tử trước đó thành một con trỏ thành một con trỏ thay vì chỉ một con trỏ .
Mã sau đó trông như thế này:
list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;
while (entry) {
if (entry->val == to_remove)
*pp = entry->next;
pp = &entry->next;
entry = entry->next;
}
Đoạn mã trên rất giống với biến thể trước đó, nhưng lưu ý cách chúng ta không còn cần phải xem trường hợp đặc biệt của phần tử đầu tiên của danh sách, vì
pp
không phảiNULL
lúc đầu. Đơn giản và khéo léo.Ngoài ra, một người nào đó trong chủ đề đó nhận xét rằng lý do này tốt hơn là vì
*pp = entry->next
nguyên tử. Nó chắc chắn là KHÔNG nguyên tử .
Biểu thức trên có chứa hai toán tử quy ước (*
và->
) và một phép gán, và cả ba điều này đều không phải là nguyên tử.
Đây là một quan niệm sai lầm phổ biến, nhưng than ôi không có gì trong C nên được coi là nguyên tử (bao gồm cả++
và các--
toán tử)!
Khi đề cập đến con trỏ về một khóa học lập trình tại trường đại học, chúng tôi đã đưa ra hai gợi ý về cách bắt đầu tìm hiểu về chúng. Đầu tiên là xem Pulum Fun With Binky . Thứ hai là suy nghĩ về lối đi của Haddocks từ Lewis Carroll's Through the looking-Glass
Bạn rất buồn, nhóm Hiệp sĩ nói với giọng điệu lo lắng: Hãy để tôi hát cho bạn nghe một bài hát để an ủi bạn.
Có phải nó rất dài không? Alice hỏi, vì cô đã nghe rất nhiều thơ ngày hôm đó.
Từ lâu, người ta nói, Hiệp sĩ nói, nhưng nó rất, rất đẹp. Mọi người nghe tôi hát nó - hoặc nó mang lại những giọt nước mắt cho họ, hoặc người nào khác - TIẾNG
"Hoặc một điều gì khác?" Alice nói, vì Hiệp sĩ đã tạm dừng đột ngột.
Bạn cũng không biết, nếu không thì không. Tên của bài hát được gọi là 'Đôi mắt của Haddocks'.
Đây là tên của bài hát phải không? "Alice nói, cố gắng cảm thấy thích thú.
Không, bạn không hiểu, anh chàng Hiệp sĩ nói, trông có vẻ hơi bực tức. Đây là cái tên được gọi. Cái tên thực sự là 'Người đàn ông cao tuổi.'
Sau đó tôi nên nói 'Đó là những gì bài hát được gọi là'? Alice tự sửa.
Không, bạn không nên: đó là một điều hoàn toàn khác! Bài hát có tên là 'Cách và phương tiện': nhưng đó chỉ là những gì nó được gọi, bạn biết đấy!
Vậy thì, bài hát là gì vậy? Alice, người lúc này hoàn toàn hoang mang.
Tôi đã đến đó, anh chàng hiệp sĩ nói. Bài hát thực sự là 'A-ngồi trên cổng': và giai điệu là phát minh của riêng tôi.
Bạn có thể muốn đọc điều này: Con trỏ đến Con trỏ
Hy vọng điều này sẽ giúp làm rõ một số nghi ngờ cơ bản.
Khi một tham chiếu đến một con trỏ được yêu cầu. Ví dụ: khi bạn muốn sửa đổi giá trị (địa chỉ được chỉ đến) của biến con trỏ được khai báo trong phạm vi của hàm gọi bên trong hàm được gọi.
Nếu bạn chuyển một con trỏ đơn làm đối số, bạn sẽ sửa đổi các bản sao cục bộ của con trỏ, không phải con trỏ ban đầu trong phạm vi gọi. Với một con trỏ đến một con trỏ, bạn sửa đổi cái sau.
Một con trỏ đến một con trỏ cũng được gọi là một tay cầm . Một cách sử dụng cho nó thường là khi một đối tượng có thể được di chuyển trong bộ nhớ hoặc loại bỏ. Một người thường có trách nhiệm khóa và mở khóa việc sử dụng đối tượng để nó không bị di chuyển khi truy cập vào nó.
Nó thường được sử dụng trong môi trường hạn chế bộ nhớ, tức là Palm OS.
Hãy xem xét hình và chương trình dưới đây để hiểu khái niệm này tốt hơn .
Theo hình, ptr1 là một con trỏ duy nhất có địa chỉ num biến .
ptr1 = #
Tương tự ptr2 là một con trỏ tới con trỏ (con trỏ kép) có địa chỉ của con trỏ ptr1 .
ptr2 = &ptr1;
Một con trỏ trỏ đến một con trỏ khác được gọi là con trỏ kép. Trong ví dụ này ptr2 là một con trỏ kép.
Giá trị từ sơ đồ trên:
Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000
Thí dụ:
#include <stdio.h>
int main ()
{
int num = 10;
int *ptr1;
int **ptr2;
// Take the address of var
ptr1 = #
// Take the address of ptr1 using address of operator &
ptr2 = &ptr1;
// Print the value
printf("Value of num = %d\n", num );
printf("Value available at *ptr1 = %d\n", *ptr1 );
printf("Value available at **ptr2 = %d\n", **ptr2);
}
Đầu ra:
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
nó là một con trỏ tới giá trị địa chỉ của con trỏ. (thật kinh khủng tôi biết)
về cơ bản, nó cho phép bạn chuyển một con trỏ tới giá trị địa chỉ của một con trỏ khác, do đó bạn có thể sửa đổi nơi con trỏ khác đang trỏ từ một hàm phụ, như:
void changeptr(int** pp)
{
*pp=&someval;
}
Một con trỏ đến con trỏ là, tốt, một con trỏ đến con trỏ.
Một ví dụ đầy ý nghĩa của someType ** là một mảng hai chiều: bạn có một mảng, chứa đầy các con trỏ tới các mảng khác, vì vậy khi bạn viết
con trỏ [5] [6]
bạn truy cập vào mảng chứa con trỏ tới các mảng khác ở vị trí thứ 5 của anh ta, lấy con trỏ (đặt tên con trỏ của anh ta) và sau đó truy cập vào phần tử thứ 6 của mảng được tham chiếu đến mảng đó (vì vậy, fpulum [6]).
Cách thức hoạt động: Đây là một biến có thể lưu trữ một con trỏ khác.
Khi nào bạn sẽ sử dụng chúng: Nhiều người sử dụng một trong số đó là nếu chức năng của bạn muốn xây dựng một mảng và trả lại cho người gọi.
//returns the array of roll nos {11, 12} through paramater
// return value is total number of students
int fun( int **i )
{
int *j;
*i = (int*)malloc ( 2*sizeof(int) );
**i = 11; // e.g., newly allocated memory 0x2000 store 11
j = *i;
j++;
*j = 12; ; // e.g., newly allocated memory 0x2004 store 12
return 2;
}
int main()
{
int *i;
int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
for ( int j=0; j<n; j++ )
printf( "roll no = %d \n", i[j] );
return 0;
}
Tôi đã tạo một video dài 5 phút giải thích cách hoạt động của con trỏ:
Có rất nhiều lời giải thích hữu ích, nhưng tôi không tìm thấy một mô tả ngắn, vì vậy ..
Về cơ bản con trỏ là địa chỉ của biến. Mã tóm tắt ngắn:
int a, *p_a;//declaration of normal variable and int pointer variable
a = 56; //simply assign value
p_a = &a; //save address of "a" to pointer variable
*p_a = 15; //override the value of the variable
//print 0xfoo and 15
//- first is address, 2nd is value stored at this address (that is called dereference)
printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a);
Ngoài ra thông tin hữu ích có thể được tìm thấy trong chủ đề Điều gì có nghĩa là tham chiếu và quy định
Và tôi không chắc lắm, khi nào có thể là con trỏ hữu ích, nhưng điểm chung là cần sử dụng chúng khi bạn đang thực hiện một số cấp phát bộ nhớ thủ công / động - malloc, calloc, v.v.
Vì vậy, tôi hy vọng nó cũng sẽ giúp làm rõ vấn đề :)