Con trỏ để làm rõ con trỏ


142

Tôi đã làm theo hướng dẫn này về cách một con trỏ đến một con trỏ hoạt động.

Hãy để tôi trích dẫn đoạn văn có liên quan:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Bây giờ chúng ta có thể thiết lập

    int **ipp = &ip1;

ippchỉ vào ip1điểm nào i. *ippip1, và **ippi, hoặc 5. Chúng ta có thể minh họa tình huống, bằng ký hiệu hộp và mũi tên quen thuộc của mình, như thế này:

nhập mô tả hình ảnh ở đây

Nếu sau đó chúng ta nói

    *ipp = ip2;

chúng tôi đã thay đổi con trỏ được trỏ bởi ipp(nghĩa là ip1) để chứa một bản sao của ip2, để ip1bây giờ nó ( ) trỏ đến j:

nhập mô tả hình ảnh ở đây


Câu hỏi của tôi là: Tại sao trong bức tranh thứ hai, ippvẫn chỉ vào ip1nhưng không ip2?


6
xin vui lòng, thêm loại ippkhi xác định nó, để câu hỏi của bạn hoàn tất ;-)
zmo

1
Bởi vì ippchỉ vào ip1như vậy *ipp = ip2cũng giống nhưip1 = ip2
Grijesh Chauhan

1
Chúng ta có thể vui lòng ngừng viết các dấu hoa thị cách xa loại nhọn không? int **ipplà ít trực quan hơn int** ipp, đặc biệt là khi **ippcó nghĩa là một điều hoàn toàn khác bên ngoài khai báo kiểu.
Darkhogg

4
@Darkhogg int **ippcó vẻ khá trực quan đối với tôi. Nó có nghĩa là, tôi đang làm cho **ippmột int. Cái nào đúng.
ithisa

2
@ user54609 ý nghĩa và trực giác cá nhân tách biệt, hãy xem xét điều này: Loại ippint**, vì vậy chỉ cần viết int**thay vì phép thuật " impdereference là một cách hiểu " .
Manu343726

Câu trả lời:


143

Quên một giây về sự tương tự chỉ. Những gì một con trỏ thực sự chứa là một địa chỉ bộ nhớ. Đây &là toán tử "address of" - tức là nó trả về địa chỉ trong bộ nhớ của một đối tượng. Các *nhà khai thác cung cấp cho bạn các đối tượng một con trỏ đề cập đến, tức là cho một con trỏ chứa địa chỉ, nó sẽ trả về đối tượng tại địa chỉ bộ nhớ. Vì vậy, khi bạn làm *ipp = ip2, những gì bạn đang làm là *ipplấy đối tượng tại địa chỉ được giữ trong ippđó ip1và sau đó gán cho ip1giá trị được lưu trữ ip2, đó là địa chỉ của j.

Đơn giản chỉ cần
& -> Địa chỉ của
*-> Giá trị tại


14
& và * chưa bao giờ dễ dàng đến thế
Ray

7
Tôi tin rằng nguồn gây nhầm lẫn chính là do sự không rõ ràng của toán tử *, trong khi khai báo biến được sử dụng để chỉ ra rằng biến, trên thực tế, là một con trỏ tới một kiểu dữ liệu nhất định. Tuy nhiên, mặt khác, nó cũng được sử dụng trong các câu lệnh để truy cập vào nội dung của biến được trỏ bởi một con trỏ (toán tử hội nghị).
Lucas A.

43

Bởi vì bạn đã thay đổi giá trị được trỏ bởi ippkhông phải giá trị của ipp. Vì vậy, ippvẫn trỏ đến ip1(giá trị của ipp), ip1giá trị của bây giờ giống với ip2giá trị của, vì vậy cả hai đều trỏ đến j.

Điều này:

*ipp = ip2;

giống như:

ip1 = ip2;

11
Có thể đáng để chỉ ra sự khác biệt giữa int *ip1 = &i*ipp = ip2;, tức là nếu bạn loại bỏ intcâu lệnh đầu tiên thì các bài tập trông rất giống nhau, nhưng nó *đang làm một cái gì đó rất khác nhau trong hai trường hợp.
Crowman

22

Giống như hầu hết các câu hỏi dành cho người mới bắt đầu trong thẻ C, câu hỏi này có thể được trả lời bằng cách quay lại các nguyên tắc đầu tiên:

  • Một con trỏ là một loại giá trị.
  • Một biến chứa một giá trị.
  • Các &nhà điều hành chuyển một biến thành một con trỏ.
  • Các *nhà điều hành chuyển một con trỏ vào một biến.

