Làm thế nào để con trỏ đến con trỏ làm việc trong C?


171

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?


43
Không không bài tập về nhà .... chỉ muốn biết..có thể tôi thấy nó rất nhiều khi tôi đọc mã C.

1
Một con trỏ tới con trỏ không phải là trường hợp đặc biệt của một cái gì đó, vì vậy tôi không hiểu những gì bạn không hiểu về void **.
akappa

đối với mảng 2D, ví dụ tốt nhất là dòng lệnh args "prog arg1 arg2" được lưu trữ char ** argv. Và nếu người gọi không muốn phân bổ bộ nhớ (chức năng được gọi sẽ phân bổ bộ nhớ)
resultsway 21/03/13

1
Bạn có một ví dụ hay về cách sử dụng "con trỏ tới con trỏ" trong Git 2.0: xem câu trả lời của tôi bên dưới
VonC

Câu trả lời:


359

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 clà một con trỏ tới chuỗi (chỉ đọc) "hello" và do đó chứa giá trị 63. cphả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ờ cpchỉ 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ờ cpplư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ên của một mảng thường mang lại địa chỉ của phần tử đầu tiên của nó. Vì vậy, nếu mảng chứa các phần tử của kiểu 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ỏ.
  • Mặc dù một mảng các chuỗi âm thanh một chiều, nhưng thực tế nó là hai chiều, vì các chuỗi là các mảng ký tự. Do đó : char **.
  • Một hàm fsẽ 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 *.
  • Nhiều lý do khác mà quá nhiều để liệt kê ở đây.

7
vâng ví dụ tốt..tôi hiểu chúng là gì..nhưng làm thế nào và khi nào sử dụng chúng là quan trọng hơn..now ..

2
Stephan đã làm rất tốt việc tái tạo, về cơ bản, sơ đồ trong Ngôn ngữ lập trình C của Kernighan & Richie. Nếu bạn đang lập trình C và không có cuốn sách này và rất tuyệt với tài liệu giấy, tôi khuyên bạn nên lấy nó, chi phí khiêm tốn (khá) sẽ tự trả rất nhanh về năng suất. Nó có xu hướng rất rõ ràng trong các ví dụ của nó.
J. Polfer

4
char * c = "xin chào" nên là const char * c = "xin chào". Ngoài ra, nhiều nhất là sai lầm khi nói rằng "một mảng được lưu trữ dưới dạng địa chỉ của phần tử đầu tiên". Một mảng được lưu trữ dưới dạng ... một mảng. Thông thường tên của nó mang lại một con trỏ đến phần tử đầu tiên của nó, nhưng không phải lúc nào cũng vậy. Về con trỏ tới con trỏ, tôi chỉ đơn giản nói rằng chúng rất hữu ích khi một hàm phải sửa đổi một con trỏ được truyền dưới dạng tham số (sau đó bạn chuyển một con trỏ đến con trỏ).
Bastien Léonard

4
Trừ khi tôi hiểu sai câu trả lời này, nó có vẻ sai. c được lưu trữ ở 58 và trỏ đến 63, cp được lưu trữ ở 55 và trỏ đến 58 và cpp không được biểu thị trong sơ đồ.
Thanatos

1
Có vẻ tốt. Hơn một vấn đề nhỏ là tất cả những gì ngăn cản tôi nói: Bài đăng tuyệt vời. Bản giải thích là tuyệt vời. Thay đổi để bỏ phiếu lên. (Có lẽ stackoverflow cần xem lại con trỏ?)
Thanatos

46

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.


32

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

http://i.stack.imgur.com/bpfxT.gif

Á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 ( prevNULL), 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ì ppkhông phải NULLlú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->nextnguyê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à 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ử)!


4
Điều này sẽ giúp hiểu rõ hơn - grisha.org/blog/2013/04/02/linus-on-under Hiểu
kumar

@kumar tham khảo tốt. tôi đã đưa nó vào câu trả lời để dễ nhìn hơn.
VonC

Video này rất cần thiết cho tôi trong việc hiểu ví dụ của bạn. Cụ thể, tôi đã cảm thấy bối rối (và hiếu chiến) cho đến khi tôi vẽ sơ đồ bộ nhớ và theo dõi tiến trình của chương trình. Điều đó nói rằng, nó vẫn có vẻ hơi bí ẩn đối với tôi.
Chris

@Chris Video tuyệt vời, cảm ơn bạn đã đề cập đến nó! Tôi đã bao gồm nhận xét của bạn trong câu trả lời để dễ nhìn hơn.
VonC

14

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.


1
Tôi đã phải đọc đoạn văn đó một vài lần ... +1 vì đã khiến tôi phải suy nghĩ!
Ruben Steins

Đây là lý do tại sao Lewis Carroll không phải là một nhà văn bình thường.
metarose

1
Vậy ... nó sẽ như thế này? tên -> 'Người đàn ông cao tuổi' -> được gọi -> 'Đôi mắt của Haddock' -> bài hát -> 'A-ngồi trên cổng'
tisaconundrum


7

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.


Giải thích rõ cho phần 'Tại sao'
Rana Deep

7

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.

máy tính.how wareworks.com Liên kết >>

Liên kết www.flippinbits.com >>


7

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 .

Sơ đồ con trỏ kép

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 = &num;

   // 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

5

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;
}

xin lỗi, tôi biết nó khá tệ Hãy thử đọc, erm, cái này: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer

5

Bạn có một biến chứa địa chỉ của một cái gì đó. Đó là một con trỏ.

Sau đó, bạn có một biến khác chứa địa chỉ của biến đầu tiên. Đó là một con trỏ đến con trỏ.


3

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]).


2
không nên nhầm lẫn các con trỏ tới các con trỏ với các mảng của tier2, ví dụ int x [10] [10] trong đó bạn viết x [5] [6] bạn truy cập giá trị trong mảng.
Pete Kirkham

Đây chỉ là một ví dụ trong đó một khoảng trống ** là phù hợp. Một con trỏ tới con trỏ chỉ là một con trỏ trỏ tới, tốt, một con trỏ.
akappa

1

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;
}


0

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 đề :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.