Rào cản để hiểu con trỏ là gì và có thể làm gì để vượt qua chúng? [đóng cửa]


449

Tại sao các con trỏ lại là một yếu tố gây nhầm lẫn hàng đầu đối với nhiều sinh viên mới, và thậm chí cũ, ở đại học C hoặc C ++? Có công cụ hay quy trình suy nghĩ nào giúp bạn hiểu cách con trỏ hoạt động ở biến, hàm và vượt mức không?

Một số điều thực hành tốt có thể được thực hiện để đưa ai đó đến mức, "Ah-hah, tôi hiểu rồi", mà không khiến họ sa lầy trong khái niệm tổng thể? Về cơ bản, khoan như kịch bản.


20
Luận điểm của câu hỏi này là con trỏ rất khó hiểu. Câu hỏi không đưa ra bằng chứng nào cho thấy con trỏ khó hiểu hơn bất kỳ thứ gì khác.
bmargulies

14
Có thể tôi đang thiếu một cái gì đó (vì tôi viết mã bằng các ngôn ngữ của GCC) nhưng tôi luôn nghĩ nếu con trỏ trong bộ nhớ là cấu trúc Khóa-> Giá trị. Vì tốn kém để truyền xung quanh một lượng lớn dữ liệu trong một chương trình, bạn tạo cấu trúc (giá trị) và chuyển xung quanh con trỏ / tham chiếu (khóa) vì khóa là biểu diễn nhỏ hơn nhiều của cấu trúc lớn hơn. Phần khó là khi bạn cần so sánh hai con trỏ / tham chiếu (bạn đang so sánh các khóa hoặc các giá trị) đòi hỏi nhiều công việc hơn để xâm nhập vào dữ liệu chứa trong cấu trúc (giá trị).
Evan Plaice

2
@ Wolfpack'08 "Dường như với tôi một bộ nhớ trong địa chỉ sẽ luôn là một số nguyên." - Sau đó, có vẻ như bạn không có gì có một loại, vì tất cả chúng chỉ là bit trong bộ nhớ. "Trên thực tế, loại của con trỏ là loại var mà con trỏ trỏ tới" - Không, loại của con trỏcon trỏ tới loại var mà con trỏ trỏ tới - điều này là tự nhiên và nên rõ ràng.
Jim Balter

2
Tôi luôn tự hỏi những gì rất khó nắm bắt trong thực tế là các biến (và hàm) chỉ là các khối bộ nhớ và các con trỏ là các biến lưu trữ địa chỉ bộ nhớ. Mô hình suy nghĩ quá thực tế này có thể không gây ấn tượng với tất cả những người hâm mộ các khái niệm trừu tượng, nhưng nó hoàn toàn giúp hiểu cách thức hoạt động của con trỏ.
Christian Rau

8
Tóm lại, sinh viên có thể không hiểu vì họ không hiểu chính xác hoặc hoàn toàn không biết cách bộ nhớ của máy tính nói chung và cụ thể là "mô hình bộ nhớ" C hoạt động. Cuốn sách này Lập trình từ Ground Up cho một bài học rất hay về các chủ đề này.
Abbafei

Câu trả lời:


745

Con trỏ là một khái niệm mà ban đầu đối với nhiều người có thể gây nhầm lẫn, đặc biệt khi nói đến việc sao chép các giá trị con trỏ xung quanh và vẫn tham chiếu cùng một khối bộ nhớ.

Tôi đã thấy rằng sự tương tự tốt nhất là coi con trỏ là một mảnh giấy có địa chỉ nhà ở trên đó và khối bộ nhớ mà nó tham chiếu như ngôi nhà thực tế. Tất cả các loại hoạt động có thể được giải thích dễ dàng.

Tôi đã thêm một số mã Delphi xuống bên dưới và một số nhận xét khi thích hợp. Tôi đã chọn Delphi vì ngôn ngữ lập trình chính khác của tôi, C #, không thể hiện những thứ như rò rỉ bộ nhớ theo cùng một cách.

Nếu bạn chỉ muốn tìm hiểu khái niệm con trỏ cấp cao, thì bạn nên bỏ qua các phần có nhãn "Bố cục bộ nhớ" trong phần giải thích bên dưới. Họ dự định đưa ra các ví dụ về bộ nhớ có thể trông như thế nào sau các hoạt động, nhưng về bản chất chúng có mức độ thấp hơn. Tuy nhiên, để giải thích chính xác cách thức tràn bộ đệm thực sự hoạt động, điều quan trọng là tôi đã thêm các sơ đồ này.

Tuyên bố miễn trừ trách nhiệm: Đối với tất cả ý định và mục đích, phần giải thích và bố trí bộ nhớ mẫu này được đơn giản hóa rất nhiều. Có nhiều chi phí hoạt động hơn và nhiều chi tiết hơn bạn sẽ cần biết nếu bạn cần xử lý bộ nhớ ở mức độ thấp. Tuy nhiên, đối với ý định giải thích bộ nhớ và con trỏ, nó đủ chính xác.


Giả sử lớp THouse được sử dụng dưới đây trông như thế này:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

Khi bạn khởi tạo đối tượng ngôi nhà, tên được đặt cho hàm tạo sẽ được sao chép vào trường riêng FName. Có một lý do nó được định nghĩa là một mảng có kích thước cố định.

Trong bộ nhớ, sẽ có một số chi phí liên quan đến việc phân bổ nhà, tôi sẽ minh họa điều này dưới đây như sau:

--- [ttttNNNNNNNNNN] ---
     ^ ^
     | |
     | + - mảng FName
     |
     + - trên cao

Vùng "tttt" nằm trên đầu, thông thường sẽ có nhiều hơn cho các loại thời gian chạy và ngôn ngữ khác nhau, như 8 hoặc 12 byte. Điều bắt buộc là bất kỳ giá trị nào được lưu trữ trong khu vực này sẽ không bao giờ bị thay đổi bởi bất kỳ thứ gì ngoài bộ cấp phát bộ nhớ hoặc các thói quen hệ thống cốt lõi, hoặc bạn có nguy cơ làm hỏng chương trình.


Phân bổ bộ nhớ

Nhận một doanh nhân để xây dựng ngôi nhà của bạn, và cung cấp cho bạn địa chỉ đến ngôi nhà. Trái ngược với thế giới thực, việc phân bổ bộ nhớ không thể được chỉ định nơi phân bổ, nhưng sẽ tìm một vị trí phù hợp với đủ chỗ và báo cáo lại địa chỉ cho bộ nhớ được phân bổ.

Nói cách khác, doanh nhân sẽ chọn vị trí.

THouse.Create('My house');

Bố cục bộ nhớ:

--- [ttttNNNNNNNNNN] ---
    1234 nhà tôi

Giữ một biến với địa chỉ

Viết địa chỉ đến ngôi nhà mới của bạn trên một tờ giấy. Bài viết này sẽ phục vụ như là tài liệu tham khảo của bạn đến ngôi nhà của bạn. Không có mảnh giấy này, bạn bị lạc và không thể tìm thấy ngôi nhà, trừ khi bạn đã ở trong đó.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Bố cục bộ nhớ:

    h
    v
--- [ttttNNNNNNNNNN] ---
    1234 nhà tôi

Sao chép giá trị con trỏ

Chỉ cần viết địa chỉ trên một tờ giấy mới. Bây giờ bạn có hai mảnh giấy sẽ đưa bạn đến cùng một ngôi nhà, không phải hai ngôi nhà riêng biệt. Bất kỳ nỗ lực nào để theo địa chỉ từ một tờ giấy và sắp xếp lại đồ đạc trong ngôi nhà đó sẽ khiến cho ngôi nhà kia đã được sửa đổi theo cách tương tự, trừ khi bạn có thể phát hiện rõ ràng rằng đó thực sự chỉ là một ngôi nhà.

Lưu ý Đây thường là khái niệm mà tôi gặp nhiều vấn đề nhất khi giải thích với mọi người, hai con trỏ không có nghĩa là hai đối tượng hoặc khối bộ nhớ.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
--- [ttttNNNNNNNNNN] ---
    1234 nhà tôi
    ^
    h2

Giải phóng bộ nhớ

Phá dỡ nhà. Sau đó, bạn có thể sử dụng lại giấy cho một địa chỉ mới nếu bạn muốn hoặc xóa nó để quên địa chỉ đến ngôi nhà không còn tồn tại.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

Ở đây trước tiên tôi xây dựng ngôi nhà, và giữ địa chỉ của nó. Sau đó, tôi làm một cái gì đó cho ngôi nhà (sử dụng nó, ... mã, để lại như một bài tập cho người đọc), và sau đó tôi giải phóng nó. Cuối cùng tôi xóa địa chỉ từ biến của tôi.

Bố cục bộ nhớ:

    h <- +
    v + - trước khi miễn phí