(Về mặt kỹ thuật tôi nên nói "lvalue" thay vì "biến", nhưng tôi cảm thấy rõ ràng hơn khi mô tả các vị trí lưu trữ có thể thay đổi là "biến".)

Vì vậy, chúng tôi có các biến:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

Biến ip1 chứa một con trỏ. Các &nhà điều hành biến ithành một con trỏ và giá trị con trỏ được gán cho ip1. Vì vậy, ip1 chứa một con trỏ đến i.

Biến ip2 chứa một con trỏ. Các &nhà điều hành biến jthành một con trỏ và con trỏ được gán cho ip2. Vì vậy, ip2 chứa một con trỏ đến j.

int **ipp = &ip1;

Biến ippchứa một con trỏ. Các &nhà điều hành chuyển biến ip1thành một con trỏ và giá trị con trỏ được gán cho ipp. Vì vậy, ippchứa một con trỏ đến ip1.

Chúng ta hãy tóm tắt câu chuyện cho đến nay:

  • i chứa 5
  • j chứa 6
  • ip1chứa "con trỏ tới i"
  • ip2chứa "con trỏ tới j"
  • ippchứa "con trỏ tới ip1"

Bây giờ chúng tôi nói

*ipp = ip2;

Các *nhà điều hành chuyển một con trỏ trở lại vào một biến. Chúng tôi lấy giá trị của ipp, đó là "con trỏ tới ip1và biến nó thành một biến. Biến nào? ip1Tất nhiên!

Vì vậy, đây chỉ đơn giản là một cách nói khác

ip1 = ip2;

Vì vậy, chúng tôi lấy giá trị của ip2. Nó là gì? "Con trỏ tới j". Chúng tôi gán giá trị con trỏ đó cho ip1, vì vậy ip1bây giờ là "con trỏ tới j"

Chúng tôi chỉ thay đổi một điều: giá trị của ip1:

  • i chứa 5
  • j chứa 6
  • ip1chứa "con trỏ tới j"
  • ip2chứa "con trỏ tới j"
  • ippchứa "con trỏ tới ip1"

Tại sao ippvẫn chỉ đến ip1và không ip2?

Một biến thay đổi khi bạn gán cho nó. Đếm các bài tập; không thể có nhiều thay đổi cho các biến hơn là có các bài tập! Bạn bắt đầu bằng cách gán cho i, j, ip1, ip2ipp. Sau đó, bạn gán cho *ipp, như chúng ta đã thấy có nghĩa giống như "gán cho ip1". Vì bạn không chỉ định ipplần thứ hai, nên nó không thay đổi!

Nếu bạn muốn thay đổi ippthì bạn sẽ phải thực sự gán cho ipp:

ipp = &ip2;

ví dụ.


21

hy vọng đoạn mã này có thể giúp đỡ.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

nó xuất ra:

nhập mô tả hình ảnh ở đây


12

Ý kiến ​​rất cá nhân của tôi là hình ảnh với mũi tên chỉ theo cách này hoặc làm cho con trỏ khó hiểu hơn. Nó làm cho chúng có vẻ giống như một số thực thể trừu tượng, bí ẩn. Họ không phải.

Giống như mọi thứ khác trong máy tính của bạn, con trỏ là số . Tên "con trỏ" chỉ là một cách hay để nói "một biến chứa địa chỉ".

Do đó, hãy để tôi khuấy động mọi thứ xung quanh bằng cách giải thích cách máy tính thực sự hoạt động.

Chúng ta có một int, nó có tên ivà giá trị 5. Điều này được lưu trữ trong bộ nhớ. Giống như mọi thứ được lưu trữ trong bộ nhớ, nó cần một địa chỉ hoặc chúng ta sẽ không thể tìm thấy nó. Hãy nói rằng ikết thúc tại địa chỉ 0x12345678 và bạn thân của nó jvới giá trị 6 kết thúc ngay sau nó. Giả sử CPU 32 bit trong đó int là 4 byte và con trỏ là 4 byte, thì các biến được lưu trữ trong bộ nhớ vật lý như thế này:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Bây giờ chúng tôi muốn chỉ vào các biến này. Chúng ta tạo một con trỏ tới int int* ip1và một int* ip2. Giống như mọi thứ trong máy tính, các biến con trỏ này cũng được phân bổ ở đâu đó trong bộ nhớ. Giả sử rằng chúng kết thúc tại các địa chỉ liền kề tiếp theo trong bộ nhớ, ngay sau đó j. Chúng tôi đặt các con trỏ chứa địa chỉ của các biến được phân bổ trước đó: ip1=&i;("sao chép địa chỉ của i vào ip1") và ip2=&j. Điều gì xảy ra giữa các dòng là:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Vì vậy, những gì chúng ta nhận được chỉ là một số bộ nhớ 4 byte chứa các số. Không có mũi tên thần bí hay ma thuật ở bất cứ đâu trong tầm nhìn.

Thực tế, chỉ bằng cách nhìn vào một bãi chứa bộ nhớ, chúng ta không thể biết được địa chỉ 0x12345680 có chứa inthay không int*. Sự khác biệt là cách chương trình của chúng tôi chọn sử dụng nội dung được lưu trữ tại địa chỉ này. (Nhiệm vụ của chương trình của chúng tôi thực sự chỉ là nói cho CPU biết phải làm gì với những con số này.)

Sau đó, chúng tôi thêm một mức độ gián tiếp với int** ipp = &ip1;. Một lần nữa, chúng ta chỉ nhận được một đoạn ký ức:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

Các mô hình có vẻ quen thuộc. Một đoạn khác gồm 4 byte chứa một số.

Bây giờ, nếu chúng ta có một bộ nhớ trong bộ nhớ RAM giả tưởng ở trên, chúng ta có thể kiểm tra thủ công những con trỏ này ở đâu. Chúng tôi xem những gì được lưu trữ tại địa chỉ của ippbiến và tìm nội dung 0x12345680. Tất nhiên đó ip1là địa chỉ được lưu trữ. Chúng ta có thể đến địa chỉ đó, kiểm tra nội dung ở đó và tìm địa chỉ của i, và cuối cùng chúng ta có thể đến địa chỉ đó và tìm số 5.

Vì vậy, nếu chúng ta lấy nội dung của ipp *ipp, chúng ta sẽ nhận được địa chỉ của biến con trỏ ip1. Bằng cách viết, *ipp=ip2chúng tôi sao chép ip2 vào ip1, nó tương đương với ip1=ip2. Trong cả hai trường hợp, chúng tôi sẽ nhận được

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Những ví dụ này đã được đưa ra cho một CPU endian lớn)


5
Mặc dù tôi có quan điểm của bạn, có giá trị khi nghĩ về con trỏ là những thực thể trừu tượng, bí ẩn. Bất kỳ triển khai cụ thể nào của con trỏ chỉ là con số, nhưng chiến lược thực hiện mà bạn phác thảo không phải là một yêu cầu của việc triển khai, nó chỉ là một chiến lược chung. Con trỏ không cần phải có cùng kích thước với int, con trỏ không cần phải là địa chỉ trong mô hình bộ nhớ ảo phẳng, v.v. đây chỉ là chi tiết thực hiện.
Eric Lippert

@EricLippert Tôi nghĩ rằng người ta có thể làm cho ví dụ này trừu tượng hơn bằng cách không sử dụng địa chỉ bộ nhớ hoặc khối dữ liệu thực tế. Nếu đó là một bảng cho biết location, value, variablevị trí 1,2,3,4,5và giá trị của vị trí A,1,B,C,3, thì ý tưởng tương ứng của con trỏ có thể được giải thích dễ dàng mà không cần sử dụng mũi tên, vốn đã gây nhầm lẫn. Với bất kỳ cách thực hiện nào mà người ta chọn, một giá trị tồn tại ở một vị trí nào đó và đây là một phần của câu đố trở nên khó hiểu khi mô hình hóa bằng mũi tên.
MirroredFate

@EricLippert Theo kinh nghiệm của tôi, hầu hết các lập trình viên C sẽ gặp vấn đề trong việc hiểu con trỏ, là những người được cho ăn các mô hình nhân tạo trừu tượng. Trừu tượng là không hữu ích, bởi vì toàn bộ mục đích của ngôn ngữ C ngày nay, là nó gần với phần cứng. Nếu bạn đang học C nhưng không có ý định viết mã gần với phần cứng, bạn đang lãng phí thời gian . Java vv là một lựa chọn tốt hơn nhiều nếu bạn không muốn biết máy tính hoạt động như thế nào, nhưng chỉ cần lập trình cấp cao.
Lundin