--- [ttttNNNNNNNNNN] --- |
    1234 Nhà tôi <- +

    h (bây giờ không ở đâu) <- +
                                + - sau khi rảnh
---------------------- | (lưu ý, bộ nhớ có thể vẫn còn
    xx34 Nhà của tôi <- + chứa một số dữ liệu)

Con trỏ lủng lẳng

Bạn nói với doanh nhân của bạn để phá hủy ngôi nhà, nhưng bạn quên xóa địa chỉ khỏi mảnh giấy của bạn. Khi sau này bạn nhìn vào mảnh giấy, bạn đã quên rằng ngôi nhà không còn ở đó nữa và đi thăm nó, với kết quả không thành công (xem thêm phần về một tài liệu tham khảo không hợp lệ bên dưới).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Sử dụng hsau cuộc gọi để .Free có thể làm việc, nhưng đó chỉ là may mắn thuần túy. Nhiều khả năng nó sẽ thất bại, tại một địa điểm của khách hàng, ở giữa một hoạt động quan trọng.

    h <- +
    v + - trước khi miễn phí
--- [ttttNNNNNNNNNN] --- |
    1234 Nhà tôi <- +

    h <- +
    v + - sau khi miễn phí
---------------------- |
    xx34 Nhà tôi <- +

Như bạn có thể thấy, h vẫn chỉ vào phần còn lại của dữ liệu trong bộ nhớ, nhưng vì nó có thể không hoàn thành, sử dụng nó như trước có thể thất bại.


Bộ nhớ bị rò rỉ

Bạn bị mất mảnh giấy và không thể tìm thấy ngôi nhà. Ngôi nhà vẫn đứng ở đâu đó và khi bạn muốn xây dựng một ngôi nhà mới, bạn không thể sử dụng lại vị trí đó.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Ở đây chúng tôi ghi đè lên nội dung của hbiến với địa chỉ của một ngôi nhà mới, nhưng ngôi nhà cũ vẫn còn ... ở đâu đó. Sau mã này, không có cách nào để đến được ngôi nhà đó, và nó sẽ bị bỏ lại. Nói cách khác, bộ nhớ được phân bổ sẽ được phân bổ cho đến khi ứng dụng đóng lại, lúc đó hệ điều hành sẽ phá hỏng nó.

Bố trí bộ nhớ sau lần cấp phát đầu tiên:

    h
    v
--- [ttttNNNNNNNNNN] ---
    1234 nhà tôi

Bố cục bộ nhớ sau khi cấp phát thứ hai:

                       h
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234 Nhà tôi 5678 Nhà tôi

Một cách phổ biến hơn để có được phương pháp này là chỉ cần quên giải phóng một cái gì đó, thay vì ghi đè lên nó như trên. Theo thuật ngữ Delphi, điều này sẽ xảy ra với phương pháp sau:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

Sau khi phương thức này được thực thi, không có chỗ nào trong các biến của chúng tôi có địa chỉ ngôi nhà tồn tại, nhưng ngôi nhà vẫn ở ngoài đó.

Bố cục bộ nhớ:

    h <- +
    v + - trước khi mất con trỏ
--- [ttttNNNNNNNNNN] --- |
    1234 Nhà tôi <- +

    h (bây giờ không ở đâu) <- +
                                + - sau khi mất con trỏ
--- [ttttNNNNNNNNNN] --- |
    1234 Nhà tôi <- +

Như bạn có thể thấy, dữ liệu cũ được giữ nguyên trong bộ nhớ và sẽ không được bộ cấp phát bộ nhớ sử dụng lại. Bộ cấp phát theo dõi những vùng bộ nhớ đã được sử dụng và sẽ không sử dụng lại chúng trừ khi bạn giải phóng nó.


Giải phóng bộ nhớ nhưng giữ tham chiếu (hiện không hợp lệ)

Phá hủy ngôi nhà, xóa một trong những mảnh giấy nhưng bạn cũng có một mảnh giấy khác có địa chỉ cũ trên đó, khi bạn đến địa chỉ, bạn sẽ không tìm thấy một ngôi nhà, nhưng bạn có thể tìm thấy thứ gì đó giống với tàn tích của một.

Có lẽ bạn thậm chí sẽ tìm thấy một ngôi nhà, nhưng đó không phải là ngôi nhà ban đầu bạn được cung cấp địa chỉ, và do đó, bất kỳ nỗ lực nào để sử dụng nó như thể nó thuộc về bạn có thể thất bại khủng khiếp.

Đôi khi, bạn thậm chí có thể thấy rằng một địa chỉ lân cận có một ngôi nhà khá lớn được thiết lập trên đó chiếm ba địa chỉ (Đường chính 1-3) và địa chỉ của bạn nằm ở giữa nhà. Bất kỳ nỗ lực nào để coi phần đó của ngôi nhà 3 địa chỉ lớn như một ngôi nhà nhỏ cũng có thể thất bại khủng khiếp.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Ở đây, ngôi nhà đã bị phá hủy, thông qua các tài liệu tham khảo h1, và trong khi h1cũng bị xóa, h2vẫn còn địa chỉ cũ, lỗi thời. Truy cập vào ngôi nhà không còn có thể hoặc có thể không hoạt động.

Đây là một biến thể của con trỏ lơ lửng ở trên. Xem bố trí bộ nhớ của nó.


Bộ đệm tràn

Bạn di chuyển nhiều thứ vào nhà hơn mức bạn có thể phù hợp, tràn vào nhà hàng xóm hoặc sân. Khi chủ sở hữu của ngôi nhà lân cận đó về nhà sau, anh ta sẽ tìm thấy tất cả những thứ anh ta sẽ xem xét của riêng mình.

Đây là lý do tôi chọn một mảng có kích thước cố định. Để thiết lập giai đoạn, giả sử rằng ngôi nhà thứ hai mà chúng ta phân bổ sẽ, vì một số lý do, được đặt trước ngôi nhà đầu tiên trong bộ nhớ. Nói cách khác, ngôi nhà thứ hai sẽ có địa chỉ thấp hơn ngôi nhà thứ nhất. Ngoài ra, chúng được phân bổ ngay cạnh nhau.

Do đó, mã này:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Bố trí bộ nhớ sau lần cấp phát đầu tiên:

                        h1
                        v
----------------------- [ttttNNNNNNNNNN]
                        5678 Nhà của tôi

Bố cục bộ nhớ sau khi cấp phát thứ hai:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
    1234 Nhà khác của tôi ở đâu đó
                        ^ --- + - ^
                            |
                            + - ghi đè

Phần thường gây ra sự cố là khi bạn ghi đè lên các phần quan trọng của dữ liệu bạn đã lưu trữ mà thực sự không nên thay đổi ngẫu nhiên. Ví dụ, có thể không có vấn đề gì khi các phần của tên của nhà h1 bị thay đổi, về mặt làm hỏng chương trình, nhưng việc ghi đè lên phần trên của đối tượng rất có thể sẽ bị sập khi bạn cố gắng sử dụng đối tượng bị hỏng, như sẽ ghi đè các liên kết được lưu trữ đến các đối tượng khác trong đối tượng.


Danh sách liên kết

Khi bạn theo dõi một địa chỉ trên một tờ giấy, bạn đến một ngôi nhà, và tại ngôi nhà đó có một mảnh giấy khác có địa chỉ mới trên đó, cho ngôi nhà tiếp theo trong chuỗi, v.v.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Ở đây chúng tôi tạo ra một liên kết từ nhà của chúng tôi đến cabin của chúng tôi. Chúng ta có thể theo chuỗi cho đến khi một ngôi nhà không có NextHousetài liệu tham khảo, có nghĩa là nó là ngôi nhà cuối cùng. Để truy cập tất cả các ngôi nhà của chúng tôi, chúng tôi có thể sử dụng mã sau đây:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Bố cục bộ nhớ (được thêm NextHouse làm liên kết trong đối tượng, được ghi chú bằng bốn LLLL trong sơ đồ bên dưới):

    h1 h2
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home + 5678Cabin +
                   | ^ |
                   + -------- + * (không có liên kết)

Trong thuật ngữ cơ bản, địa chỉ bộ nhớ là gì?

Một địa chỉ bộ nhớ trong điều khoản cơ bản chỉ là một số. Nếu bạn nghĩ bộ nhớ là một mảng lớn byte, thì byte đầu tiên có địa chỉ 0, tiếp theo là địa chỉ 1 và cứ thế trở lên. Điều này được đơn giản hóa, nhưng đủ tốt.

Vì vậy, bố trí bộ nhớ này:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234 Nhà tôi 5678 Nhà tôi

Có thể có hai địa chỉ này (ngoài cùng bên trái - là địa chỉ 0):

  • h1 = 4
  • h2 = 23

Điều đó có nghĩa là danh sách được liên kết của chúng tôi ở trên có thể giống như thế này:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home 0028 5678Cabin 0000
                   | ^ |
                   + -------- + * (không có liên kết)

Nó là điển hình để lưu trữ một địa chỉ "không nơi nào" là địa chỉ không.


Trong thuật ngữ cơ bản, một con trỏ là gì?

Một con trỏ chỉ là một biến giữ một địa chỉ bộ nhớ. Thông thường bạn có thể yêu cầu ngôn ngữ lập trình cung cấp cho bạn số của nó, nhưng hầu hết các ngôn ngữ lập trình và thời gian chạy đều cố gắng che giấu sự thật rằng có một số bên dưới, chỉ vì bản thân con số đó không thực sự có ý nghĩa gì với bạn. Tốt nhất là nghĩ về một con trỏ như một hộp đen, tức là. bạn không thực sự biết hoặc quan tâm đến cách nó được thực hiện, miễn là nó hoạt động.


59
Các bộ đệm tràn ngập là vui nhộn. "Hàng xóm trở về nhà, làm nứt hộp sọ của anh ta trượt trên rác của bạn và kiện bạn vào quên lãng."
gtd

12
Đây là một lời giải thích tốt đẹp của khái niệm, chắc chắn. Khái niệm này KHÔNG phải là điều mà tôi thấy khó hiểu về con trỏ, vì vậy toàn bộ bài luận này hơi lãng phí.
Breton

10
Nhưng chỉ cần hỏi, bạn thấy khó hiểu về con trỏ là gì?
Lasse V. Karlsen

11
Tôi đã xem lại bài đăng này nhiều lần kể từ khi bạn viết câu trả lời. Sự làm rõ của bạn với mã là tuyệt vời và tôi đánh giá cao bạn xem lại nó để thêm / tinh chỉnh thêm suy nghĩ. Bravo Lasse!
David McGraw

3
Không có cách nào một trang văn bản (dù dài bao nhiêu) có thể tổng hợp mọi sắc thái của quản lý bộ nhớ, tài liệu tham khảo, con trỏ, v.v. trang bắt đầu trên thông tin. Có nhiều thứ để học không? Chắc chắn, khi nào thì không?
Lasse V. Karlsen

153

Trong lớp Comp Sci đầu tiên của tôi, chúng tôi đã làm bài tập sau. Cấp, đây là một giảng đường với khoảng 200 sinh viên trong đó ...

Giáo sư viết lên bảng: int john;

John đứng lên

Giáo sư viết: int *sally = &john;

Sally đứng lên, chỉ vào john

Giáo sư: int *bill = sally;

Bill đứng dậy, chỉ vào John

Giáo sư: int sam;

Sam đứng lên

Giáo sư: bill = &sam;

Bill bây giờ chỉ vào Sam.

Tôi nghĩ rằng bạn có được ý tưởng. Tôi nghĩ rằng chúng tôi đã dành khoảng một giờ để làm điều này, cho đến khi chúng tôi đi qua những điều cơ bản của việc gán con trỏ.


5
Tôi không nghĩ rằng tôi đã hiểu sai. Ý định của tôi là thay đổi giá trị của biến chỉ từ John sang Sam. Việc trình bày với mọi người khó hơn một chút, vì có vẻ như bạn đang thay đổi giá trị của cả hai con trỏ.
Thử

24
Nhưng lý do khiến nó khó hiểu là vì nó không giống như john đứng dậy khỏi chỗ ngồi và sau đó sam ngồi xuống, như chúng ta có thể tưởng tượng. Nó giống như sam đến và đưa tay vào john và nhân bản chương trình sam vào cơ thể của john, giống như hugo dệt trong ma trận được tải lại.
Breton

59
Giống như Sam chiếm chỗ của John, và John bay quanh phòng cho đến khi anh ta va vào một thứ gì đó quan trọng và gây ra một vụ đột nhập.
just_wes

2
Cá nhân tôi thấy ví dụ này phức tạp không cần thiết. Prof của tôi bảo tôi chỉ vào một ánh sáng và nói "bàn tay của bạn là con trỏ đến vật sáng".
Celeritas

Vấn đề với loại ví dụ này là con trỏ tới X và X không giống nhau. Và điều này không được miêu tả với mọi người.
Isaac Nequittepas

124

Một sự tương tự tôi thấy hữu ích cho việc giải thích con trỏ là siêu liên kết. Hầu hết mọi người có thể hiểu rằng một liên kết trên một trang web 'trỏ' đến một trang khác trên internet và nếu bạn có thể sao chép và dán siêu liên kết đó thì cả hai sẽ trỏ đến cùng một trang web gốc. Nếu bạn đi và chỉnh sửa trang gốc đó, thì hãy theo một trong các liên kết (con trỏ) đó, bạn sẽ nhận được trang cập nhật mới đó.


15
Tôi thực sự thích điều này. Không khó để thấy rằng việc viết ra một siêu liên kết hai lần không làm cho hai trang web xuất hiện (giống như int *a = bkhông tạo ra hai bản sao *b).
gièm pha

4
Điều này thực sự rất trực quan và một cái gì đó mọi người sẽ có thể liên quan đến. Mặc dù có rất nhiều tình huống mà sự tương tự này sụp đổ. Tuyệt vời cho một giới thiệu nhanh mặc dù. +1
Brian Wigginton

Một liên kết đến một trang được mở ra hai lần thường tạo ra hai trường hợp gần như hoàn toàn độc lập của trang web đó. Tôi nghĩ rằng một siêu liên kết có thể là một sự tương tự tốt với một nhà xây dựng có thể, nhưng không phải là một con trỏ.
Utkan Gezer

@ThoAppelsin Chẳng nhất thiết là đúng, nếu bạn đang truy cập một trang web html tĩnh, thì bạn đang truy cập một tệp duy nhất trên máy chủ.
kịch tính

5
Bạn đang xem xét nó. Các siêu liên kết trỏ đến các tệp trên máy chủ, đó là phạm vi tương tự.
kịch tính

48

Lý do con trỏ dường như gây nhầm lẫn cho nhiều người là vì họ chủ yếu đi kèm với ít hoặc không có nền tảng về kiến ​​trúc máy tính. Vì nhiều người dường như không có ý tưởng về cách máy tính (máy) thực sự được triển khai - hoạt động trong C / C ++ có vẻ xa lạ.

Một thao tác khoan là yêu cầu họ triển khai một máy ảo dựa trên mã byte đơn giản (trong bất kỳ ngôn ngữ nào họ chọn, python hoạt động rất tốt cho việc này) với một tập lệnh được tập trung vào các thao tác con trỏ (tải, lưu trữ, địa chỉ trực tiếp / gián tiếp). Sau đó yêu cầu họ viết các chương trình đơn giản cho tập lệnh đó.

Bất cứ điều gì đòi hỏi nhiều hơn một chút so với bổ sung đơn giản sẽ liên quan đến con trỏ và họ chắc chắn sẽ có được nó.


2
Hấp dẫn. Không biết làm thế nào để bắt đầu đi về điều này mặc dù. Bất kỳ nguồn lực để chia sẻ?
Karolis

1
Tôi đồng ý. Ví dụ, tôi đã học lập trình lắp ráp trước C và biết cách đăng ký hoạt động, việc học con trỏ rất dễ dàng. Trên thực tế, không có nhiều học hỏi, tất cả đều diễn ra rất tự nhiên.
Milan Babuškov

Lấy một CPU cơ bản, nói một cái gì đó chạy máy cắt cỏ hoặc máy rửa chén và thực hiện nó. Hoặc một tập hợp con rất cơ bản của ARM hoặc MIPS. Cả hai đều có một ISA rất đơn giản.
Daniel Goldberg

1
Có thể đáng để chỉ ra rằng phương pháp giáo dục này đã được chính Donald Knuth ủng hộ / thực hành. Nghệ thuật lập trình máy tính của Knuth mô tả một kiến ​​trúc giả định đơn giản và yêu cầu sinh viên thực hiện các giải pháp để thực hành các vấn đề bằng ngôn ngữ lắp ráp giả thuyết cho kiến ​​trúc đó. Khi nó trở nên khả thi trên thực tế, một số sinh viên đọc sách của Knuth thực sự triển khai kiến ​​trúc của mình như một VM (hoặc sử dụng một triển khai hiện có) và thực sự chạy các giải pháp của họ. IMO đây là một cách tuyệt vời để học, nếu bạn có thời gian.
WeirdlyCheezy