@EricLippert Và vâng, có thể tồn tại nhiều cách triển khai khác nhau của các con trỏ, trong đó các con trỏ không nhất thiết phải tương ứng với các địa chỉ. Nhưng vẽ mũi tên sẽ không giúp bạn hiểu cách thức hoạt động của chúng. Tại một số điểm, bạn phải rời khỏi suy nghĩ trừu tượng và xuống cấp độ phần cứng, nếu không bạn không nên sử dụng C. Có nhiều ngôn ngữ hiện đại, phù hợp hơn dành cho lập trình cấp cao hoàn toàn trừu tượng.
Lundin

@Lundin: Tôi cũng không phải là một fan hâm mộ lớn của sơ đồ mũi tên; khái niệm về một mũi tên như dữ liệu là một điều khó khăn. Tôi thích nghĩ về nó một cách trừu tượng nhưng không có mũi tên. Các &nhà điều hành trên một biến mang đến cho bạn một đồng xu mà đại diện cho biến đó. Các *nhà điều hành trên đồng tiền cung cấp cho bạn sao lưu các thay đổi. Không cần mũi tên!
Eric Lippert

8

Lưu ý các bài tập:

ipp = &ip1;

kết quả ippđể chỉ đến ip1.

Vì vậy, ippđể chỉ ra ip2, chúng ta nên thay đổi theo cách tương tự,

ipp = &ip2;

điều mà chúng tôi rõ ràng là không làm Thay vào đó, chúng tôi đang thay đổi giá trị tại địa chỉ được chỉ bởi ipp.
Bằng cách làm

*ipp = ip2;

chúng tôi chỉ thay thế giá trị được lưu trữ trong ip1.

ipp = &ip1, có nghĩa là *ipp = ip1 = &i,
Bây giờ , *ipp = ip2 = &j.
Vì vậy, *ipp = ip2về cơ bản là giống như ip1 = ip2.


5
ipp = &ip1;

Không có sự phân công sau này đã thay đổi giá trị của ipp. Đây là lý do tại sao nó vẫn chỉ đến ip1.

Những gì bạn làm với *ipp, tức là với ip1, không thay đổi thực tế mà ippchỉ ra ip1.


5

Câu hỏi của tôi là: Tại sao trong bức ảnh thứ hai, ipp vẫn trỏ đến ip1 mà không phải là ip2?

bạn đã đặt những bức ảnh đẹp, tôi sẽ cố gắng tạo ra nghệ thuật ascii đẹp:

Giống như @ Robert-S-Barnes đã nói trong câu trả lời của mình: hãy quên đi những gợi ý , và những gì chỉ vào cái gì, nhưng hãy nghĩ về bộ nhớ. Về cơ bản, int*có nghĩa là nó chứa địa chỉ của một biến và một int**địa chỉ chứa một biến chứa địa chỉ của một biến. Sau đó, bạn có thể sử dụng đại số của con trỏ để truy cập các giá trị hoặc địa chỉ: &foophương tiện address of foo*foophương tiệnvalue of the address contained in foo .

Vì vậy, vì con trỏ liên quan đến việc xử lý bộ nhớ, cách tốt nhất để thực sự "hữu hình" đó là hiển thị những gì đại số con trỏ làm cho bộ nhớ.

Vì vậy, đây là bộ nhớ chương trình của bạn (được đơn giản hóa cho mục đích của ví dụ):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

khi bạn làm mã ban đầu:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

Đây là bộ nhớ của bạn trông như thế nào:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

đó bạn có thể nhìn thấy ip1ip2nhận được địa chỉ của ijipp vẫn không tồn tại. Đừng quên rằng địa chỉ đơn giản là số nguyên được lưu trữ với một loại đặc biệt.

Sau đó, bạn khai báo và định nghĩa, ippchẳng hạn như:

int **ipp = &ip1;

vì vậy đây là bộ nhớ của bạn:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

và sau đó, bạn đang thay đổi giá trị được chỉ ra bởi địa chỉ được lưu trữ ipp, đó là địa chỉ được lưu trữ trong ip1:

*ipp = ip2;

bộ nhớ của chương trình là

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: như int*là một loại đặc biệt, tôi thích luôn luôn tránh khai báo nhiều con trỏ trên cùng một dòng, vì tôi nghĩ rằng int *x;hoặcint *x, *y; ký hiệu ký hiệu có thể gây hiểu nhầm. Tôi thích viếtint* x; int* y;

HTH