4
@Luke Tôi không nghĩ rằng thật dễ hiểu cho những người chỉ đơn giản là không thể nắm bắt được con trỏ (hay nói chính xác hơn là nói chung). Về cơ bản, bạn cho rằng những người không hiểu về con trỏ trong C sẽ có thể bắt đầu học lắp ráp, hiểu kiến ​​trúc cơ bản của máy tính và quay lại C với sự hiểu biết về con trỏ. Điều này có thể đúng với nhiều người, nhưng theo một số nghiên cứu, dường như một số người vốn không thể nắm bắt được sự quyết đoán, thậm chí về nguyên tắc (tôi vẫn thấy điều đó rất khó tin, nhưng có lẽ tôi đã may mắn với "học sinh của mình ").
Luaan

27

Tại sao con trỏ lại là một yếu tố gây nhầm lẫn hàng đầu đối với nhiều sinh viên mới, và thậm chí cũ, ở đại học trong ngôn ngữ C / C ++?

Khái niệm giữ chỗ cho một giá trị - biến - ánh xạ vào thứ chúng ta được dạy ở trường - đại số. Không có song song tồn tại mà bạn có thể vẽ mà không hiểu cách bộ nhớ được sắp xếp vật lý trong máy tính và không ai nghĩ về loại điều này cho đến khi họ xử lý những thứ ở mức độ thấp - ở cấp độ truyền thông C / C ++ / byte .

Có công cụ hay quy trình suy nghĩ nào giúp bạn hiểu cách con trỏ hoạt động ở biến, hàm và vượt mức không?

Địa chỉ hộp. Tôi nhớ khi tôi đang học lập trình BASIC thành các máy vi tính, có những cuốn sách nhỏ này có các trò chơi trong đó và đôi khi bạn phải chọc các giá trị vào các địa chỉ cụ thể. Họ đã có một hình ảnh của một loạt các hộp, được dán nhãn tăng dần bằng 0, 1, 2 ... và được giải thích rằng chỉ một điều nhỏ (một byte) có thể nằm gọn trong các hộp này và có rất nhiều trong số họ - một số máy tính có tới 65535! Họ ở cạnh nhau, và tất cả họ đều có một địa chỉ.

Một số điều thực hành tốt có thể được thực hiện để đưa ai đó đến mức, "Ah-hah, tôi hiểu rồi", mà không khiến họ sa lầy trong khái niệm tổng thể? Về cơ bản, khoan như kịch bản.

Cho một mũi khoan? Tạo cấu trúc:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Ví dụ tương tự như trên, ngoại trừ trong C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Đầu ra:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Có lẽ điều đó giải thích một số điều cơ bản thông qua ví dụ?


+1 cho "không hiểu cách bộ nhớ được đặt vật lý". Tôi đến C từ một nền tảng ngôn ngữ lắp ráp và khái niệm con trỏ rất tự nhiên và dễ dàng; và tôi đã thấy những người chỉ có một cuộc đấu tranh ngôn ngữ cấp cao hơn để tìm ra nó. Để làm cho nó tệ hơn cú pháp là khó hiểu (con trỏ hàm!), Vì vậy việc học khái niệm và cú pháp cùng một lúc là một công thức cho rắc rối.
Brendan

4
Tôi biết đây là một bài viết cũ, nhưng sẽ rất tuyệt nếu đầu ra của mã được cung cấp được thêm vào bài viết.
Josh

Vâng, nó tương tự như đại số (mặc dù đại số có một điểm dễ hiểu hơn khi có "biến" bất biến). Nhưng khoảng một nửa số người tôi biết không có hiểu biết về đại số trong thực tế. Nó chỉ không tính toán cho họ. Họ biết tất cả những "phương trình" và đơn thuốc để đi đến kết quả, nhưng họ áp dụng chúng một cách ngẫu nhiên và vụng về. Và họ không thể mở rộng chúng cho mục đích riêng của họ - đó chỉ là một hộp đen không thể thay đổi, không thể thay thế cho họ. Nếu bạn hiểu đại số và có thể sử dụng nó một cách hiệu quả, bạn đã đi trước gói - ngay cả trong số các lập trình viên.
Luaan

24

Lý do tôi đã có một thời gian khó hiểu con trỏ, lúc đầu, là nhiều lời giải thích bao gồm rất nhiều điều tào lao về việc chuyển qua tham chiếu. Tất cả điều này không gây nhầm lẫn vấn đề. Khi bạn sử dụng một tham số con trỏ, bạn vẫn truyền theo giá trị; nhưng giá trị xảy ra là một địa chỉ chứ không phải là một int.

Một số người khác đã liên kết với hướng dẫn này, nhưng tôi có thể làm nổi bật khoảnh khắc khi tôi bắt đầu hiểu về con trỏ:

Hướng dẫn về Con trỏ và Mảng trong C: Chương 3 - Con trỏ và Chuỗi

int puts(const char *s);

Hiện tại, bỏ qua const.tham số được truyền đến puts()là một con trỏ, đó là giá trị của một con trỏ (vì tất cả các tham số trong C được truyền theo giá trị) và giá trị của một con trỏ là địa chỉ mà nó trỏ tới, hoặc, đơn giản , một địa chỉ. Do đó, khi chúng ta viết puts(strA);như chúng ta đã thấy, chúng ta sẽ chuyển địa chỉ của strA [0].

Khoảnh khắc tôi đọc những từ này, những đám mây tách ra và một tia sáng mặt trời bao trùm lấy tôi bằng sự hiểu biết về con trỏ.

Ngay cả khi bạn là nhà phát triển VB .NET hoặc C # (như tôi) và không bao giờ sử dụng mã không an toàn, vẫn đáng để hiểu cách con trỏ hoạt động hoặc bạn sẽ không hiểu cách tham chiếu đối tượng hoạt động. Sau đó, bạn sẽ có một khái niệm phổ biến nhưng sai lầm rằng chuyển một tham chiếu đối tượng đến một phương thức sao chép đối tượng.


Để tôi tự hỏi ý nghĩa của việc có một con trỏ là gì. Trong hầu hết các khối mã tôi gặp, một con trỏ chỉ được nhìn thấy trong khai báo của nó.
Wolfpack'08

@ Wolfpack'08 ... cái gì ?? Bạn đang nhìn vào mã nào ??
Kyle Strand

@KyleStrand Hãy để tôi xem.
Wolfpack'08

19

Tôi tìm thấy "Hướng dẫn về con trỏ và mảng trong C" của Ted Jensen là một tài nguyên tuyệt vời để tìm hiểu về con trỏ. Nó được chia thành 10 bài học, bắt đầu bằng một lời giải thích về con trỏ là gì (và chúng dùng để làm gì) và hoàn thiện với các con trỏ hàm. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Tiếp tục từ đó, Hướng dẫn lập trình mạng của Beej dạy API socket Unix, từ đó bạn có thể bắt đầu làm những điều thực sự thú vị. http://beej.us/guide/bgnet/


1
Tôi thứ hai hướng dẫn của Ted Jensen. Nó chia nhỏ con trỏ đến một mức độ chi tiết, không quá chi tiết, không có cuốn sách nào tôi đọc. Vô cùng hữu ích! :)
Dave Gallagher

12

Sự phức tạp của con trỏ vượt xa những gì chúng ta có thể dễ dàng dạy. Có học sinh chỉ vào nhau và sử dụng các mảnh giấy với địa chỉ nhà là cả hai công cụ học tập tuyệt vời. Họ làm một công việc tuyệt vời để giới thiệu các khái niệm cơ bản. Thật vậy, học các khái niệm cơ bản là rất quan trọng để sử dụng thành công con trỏ. Tuy nhiên, trong mã sản xuất, việc tham gia vào các kịch bản phức tạp hơn nhiều so với các trình diễn đơn giản này có thể gói gọn.

Tôi đã tham gia vào các hệ thống nơi chúng tôi có các cấu trúc chỉ đến các cấu trúc khác chỉ đến các cấu trúc khác. Một số trong các cấu trúc đó cũng chứa các cấu trúc nhúng (thay vì con trỏ đến các cấu trúc bổ sung). Đây là nơi con trỏ nhận được thực sự khó hiểu. Nếu bạn có nhiều cấp độ gián tiếp và bạn bắt đầu kết thúc bằng mã như thế này:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

nó có thể gây nhầm lẫn thực sự nhanh chóng (tưởng tượng nhiều dòng hơn, và có khả năng nhiều cấp độ hơn). Ném vào các mảng của con trỏ và nút tới con trỏ nút (cây, danh sách được liên kết) và nó vẫn còn tệ hơn. Tôi đã thấy một số nhà phát triển thực sự giỏi bị lạc một khi họ bắt đầu làm việc trên các hệ thống như vậy, ngay cả những nhà phát triển hiểu rất rõ những điều cơ bản.

Cấu trúc phức tạp của con trỏ không nhất thiết chỉ ra mã kém, (mặc dù chúng có thể). Thành phần là một phần quan trọng của lập trình hướng đối tượng tốt, và trong các ngôn ngữ có con trỏ thô, chắc chắn nó sẽ dẫn đến tình trạng đa hướng. Hơn nữa, các hệ thống thường cần sử dụng các thư viện của bên thứ ba với các cấu trúc không khớp với nhau về kiểu dáng hoặc kỹ thuật. Trong những tình huống như vậy, sự phức tạp sẽ tự nhiên phát sinh (mặc dù chắc chắn, chúng ta nên chiến đấu với nó càng nhiều càng tốt).

Tôi nghĩ rằng điều tốt nhất các trường đại học có thể làm để giúp sinh viên học con trỏ là sử dụng các cuộc biểu tình tốt, kết hợp với các dự án yêu cầu sử dụng con trỏ. Một dự án khó sẽ làm nhiều hơn cho sự hiểu biết con trỏ hơn một ngàn cuộc biểu tình. Biểu tình có thể giúp bạn hiểu biết nông cạn, nhưng để nắm bắt sâu sắc con trỏ, bạn phải thực sự sử dụng chúng.


10

Tôi nghĩ rằng tôi đã thêm một sự tương tự vào danh sách này mà tôi thấy rất hữu ích khi giải thích các gợi ý (trở lại trong ngày) với tư cách là một Gia sư Khoa học Máy tính; đầu tiên, hãy:


Đặt giai đoạn :

Hãy xem xét một bãi đậu xe với 3 không gian, những không gian này được đánh số:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

Theo một cách nào đó, đây giống như các vị trí bộ nhớ, chúng là tuần tự và liền kề .. giống như một mảng. Ngay bây giờ không có xe trong đó vì vậy nó giống như một mảng trống ( parking_lot[3] = {0}).


Thêm dữ liệu

Một bãi đậu xe không bao giờ trống rỗng lâu ... nếu nó làm điều đó sẽ là vô nghĩa và không ai sẽ xây dựng bất kỳ. Vì vậy, hãy nói rằng khi ngày di chuyển trên rất nhiều với 3 chiếc xe, một chiếc xe màu xanh, một chiếc xe màu đỏ và một chiếc xe màu xanh lá cây:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Những chiếc xe này đều là cùng loại (ô tô) để có một cách để nghĩ về điều này là những chiếc xe của chúng tôi là một số loại dữ liệu (nói một int) nhưng họ có giá trị khác nhau ( blue, red, green, đó có thể là một màu enum)


Nhập con trỏ

Bây giờ nếu tôi đưa bạn vào bãi đậu xe này và yêu cầu bạn tìm cho tôi một chiếc xe màu xanh, bạn mở rộng một ngón tay và sử dụng nó để chỉ vào một chiếc xe màu xanh ở điểm 1. Điều này giống như lấy một con trỏ và gán nó vào một địa chỉ bộ nhớ ( int *finger = parking_lot)

Ngón tay của bạn (con trỏ) không phải là câu trả lời cho câu hỏi của tôi. Nhìn vào ngón tay của bạn không cho tôi biết gì, nhưng nếu tôi nhìn vào nơi ngón tay của bạn đang chỉ vào (hội thảo con trỏ), tôi có thể tìm thấy chiếc xe (dữ liệu) mà tôi đang tìm kiếm.


Tái chỉ định con trỏ

Bây giờ tôi có thể yêu cầu bạn tìm một chiếc xe màu đỏ thay thế và bạn có thể chuyển hướng ngón tay của bạn đến một chiếc xe mới. Bây giờ con trỏ của bạn (giống như trước đây) đang hiển thị cho tôi dữ liệu mới (vị trí đỗ xe nơi có thể tìm thấy chiếc xe màu đỏ) cùng loại (chiếc xe).

Con trỏ không thay đổi về mặt vật lý, nó vẫn là ngón tay của bạn , chỉ là dữ liệu mà nó hiển thị cho tôi đã thay đổi. (địa chỉ "chỗ đậu xe")


Con trỏ kép (hoặc con trỏ tới con trỏ)

Điều này làm việc với nhiều hơn một con trỏ là tốt. Tôi có thể hỏi con trỏ ở đâu, đang chỉ vào chiếc xe màu đỏ và bạn có thể sử dụng bàn tay khác của mình và chỉ bằng một ngón tay đến ngón tay đầu tiên. (nó giống như int **finger_two = &finger)

Bây giờ nếu tôi muốn biết chiếc xe màu xanh đang ở đâu, tôi có thể đi theo hướng ngón tay thứ nhất đến ngón tay thứ hai, đến chiếc xe (dữ liệu).


Con trỏ lơ lửng

Bây giờ, hãy nói rằng bạn đang cảm thấy rất giống một bức tượng và bạn muốn nắm tay chỉ vào chiếc xe màu đỏ vô thời hạn. Nếu chiếc xe màu đỏ đó lái đi thì sao?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

Con trỏ của bạn vẫn đang chỉ vào nơi chiếc xe màu đỏ đã ở nhưng không còn nữa. Giả sử một chiếc xe mới kéo vào đó ... một chiếc xe màu cam. Bây giờ nếu tôi hỏi bạn một lần nữa, "chiếc xe màu đỏ ở đâu", bạn vẫn chỉ vào đó, nhưng bây giờ bạn đã sai. Đó không phải là một chiếc xe màu đỏ, đó là màu cam.


Số học con trỏ

Ok, vì vậy bạn vẫn chỉ vào điểm đỗ xe thứ hai (hiện đang bị chiếm giữ bởi chiếc xe màu cam)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Bây giờ tôi có một câu hỏi mới ... Tôi muốn biết màu sắc của chiếc xe ở vị trí đỗ xe tiếp theo . Bạn có thể thấy bạn đang chỉ vào vị trí 2, vì vậy bạn chỉ cần thêm 1 và bạn đang chỉ vào vị trí tiếp theo. ( finger+1), vì tôi muốn biết dữ liệu ở đó là gì, bạn phải kiểm tra điểm đó (không chỉ ngón tay) để bạn có thể trì hoãn con trỏ ( *(finger+1)) để xem có một chiếc xe màu xanh lá cây ở đó (dữ liệu tại vị trí đó )


Chỉ không sử dụng từ "con trỏ kép". Con trỏ có thể trỏ đến bất cứ thứ gì, vì vậy rõ ràng bạn có thể có con trỏ trỏ đến các con trỏ khác. Chúng không phải là con trỏ kép.
gnasher729

Tôi nghĩ rằng điều này bỏ lỡ quan điểm rằng các ngón tay của chính họ, để tiếp tục sự tương tự của bạn, mỗi người chiếm một vị trí đỗ xe. Tôi không chắc chắn rằng mọi người có bất kỳ khó khăn nào trong việc hiểu con trỏ ở mức độ trừu tượng cao của sự tương tự của bạn, nó hiểu rằng con trỏ là những thứ có thể thay đổi chiếm vị trí bộ nhớ và điều này hữu ích, dường như trốn tránh mọi người.
Emmet

1
@Emmet - Tôi không đồng ý rằng có rất nhiều người có thể đi vào con trỏ WRT, nhưng tôi đọc câu hỏi: "without getting them bogged down in the overall concept"như một sự hiểu biết ở cấp độ cao. Và theo quan điểm của bạn: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- bạn sẽ rất ngạc nhiên khi có nhiều người không hiểu con trỏ thậm chí đến mức này
Mike

Có công đức nào trong việc mở rộng sự tương tự ngón tay ô tô với một người (với một hoặc nhiều ngón tay - và một bất thường di truyền có thể cho phép mỗi người chỉ vào bất kỳ hướng nào!) Ngồi trong một trong những chiếc xe chỉ vào một chiếc xe khác (hoặc cúi xuống chỉ vào khu đất hoang bên cạnh lô đất như một "con trỏ chưa được xác định" hoặc toàn bộ bàn tay chỉ ra một hàng không gian dưới dạng một "con trỏ kích thước cố định [5]" hoặc cuộn tròn vào lòng bàn tay "con trỏ null" chỉ ra một nơi nào đó mà người ta biết rằng KHÔNG BAO GIỜ có xe hơi) ... 8-)
SlySven

lời giải thích của bạn rất đáng trân trọng và tốt cho người mới bắt đầu.
Yatendra Rathore

9

Tôi không nghĩ rằng con trỏ là một khái niệm đặc biệt khó khăn - hầu hết các mô hình tinh thần của học sinh ánh xạ tới thứ gì đó như thế này và một số bản phác thảo hộp nhanh có thể giúp ích.

Khó khăn, ít nhất là những gì tôi đã trải qua trong quá khứ và thấy người khác giải quyết, là việc quản lý các con trỏ trong C / C ++ có thể bị rối loạn không đáng có.


9

Một ví dụ về một hướng dẫn với một bộ sơ đồ tốt giúp ích rất nhiều cho sự hiểu biết về con trỏ .