với ví dụ của bạn, giá trị ban đầu của ip2nên 3không 4.
Dipto 6/214

1
oh, tôi chỉ thay đổi bộ nhớ để nó phù hợp với thứ tự khai báo. Tôi đoán tôi đã sửa nó làm như vậy?
zmo

5

Bởi vì khi bạn nói

*ipp = ip2

bạn đang nói 'đối tượng được chỉ bởi ipp' để chỉ hướng của bộ nhớip2 đang chỉ.

Bạn không nói ippđể chỉ ip2.


4

Nếu bạn thêm toán tử dereference *vào con trỏ, bạn chuyển hướng từ con trỏ đến đối tượng trỏ.

Ví dụ:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Vì thế:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

3

Nếu bạn muốn ippchỉ đến ip2, bạn phải nói ipp = &ip2;. Tuy nhiên, điều này sẽ để lại ip1vẫn chỉ vào i.


3

Bạn bắt đầu thiết lập

ipp = &ip1;

Bây giờ hãy coi đó là,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 

3

Xem xét từng biến đại diện như thế này:

type  : (name, adress, value)

vì vậy các biến của bạn nên được biểu diễn như thế này

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Vì giá trị của ippnó là &ip1như vậy

*ipp = ip2;

thay đổi giá trị tại addess &ip1thành giá trị của ip2, có nghĩa ip1là thay đổi:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Nhưng ippvẫn:

(ipp, &ipp, &ip1)

Vì vậy, giá trị của ippstill &ip1có nghĩa là nó vẫn trỏ đến ip1.


1

Bởi vì bạn đang thay đổi con trỏ của *ipp. Nó có nghĩa là

  1. ipp (tên thay đổi) ---- đi vào trong.
  2. bên trong ipplà địa chỉ củaip1 .
  3. bây giờ *ippđi đến (địa chỉ bên trong) ip1.

Bây giờ chúng tôi đang ở ip1. *ipp(tức là ip1) = ip2.
ip2chứa địa chỉ của nội dung j.so ip1sẽ được thay thế bằng chứa ip2 (tức là địa chỉ của j), CHÚNG TÔI KHÔNG THAY ĐỔI ippNỘI DUNG. ĐÓ LÀ NÓ.


1

*ipp = ip2; ngụ ý:

Gán ip2cho biến chỉ bởi ipp. Vì vậy, điều này tương đương với:

ip1 = ip2;

Nếu bạn muốn địa chỉ của ip2được lưu trữ ipp, chỉ cần làm:

ipp = &ip2;

Bây giờ ippchỉ vào ip2.


0

ippcó thể giữ một giá trị (tức là trỏ đến) một con trỏ tới đối tượng kiểu con trỏ . Khi bạn làm

ipp = &ip2;  

sau đó ippchứa địa chỉ của biến (con trỏ)ip2 , là ( &ip2) của kiểu con trỏ thành con trỏ . Bây giờ mũi tên ipptrong pic thứ hai sẽ trỏ đến ip2.

Wiki nói:
Các *nhà điều hành là một toán tử tham chiếu hoạt động trên biến con trỏ, và trả về một l-giá trị (biến) tương đương với giá trị tại địa chỉ con trỏ. Điều này được gọi là hội nghị con trỏ.

Áp dụng *toán tử trên việc ipphủy đăng ký nó thành giá trị l của con trỏ đểint gõ. Giá trị l *ippđược ước tính là của con trỏint kiểu , nó có thể giữ địa chỉ của intdữ liệu kiểu. Sau khi tuyên bố

ipp = &ip1;

ippđang giữ địa chỉ của ip1*ippđang giữ địa chỉ của (trỏ đến) i. Bạn có thể nói đó *ipplà một bí danh ip1. Cả hai **ipp*ip1là bí danh cho i.
Bằng cách làm

 *ipp = ip2;  

*ippip2cả hai điểm đến cùng một vị trí nhưng ippvẫn đang trỏ đến ip1.

Điều *ipp = ip2;thực sự là nó sao chép nội dung của ip2(địa chỉ của j) thành ip1(như *ipplà một bí danh choip1 ), thực tế là làm cho cả hai con trỏ ip1ip2trỏ đến cùng một đối tượng ( j).
Vì vậy, trong hình thứ hai, mũi tên của ip1ip2đang chỉ vào jtrong khi ippvẫn đang chỉ vào ip1vì không có sửa đổi nào được thực hiện để thay đổi giá trị củaipp .

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.