Joel Spolsky đưa ra một số điểm tốt về việc hiểu con trỏ trong bài viết Hướng dẫn phỏng vấn về du kích của mình :

Vì một số lý do, hầu hết mọi người dường như được sinh ra mà không có phần não bộ hiểu được con trỏ. Đây là một điều năng khiếu, không phải là một kỹ năng - nó đòi hỏi một hình thức phức tạp của suy nghĩ gấp đôi mà một số người không thể làm được.


8

Vấn đề với con trỏ không phải là khái niệm. Đó là sự thực thi và ngôn ngữ liên quan. Kết quả nhầm lẫn thêm khi giáo viên cho rằng đó là Ý TƯỞNG của con trỏ rất khó, và không phải là biệt ngữ, hay sự lộn xộn hỗn độn C và C ++ tạo nên khái niệm này. Vì vậy, rất nhiều nỗ lực được giải thích về khái niệm này (như trong câu trả lời được chấp nhận cho câu hỏi này) và thật lãng phí cho một người như tôi, vì tôi đã hiểu tất cả về điều đó. Nó chỉ giải thích phần sai của vấn đề.

Để cho bạn biết tôi đến từ đâu, tôi là người hiểu rất rõ về con trỏ và tôi có thể sử dụng chúng thành thạo ngôn ngữ trình biên dịch. Bởi vì trong ngôn ngữ hợp ngữ, chúng không được gọi là con trỏ. Chúng được gọi là địa chỉ. Khi nói đến lập trình và sử dụng con trỏ trong C, tôi đã phạm rất nhiều lỗi và thực sự bối rối. Tôi vẫn chưa sắp xếp cái này ra. Tôi sẽ cho bạn một ví dụ.

Khi một api nói:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

nó muốn gì

nó có thể muốn:

một số đại diện cho một địa chỉ cho bộ đệm

(Để cho nó, tôi nói doIt(mybuffer), hay doIt(*myBuffer)?)

một số đại diện cho địa chỉ đến một địa chỉ cho bộ đệm

(đó là doIt(&mybuffer)hay doIt(mybuffer)hay doIt(*mybuffer)?)

một số đại diện cho địa chỉ đến địa chỉ cho địa chỉ vào bộ đệm

(có lẽ đó là doIt(&mybuffer). hoặc là nó doIt(&&mybuffer)? hoặc thậm chí doIt(&&&mybuffer))

và v.v., và ngôn ngữ liên quan không làm cho nó rõ ràng vì nó liên quan đến các từ "con trỏ" và "tham chiếu" không mang nhiều ý nghĩa và sự rõ ràng đối với tôi như "x giữ địa chỉ cho y" và " chức năng này yêu cầu một địa chỉ để y ". Ngoài ra, câu trả lời phụ thuộc vào việc "mybuffer" bắt đầu với cái quái gì, và ý định làm gì với nó. Ngôn ngữ không hỗ trợ các cấp độ lồng nhau gặp phải trong thực tế. Giống như khi tôi phải đưa một "con trỏ" vào một hàm tạo bộ đệm mới và nó sửa đổi con trỏ để trỏ đến vị trí mới của bộ đệm. Liệu nó thực sự muốn con trỏ, hoặc một con trỏ đến con trỏ, vì vậy nó biết nơi để sửa đổi nội dung của con trỏ. Hầu hết thời gian tôi chỉ cần đoán ý nghĩa của "

"Con trỏ" quá tải. Là một con trỏ một địa chỉ cho một giá trị? hoặc nó là một biến giữ một địa chỉ cho một giá trị. Khi một hàm muốn một con trỏ, nó muốn địa chỉ mà biến con trỏ giữ hay nó muốn địa chỉ cho biến con trỏ? Tôi bối rối.


Tôi đã thấy nó được giải thích như thế này: nếu bạn thấy một khai báo con trỏ như thế double *(*(*fn)(int))(char), thì kết quả đánh giá *(*(*fn)(42))('x')sẽ là a double. Bạn có thể loại bỏ các lớp đánh giá để hiểu các loại trung gian phải là gì.
Bernd Jendrissek

@BerndJendrissek Không chắc tôi theo dõi. Kết quả của việc đánh giá (*(*fn)(42))('x') là gì?
Breton

bạn nhận được một thứ (hãy gọi nó x), nếu bạn đánh giá *x, bạn sẽ nhận được gấp đôi.
Bernd Jendrissek

@BerndJendrissek Đây có phải là để giải thích một cái gì đó về con trỏ? Tôi không hiểu Ý bạn là sao? Tôi đã loại bỏ một lớp và không nhận được thông tin mới về bất kỳ loại trung gian nào. Nó giải thích gì về những gì một chức năng cụ thể sẽ chấp nhận? Nó phải làm gì với bất cứ điều gì?
Breton

Có lẽ thông điệp trong lời giải thích này (và nó không phải là của tôi, tôi ước gì có thể tìm thấy nơi đầu tiên tôi nhìn thấy nó) là để nghĩ về nó ít về những gì fn đang và nhiều hơn nữa về những gì bạn có thể làm vớifn
Bernd Jendrissek

8

Tôi nghĩ rào cản chính để hiểu con trỏ là giáo viên tồi.

Hầu như tất cả mọi người đều được dạy dối trá về con trỏ: Rằng chúng không có gì khác ngoài địa chỉ bộ nhớ hoặc chúng cho phép bạn trỏ đến các vị trí tùy ý .

Và tất nhiên là chúng khó hiểu, nguy hiểm và bán ma thuật.

Không ai trong số đó là sự thật. Con trỏ thực sự là những khái niệm khá đơn giản, miễn là bạn tuân theo những gì ngôn ngữ C ++ nói về chúng và không thấm nhuần chúng với các thuộc tính "thường" hóa ra hoạt động trong thực tế, nhưng vẫn không được ngôn ngữ đảm bảo và vì vậy không phải là một phần của khái niệm thực tế về một con trỏ.

Tôi đã cố gắng viết lên một lời giải thích về điều này một vài tháng trước trong bài đăng trên blog này - hy vọng nó sẽ giúp được ai đó.

(Lưu ý, trước khi bất kỳ ai hiểu ý tôi, vâng, tiêu chuẩn C ++ nói rằng con trỏ đại diện cho địa chỉ bộ nhớ. địa chỉ ". Sự khác biệt là quan trọng)


Rốt cuộc, một con trỏ null không trỏ đến địa chỉ 0 trong bộ nhớ, mặc dù đó là "giá trị" C bằng không. Đó là một khái niệm hoàn toàn riêng biệt và nếu bạn xử lý sai, bạn có thể sẽ giải quyết (và hội nghị) một cái gì đó mà bạn không mong đợi. Trong một số trường hợp, đó thậm chí có thể là địa chỉ 0 trong bộ nhớ (đặc biệt là không gian địa chỉ thường bằng phẳng), nhưng trong những trường hợp khác, nó có thể bị bỏ qua dưới dạng hành vi không xác định bởi trình biên dịch tối ưu hóa hoặc truy cập vào một phần khác của bộ nhớ có liên quan với "zero" cho loại con trỏ đã cho. Nảy sinh vui nhộn.
Luaan

Không cần thiết. Bạn cần có khả năng mô hình hóa máy tính trong đầu để con trỏ có ý nghĩa (và để gỡ lỗi các chương trình khác). Không phải ai cũng làm được điều này.
Thorbjørn Ravn Andersen

5

Tôi nghĩ rằng điều làm cho con trỏ trở nên khó học là cho đến khi con trỏ bạn cảm thấy thoải mái với ý tưởng rằng "tại vị trí bộ nhớ này là một tập hợp các bit đại diện cho một int, một nhân đôi, một ký tự, bất cứ điều gì".

Khi bạn lần đầu tiên nhìn thấy một con trỏ, bạn không thực sự có được những gì ở vị trí bộ nhớ đó. "Ý bạn là gì, nó giữ một địa chỉ ?"

Tôi không đồng ý với quan điểm rằng "bạn có được chúng hay không".

Chúng trở nên dễ hiểu hơn khi bạn bắt đầu tìm kiếm sử dụng thực sự cho chúng (như không chuyển các cấu trúc lớn vào các chức năng).


5

Lý do rất khó hiểu không phải vì đó là một khái niệm khó mà vì cú pháp không nhất quán .

   int *mypointer;

Trước tiên, bạn được biết rằng phần ngoài cùng bên trái của một biến tạo xác định loại biến. Khai báo con trỏ không hoạt động như thế này trong C và C ++. Thay vào đó họ nói rằng biến đang chỉ vào loại bên trái. Trong trường hợp này: *mypulum đang trỏ vào một int.

Tôi đã không hoàn toàn nắm bắt được con trỏ cho đến khi tôi thử sử dụng chúng trong C # (không an toàn), chúng hoạt động theo cùng một cách chính xác nhưng với cú pháp hợp lý và nhất quán. Con trỏ là một loại chính nó. Ở đây mypulum một con trỏ tới một int.

  int* mypointer;

Đừng để tôi bắt đầu với các con trỏ hàm ...


2
Trên thực tế, cả hai mảnh vỡ của bạn đều hợp lệ C. Đây là vấn đề của rất nhiều năm theo kiểu C mà cái đầu tiên là phổ biến hơn. Thứ hai là khá phổ biến hơn một chút trong C ++, ví dụ.
RBerteig

1
Đoạn thứ hai không thực sự hoạt động tốt với các khai báo phức tạp hơn. Và cú pháp không phải là "không nhất quán" khi bạn nhận ra rằng phần bên phải của khai báo con trỏ hiển thị cho bạn những gì bạn phải làm với con trỏ để có được một cái gì đó có kiểu xác định kiểu nguyên tử ở bên trái.
Bernd Jendrissek

2
int *p;có nghĩa đơn giản: *plà một số nguyên. int *p, **ppcó nghĩa là: *p**pplà số nguyên.
Miles Rout

@MilesRout: Nhưng đó chính xác là vấn đề. *p**ppkhông nguyên, bởi vì bạn không bao giờ được khởi tạo phoặc pphoặc *ppđể trỏ đến bất cứ điều gì. Tôi hiểu lý do tại sao một số người thích gắn bó với ngữ pháp trong phần này, đặc biệt vì một số trường hợp cạnh và trường hợp phức tạp đòi hỏi bạn phải làm như vậy (mặc dù vậy, bạn vẫn có thể làm việc một cách tầm thường trong mọi trường hợp tôi biết) ... nhưng tôi không nghĩ những trường hợp đó quan trọng hơn thực tế là việc dạy đúng hướng là sai lệch với người mới. Chưa kể loại xấu xí! :)
Các cuộc đua nhẹ nhàng trong quỹ đạo

@LightnessRacesinOrbit Dạy liên kết đúng không phải là sai lệch. Đó là cách duy nhất để dạy nó. KHÔNG dạy nó là sai lệch.
Miles Rout

5

Tôi có thể làm việc với con trỏ khi tôi chỉ biết C ++. Tôi biết phải làm gì trong một số trường hợp và không nên làm gì từ bản dùng thử / lỗi. Nhưng điều khiến tôi hiểu hoàn toàn là ngôn ngữ lắp ráp. Nếu bạn thực hiện một số sửa lỗi cấp độ nghiêm túc với chương trình hợp ngữ bạn đã viết, bạn sẽ có thể hiểu được rất nhiều điều.


4

Tôi thích sự tương tự địa chỉ nhà, nhưng tôi luôn nghĩ rằng địa chỉ được gửi đến hộp thư. Bằng cách này, bạn có thể hình dung khái niệm hủy bỏ con trỏ (mở hộp thư).

Ví dụ: theo danh sách được liên kết: 1) bắt đầu bằng giấy của bạn với địa chỉ 2) Chuyển đến địa chỉ trên giấy 3) Mở hộp thư để tìm một mảnh giấy mới có địa chỉ tiếp theo trên đó

Trong danh sách liên kết tuyến tính, hộp thư cuối cùng không có gì trong đó (cuối danh sách). Trong danh sách liên kết tròn, hộp thư cuối cùng có địa chỉ của hộp thư đầu tiên trong đó.

Lưu ý rằng bước 3 là nơi xảy ra tình trạng thiếu thẩm quyền và là nơi bạn sẽ gặp sự cố hoặc sai khi địa chỉ không hợp lệ. Giả sử bạn có thể đi đến hộp thư của một địa chỉ không hợp lệ, hãy tưởng tượng rằng có một lỗ đen hoặc thứ gì đó trong đó biến thế giới bên trong :)


Một sự phức tạp khó chịu với sự tương tự số hộp thư là trong khi ngôn ngữ do Dennis Ritchie phát minh định nghĩa hành vi theo các địa chỉ của byte và các giá trị được lưu trữ trong các byte đó, thì ngôn ngữ được xác định bởi Tiêu chuẩn C mời gọi việc tối ưu hóa "tối ưu hóa" để sử dụng một hành vi mô hình phức tạp hơn nhưng xác định các khía cạnh khác nhau của mô hình theo những cách mơ hồ, mâu thuẫn và không đầy đủ.
supercat

3

Tôi nghĩ rằng lý do chính khiến mọi người gặp rắc rối với nó là vì nó thường không được dạy theo cách thú vị và hấp dẫn. Tôi muốn thấy một giảng viên nhận được 10 tình nguyện viên từ đám đông và đưa cho họ một thước kẻ mỗi mét, khiến họ đứng xung quanh trong một cấu hình nhất định và sử dụng thước kẻ để chỉ vào nhau. Sau đó hiển thị số học con trỏ bằng cách di chuyển mọi người xung quanh (và nơi họ chỉ ra người cai trị của họ). Đó là một cách đơn giản nhưng hiệu quả (và trên hết là đáng nhớ) để hiển thị các khái niệm mà không bị sa lầy vào cơ học.

Một khi bạn đã đến C và C ++, dường như khó khăn hơn đối với một số người. Tôi không chắc liệu điều này có phải là vì cuối cùng họ đã đưa ra lý thuyết rằng họ không nắm bắt chính xác vào thực tế hay vì thao tác con trỏ vốn đã khó hơn trong các ngôn ngữ đó. Tôi không thể nhớ rõ quá trình chuyển đổi của mình, nhưng tôi biết con trỏ trong Pascal và sau đó chuyển sang C và bị mất hoàn toàn.


2

Tôi không nghĩ rằng chính con trỏ là khó hiểu. Hầu hết mọi người có thể hiểu khái niệm. Bây giờ bạn có thể nghĩ về bao nhiêu con trỏ hoặc bao nhiêu mức độ gián tiếp mà bạn cảm thấy thoải mái. Nó không mất quá nhiều để đưa mọi người ra rìa. Việc chúng có thể được thay đổi một cách vô tình bởi các lỗi trong chương trình của bạn cũng có thể khiến chúng rất khó gỡ lỗi khi có sự cố trong mã của bạn.


2

Tôi nghĩ rằng nó thực sự có thể là một vấn đề cú pháp. Cú pháp C / C ++ cho con trỏ có vẻ không nhất quán và phức tạp hơn mức cần thiết.

Trớ trêu thay, điều thực sự giúp tôi hiểu được con trỏ là bắt gặp khái niệm về một trình vòng lặp trong Thư viện mẫu chuẩn c ++ . Thật là mỉa mai vì tôi chỉ có thể giả định rằng các trình vòng lặp được hình thành như một sự khái quát hóa của con trỏ.

Đôi khi bạn không thể nhìn thấy rừng cho đến khi bạn học cách bỏ qua những cái cây.


Vấn đề chủ yếu nằm ở cú pháp khai báo C. Nhưng việc sử dụng con trỏ chắc chắn sẽ dễ dàng hơn nếu (*p)(p->), và do đó chúng tôi sẽ p->->xthay vì mơ hồ*p->x
MSalters

@MSalters Trời ơi, bạn đang nói đùa phải không? Không có sự mâu thuẫn ở đó. a->bđơn giản có nghĩa là (*a).b.
Miles Rout

@Miles: Trên thực tế, và bởi logic mà * p->xphương tiện * ((*a).b)trong khi *p -> xphương tiện (*(*p)) -> x. Trộn các toán tử tiền tố và hậu tố gây ra phân tích cú pháp mơ hồ.
MSalters

@MSalters không, vì khoảng trắng là không liên quan. Điều đó giống như nói rằng 1+2 * 3phải là 9.
Miles Rout

2

Sự nhầm lẫn đến từ nhiều lớp trừu tượng trộn lẫn với nhau trong khái niệm "con trỏ". Các lập trình viên không bị nhầm lẫn bởi các tham chiếu thông thường trong Java / Python, nhưng các con trỏ khác nhau ở chỗ chúng phơi bày các đặc điểm của kiến ​​trúc bộ nhớ cơ bản.

Đó là một nguyên tắc tốt để tách sạch các lớp trừu tượng và con trỏ không làm điều đó.


1
Điều thú vị là, con trỏ C thực sự không phơi bày bất kỳ sự lôi cuốn nào của kiến ​​trúc bộ nhớ cơ bản. Sự khác biệt duy nhất giữa các tham chiếu Java và các con trỏ C là bạn có thể có các kiểu phức tạp liên quan đến các con trỏ (ví dụ: int *** hoặc char * ( ) (void * )), có số học con trỏ cho các mảng và con trỏ tới các thành viên cấu trúc, sự hiện diện của void *, và tính đối ngẫu của mảng / con trỏ. Ngoài ra, họ làm việc như nhau.
jpalecek

Điểm tốt. Đó là số học con trỏ và khả năng tràn bộ đệm - thoát ra khỏi sự trừu tượng bằng cách thoát ra khỏi vùng nhớ hiện có liên quan - điều đó thực hiện.
Joshua Fox

@jpalecek: Thật dễ hiểu khi con trỏ hoạt động trên các triển khai chứng minh hành vi của chúng theo kiến ​​trúc cơ bản. Nói foo[i]có nghĩa là đi đến một điểm nhất định, tiến về phía trước một khoảng cách nhất định và nhìn thấy những gì ở đó. Điều làm phức tạp mọi thứ là lớp trừu tượng thêm phức tạp hơn nhiều được Standard bổ sung hoàn toàn vì lợi ích của trình biên dịch, nhưng mô hình hóa mọi thứ theo cách phù hợp với nhu cầu lập trình viên và nhu cầu trình biên dịch cũng vậy.
supercat

2

Cách tôi muốn giải thích là về mảng và chỉ mục - mọi người có thể không quen thuộc với con trỏ, nhưng họ thường biết chỉ số là gì.

Vì vậy, tôi nói hãy tưởng tượng rằng RAM là một mảng (và bạn chỉ có 10 byte RAM):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Sau đó, một con trỏ tới một biến thực sự chỉ là chỉ số của (byte đầu tiên của) biến đó trong RAM.

Vì vậy, nếu bạn có một con trỏ / chỉ mục unsigned char index = 2, thì giá trị rõ ràng là phần tử thứ ba hoặc số 4. Một con trỏ tới một con trỏ là nơi bạn lấy số đó và sử dụng nó làm chỉ mục, như thế RAM[RAM[index]].

Tôi sẽ vẽ một mảng trên một danh sách giấy, và chỉ sử dụng nó để hiển thị những thứ như nhiều con trỏ trỏ đến cùng một bộ nhớ, số học con trỏ, con trỏ đến con trỏ, v.v.


1

Số hộp thư bưu điện.

Đó là một phần thông tin cho phép bạn truy cập vào một cái gì đó khác.

. mặt khác - nếu bưu điện chuyển tiếp thư, thì bạn có một con trỏ tới một con trỏ.)


1

Không phải là một cách tồi để nắm bắt nó, thông qua các trình vòng lặp .. nhưng hãy tiếp tục tìm kiếm, bạn sẽ thấy Alexandrescu bắt đầu phàn nàn về chúng.

Nhiều nhà phát triển C ++ cũ (không bao giờ hiểu rằng các trình vòng lặp là một con trỏ hiện đại trước khi bỏ ngôn ngữ) nhảy sang C # và vẫn tin rằng họ có các trình lặp tốt.

Hmm, vấn đề là tất cả các trình lặp đều ở tỷ lệ cược hoàn toàn ở mức mà các nền tảng thời gian chạy (Java / CLR) đang cố gắng đạt được: sử dụng mới, đơn giản, mọi người đều sử dụng. Điều này có thể tốt, nhưng họ đã nói điều đó một lần trong cuốn sách màu tím và họ đã nói điều đó ngay cả trước và trước C:

Vô cảm.

Một khái niệm rất mạnh mẽ nhưng không bao giờ như vậy nếu bạn làm theo mọi cách .. Các trình vòng lặp rất hữu ích vì chúng giúp trừu tượng hóa các thuật toán, một ví dụ khác. Và thời gian biên dịch là nơi dành cho một thuật toán, rất đơn giản. Bạn biết mã + dữ liệu hoặc trong ngôn ngữ khác C #:

IEnumerable + LINQ + Massive Framework = 300MB thời gian xử phạt hình phạt cho sự tệ hại, kéo các ứng dụng qua hàng đống phiên bản của các loại tham chiếu ..

"Con trỏ Le là rẻ."


4
Điều này có liên quan gì?
Neil Williams

... Bạn đang cố gắng nói gì, ngoài "liên kết tĩnh là điều tốt nhất từng có" và "Tôi không hiểu bất cứ điều gì khác biệt so với những gì tôi đã học trước đây hoạt động"?
Luaan

Luaan, bạn không thể biết người ta có thể học được gì bằng cách phân tách JIT năm 2000 phải không? Rằng nó kết thúc trong một bảng nhảy, từ một bảng con trỏ, như được hiển thị năm 2000 trực tuyến trong ASM, vì vậy không hiểu bất cứ điều gì khác biệt có thể có ý nghĩa khác: đọc cẩn thận là một kỹ năng thiết yếu, hãy thử lại.
rama-jka toti

1

Một số câu trả lời ở trên đã khẳng định rằng "con trỏ không thực sự khó", nhưng vẫn không tiếp tục giải quyết trực tiếp nơi "con trỏ khó!" đến từ. Vài năm trước, tôi dạy kèm cho sinh viên CS năm đầu tiên (chỉ một năm, vì tôi rõ ràng rất thích nó) và rõ ràng với tôi rằng ý tưởng về con trỏ không khó. Điều khó hiểu là tại sao và khi nào bạn muốn có một con trỏ .

Tôi không nghĩ bạn có thể ly dị câu hỏi đó - tại sao và khi nào nên sử dụng một con trỏ - từ việc giải thích các vấn đề kỹ thuật phần mềm rộng lớn hơn. Tại sao mọi biến không phải là biến toàn cục và tại sao người ta phải đưa mã tương tự vào các hàm (điều đó, lấy cái này, sử dụng con trỏ để chuyên môn hóa hành vi của chúng đến trang web cuộc gọi của chúng).


0

Tôi không thấy những gì rất khó hiểu về con trỏ. Họ trỏ đến một vị trí trong bộ nhớ, đó là lưu trữ địa chỉ bộ nhớ. Trong C / C ++, bạn có thể chỉ định loại con trỏ trỏ tới. Ví dụ:

int* my_int_pointer;

Nói rằng my_int_pulum chứa địa chỉ đến một vị trí chứa int.

Vấn đề với con trỏ là chúng trỏ đến một vị trí trong bộ nhớ, do đó rất dễ đi vào một số vị trí mà bạn không nên ở. Bằng chứng là nhìn vào nhiều lỗ hổng bảo mật trong các ứng dụng C / C ++ từ tràn bộ đệm (tăng con trỏ vượt qua ranh giới được phân bổ).


0

Chỉ cần nhầm lẫn nhiều thứ hơn một chút, đôi khi bạn phải làm việc với tay cầm thay vì con trỏ. Tay cầm là con trỏ tới con trỏ, để mặt sau có thể di chuyển mọi thứ trong bộ nhớ để chống phân mảnh. Nếu con trỏ thay đổi trong thói quen giữa, kết quả không thể đoán trước, vì vậy trước tiên bạn phải khóa tay cầm để đảm bảo không có gì đi đâu cả.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 nói về nó một chút mạch lạc hơn tôi. :-)


-1: Xử lý không phải là con trỏ đến con trỏ; chúng không phải là con trỏ theo bất kỳ ý nghĩa nào. Đừng nhầm lẫn chúng.
Ian Goldby

"Chúng không phải là con trỏ theo bất kỳ ý nghĩa nào" - ừm, tôi xin khác.
SarekOfVulcan

Một con trỏ là một vị trí bộ nhớ. Một tay cầm là bất kỳ định danh duy nhất. Nó có thể là một con trỏ, nhưng nó cũng có thể là một chỉ mục trong một mảng hoặc bất cứ thứ gì khác cho vấn đề đó. Liên kết bạn đưa ra chỉ là một trường hợp đặc biệt trong đó tay cầm là một con trỏ, nhưng nó không phải như vậy. Xem thêm parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Liên kết đó không hỗ trợ cho khẳng định của bạn rằng chúng không phải là con trỏ theo bất kỳ ý nghĩa nào - "Ví dụ: tay cầm có thể là Fred **, nơi con trỏ Fred * nhọn ..." Tôi không nghĩ -1 là công bằng.
SarekOfVulcan

0

Mọi người mới bắt đầu C / C ++ đều có cùng một vấn đề và vấn đề đó xảy ra không phải vì "con trỏ khó học" mà là "ai và cách giải thích". Một số người học thu thập nó bằng lời nói một cách trực quan và cách tốt nhất để giải thích nó là sử dụng ví dụ "đào tạo" (phù hợp với ví dụ bằng lời nói và hình ảnh).

Trong đó "đầu máy" là một con trỏ không thể chứa bất cứ thứ gì và "toa xe" là thứ "đầu máy" cố gắng kéo (hoặc trỏ đến). Sau đó, bạn có thể tự phân loại "toa xe", nó có thể giữ động vật, thực vật hoặc con người (hoặc kết hợp chúng).

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.