Mọi người thấy khó khăn gì về con trỏ C? [đóng cửa]


173

Từ số lượng câu hỏi được đăng ở đây, rõ ràng mọi người có một số vấn đề cơ bản khi tìm hiểu về con trỏ và số học con trỏ.

Tôi tò mò muốn biết tại sao. Họ chưa bao giờ thực sự gây ra cho tôi những vấn đề lớn (mặc dù lần đầu tiên tôi biết về họ trong thời kỳ đồ đá mới). Để viết câu trả lời tốt hơn cho những câu hỏi này, tôi muốn biết những gì mọi người cảm thấy khó khăn.

Vì vậy, nếu bạn đang vật lộn với con trỏ, hoặc gần đây bạn đột nhiên "hiểu", các khía cạnh của con trỏ gây ra vấn đề cho bạn là gì?


54
Snarky, nói quá, tranh cãi không cần thiết, trả lời với một sự thật: Họ đã bị què quặt --- bị đánh lừa về mặt tinh thần, tôi nói với 'ya --- bằng cách nói một trong số đó là ngôn ngữ cấp cao "biểu cảm" trước tiên. Họ nên bắt đầu bằng cách lập trình trên kim loại trần như God a Daniel Boone dự định!
dmckee --- ex-moderator mèo con

3
... và Lập trình viên sẽ tốt hơn vì nó sẽ phát triển thành một cuộc thảo luận mặc dù bạn đã nỗ lực hết sức.
dmckee --- ex-moderator mèo con

Đây là tranh luận và chủ quan, nó tạo ra kết quả N, khó khăn của tôi là dễ dàng của bạn. Nó có thể sẽ hoạt động trên các lập trình viên, nhưng chúng tôi chưa di chuyển những câu hỏi này vì trang web chưa hết beta.
Sam Saffron

2
@Sam Saffron: Mặc dù tôi thường đồng ý rằng đây là câu hỏi dành cho lập trình viên. Đây thực sự sẽ không tệ nếu mọi người sẵn sàng đánh dấu "Tôi nghĩ nó dễ dàng" và "Tôi ghét nhìn thấy con trỏ" là thư rác họ đang.
jkerian

3
Ai đó phải đưa ra điều này: "Nó giống như một ngón tay chỉ ra mặt trăng. Đừng tập trung vào ngón tay nếu không bạn sẽ bỏ lỡ tất cả vinh quang trên trời" --Bruce Lee
mu quá ngắn

Câu trả lời:


86

Tôi nghi ngờ mọi người đang đi quá sâu vào câu trả lời của họ. Một sự hiểu biết về lập lịch, hoạt động CPU thực tế hoặc quản lý bộ nhớ cấp lắp ráp không thực sự cần thiết.

Khi tôi đang giảng dạy, tôi thấy những lỗ hổng sau đây trong sự hiểu biết của sinh viên là nguồn gốc phổ biến nhất của các vấn đề:

  1. Heap vs Stack lưu trữ. Nó chỉ đơn giản là tuyệt đẹp khi nhiều người không hiểu điều này, ngay cả trong một ý nghĩa chung.
  2. Khung xếp chồng. Chỉ là khái niệm chung về một phần dành riêng của ngăn xếp cho các biến cục bộ, cùng với lý do đó là một 'ngăn xếp' ... các chi tiết như lấy vị trí trả về, chi tiết xử lý ngoại lệ và các thanh ghi trước đó có thể được giữ an toàn cho đến khi ai đó cố gắng xây dựng một trình biên dịch.
  3. "Bộ nhớ là bộ nhớ là bộ nhớ" Truyền chỉ thay đổi phiên bản nào của toán tử hoặc dung lượng mà trình biên dịch cung cấp cho một đoạn bộ nhớ cụ thể. Bạn biết bạn đang giải quyết vấn đề này khi mọi người nói về "biến X (nguyên thủy) thực sự là gì".

Hầu hết các sinh viên của tôi đã có thể hiểu một bản vẽ đơn giản của một đoạn bộ nhớ, thường là phần biến cục bộ của ngăn xếp ở phạm vi hiện tại. Nói chung cung cấp địa chỉ hư cấu rõ ràng cho các địa điểm khác nhau đã giúp.

Tôi đoán tóm lại, tôi đang nói rằng nếu bạn muốn hiểu con trỏ, bạn phải hiểu các biến và những gì chúng thực sự có trong các kiến ​​trúc hiện đại.


13
IMHO, hiểu stack và heap là không cần thiết như chi tiết CPU cấp thấp. Chồng và đống xảy ra là chi tiết thực hiện. Thông số ISO C không có một đề cập nào về từ "stack" và K & R cũng không.
sigjuice

4
@sigjuice: Sự phản đối của bạn bỏ lỡ điểm của cả câu hỏi và câu trả lời. A) K & R C là lỗi thời B) ISO C không phải là ngôn ngữ duy nhất có con trỏ, điểm 1 và 2 của tôi được phát triển dựa trên ngôn ngữ không dựa trên C C) 95% kiến ​​trúc (không phải ngôn ngữ) sử dụng heap / stack system, nó đủ phổ biến để các trường hợp ngoại lệ được giải thích liên quan đến nó. D) Điểm của câu hỏi là "tại sao mọi người không hiểu con trỏ", chứ không phải "làm thế nào để tôi giải thích ISO C"
jkerian

9
@ John Marchetti: Thậm chí nhiều hơn ... cho rằng câu hỏi là "Vấn đề gốc rễ với vấn đề con người với con trỏ" là gì, tôi không nghĩ những người hỏi các câu hỏi liên quan đến con trỏ sẽ cực kỳ ấn tượng với "Bạn không ' t thực sự cần biết "như một câu trả lời. Rõ ràng là họ không đồng ý. :)
jkerian

3
@jkerian Nó có thể đã lỗi thời, nhưng ba hoặc bốn trang K & R giải thích các con trỏ làm như vậy mà không cần chi tiết triển khai. Một kiến ​​thức về chi tiết triển khai rất hữu ích vì nhiều lý do, nhưng IMHO, nó không phải là điều kiện tiên quyết để hiểu các cấu trúc chính của ngôn ngữ.
sigjuice

3
"Nói chung việc cung cấp địa chỉ hư cấu rõ ràng cho các địa điểm khác nhau đã giúp." -> +1
dòng chảy

146

Khi tôi mới bắt đầu làm việc với họ, vấn đề lớn nhất tôi gặp phải là cú pháp.

int* ip;
int * ip;
int *ip;

đều giống nhau.

nhưng:

int* ip1, ip2;  //second one isn't a pointer!
int *ip1, *ip2;

Tại sao? bởi vì phần "con trỏ" của khai báo thuộc về biến chứ không phải kiểu.

Và sau đó, hội thảo sẽ sử dụng một ký hiệu rất giống nhau:

*ip = 4;  //sets the value of the thing pointed to by ip to '4'
x = ip;   //hey, that's not '4'!
x = *ip;  //ahh... there's that '4'

Ngoại trừ khi bạn thực sự cần lấy một con trỏ ... thì bạn sử dụng ký hiệu và!

int *ip = &x;

Hoan hô cho sự nhất quán!

Sau đó, rõ ràng chỉ là những kẻ ngốc và chứng minh rằng chúng thông minh đến mức nào, rất nhiều nhà phát triển thư viện sử dụng con trỏ tới con trỏ và nếu họ mong đợi một loạt những điều đó, thì tại sao không chuyển một con trỏ đến đó .

void foo(****ipppArr);

để gọi cái này, tôi cần địa chỉ của mảng con trỏ tới con trỏ tới con trỏ của ints:

foo(&(***ipppArr));

Trong sáu tháng, khi tôi phải duy trì mã này, tôi sẽ dành nhiều thời gian hơn để cố gắng tìm ra ý nghĩa của tất cả những điều này hơn là viết lại từ đầu. (vâng, có lẽ đã sai cú pháp đó - đã được một thời gian kể từ khi tôi làm bất cứ điều gì trong C. Tôi hơi nhớ nó, nhưng sau đó tôi là một người theo chủ nghĩa đại chúng)


21
Nhận xét của bạn về cái đầu tiên, >> * ip = 4; // đặt giá trị của ip thành '4' << là sai. Nó sẽ là >> // đặt giá trị của vật được trỏ bởi ip thành '4'
aaaa bbbb

8
Xếp chồng quá nhiều loại lên nhau là một ý tưởng tồi, trong bất kỳ ngôn ngữ nào. Bạn có thể tìm thấy viết "foo (& (*** ipppArr));" lạ trong C, nhưng viết một cái gì đó như "std :: map <std :: cặp <int, int>, std :: cặp <std :: vector <int>, std :: tuple <int, double, std :: list <int >>>> "trong C ++ cũng rất phức tạp. Điều này không có nghĩa là các con trỏ trong các thùng chứa C hoặc STL trong C ++ rất phức tạp. Nó chỉ có nghĩa là bạn phải sử dụng các định nghĩa loại tốt hơn để làm cho người đọc mã của bạn dễ hiểu hơn.
Patrick

20
Tôi thực sự không thể tin rằng một sự hiểu lầm về cú pháp là câu trả lời được bình chọn nhiều nhất. Đó là phần dễ nhất về con trỏ.
jason

4
Ngay cả khi đọc câu trả lời này, tôi đã cố gắng lấy một tờ giấy và vẽ bức tranh. Trong C, tôi luôn luôn vẽ tranh.
Michael Easter

19
@Jason Biện pháp khách quan nào là khó khăn ngoài những gì mà phần lớn mọi người cảm thấy khó khăn?
Rupert Madden-Abbott

52

Hiểu đúng về con trỏ đòi hỏi kiến ​​thức về kiến ​​trúc của máy bên dưới.

Nhiều lập trình viên ngày nay không biết máy của họ hoạt động như thế nào, giống như hầu hết những người biết lái xe không biết gì về động cơ.


18
@dmckee: Chà, tôi sai à? Có bao nhiêu lập trình viên Java có thể đối phó với một segfault?
Robert Harvey

5
Là segfaults một cái gì đó để làm với sự thay đổi thanh? - Một lập trình viên Java
Tom Anderson

6
@Robert: nó có nghĩa là một bổ sung chính hãng. Đây là một chủ đề khó khăn để thảo luận mà không làm tổn thương cảm xúc của mọi người. Và tôi e rằng bình luận của tôi đã ngăn chặn chính cuộc xung đột mà tôi nghĩ bạn đã tránh được. Mea cupla.
dmckee --- ex-moderator mèo con

30
Tôi không đồng ý; bạn không cần phải hiểu kiến ​​trúc cơ bản để có được con trỏ (dù sao chúng cũng là một sự trừu tượng).
jason

11
@Jason: Trong C, một con trỏ thực chất là một địa chỉ bộ nhớ. Làm việc với họ một cách an toàn là không thể nếu không hiểu kiến ​​trúc máy. Xem en.wikipedia.org/wiki/Pulum_(computing)chánzo.org/pointers/#def định
Robert Harvey

42

Khi tiếp xúc với con trỏ, những người bị lẫn lộn đang ở một trong hai trại. Tôi đã (am?) Trong cả hai.

Các array[] đám đông

Đây là đám đông ngay thẳng không biết cách dịch từ ký hiệu con trỏ sang ký hiệu mảng (hoặc thậm chí không biết rằng chúng thậm chí có liên quan với nhau). Dưới đây là bốn cách để truy cập các phần tử của một mảng:

  1. ký hiệu mảng (lập chỉ mục) với tên mảng
  2. ký hiệu mảng (lập chỉ mục) với tên con trỏ
  3. ký hiệu con trỏ (*) với tên con trỏ
  4. ký hiệu con trỏ (*) với tên mảng

 

int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;

array       element            pointer
notation    number     vals    notation

vals[0]     0          10      *(ptr + 0)
ptr[0]                         *(vals + 0)

vals[1]     1          20      *(ptr + 1)
ptr[1]                         *(vals + 1)

vals[2]     2          30      *(ptr + 2)
ptr[2]                         *(vals + 2)

vals[3]     3          40      *(ptr + 3)
ptr[3]                         *(vals + 3)

vals[4]     4          50      *(ptr + 4)
ptr[4]                         *(vals + 4)

Ý tưởng ở đây là việc truy cập các mảng thông qua con trỏ dường như khá đơn giản và dễ hiểu, nhưng rất nhiều điều rất phức tạp và thông minh có thể được thực hiện theo cách này. Một số trong đó có thể khiến các lập trình viên C / C ++ có kinh nghiệm hoang mang, huống hồ là những người mới chưa có kinh nghiệm.

Các reference to a pointerpointer to a pointerđám đông

Điều này là một bài viết tuyệt vời giải thích sự khác biệt và tôi sẽ trích dẫn và đánh cắp một số mã từ :)

Một ví dụ nhỏ, có thể rất khó để thấy chính xác những gì tác giả muốn làm nếu bạn bắt gặp một cái gì đó như thế này:

//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(pvar);
  ....
  return 0;
}

Hoặc, ở mức độ thấp hơn, một cái gì đó như thế này:

//function prototype
void func(int** ppInt);

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(&pvar);
  ....
  return 0;
}

Vì vậy, vào cuối ngày, chúng ta thực sự giải quyết được gì với tất cả những điều vô nghĩa này? Không có gì.

Bây giờ chúng ta đã thấy cú pháp của ptr-to-ptr và ref-to-ptr. Có bất kỳ lợi thế của cái này hơn cái kia không? Tôi sợ, không. Việc sử dụng một trong cả hai, đối với một số lập trình viên chỉ là sở thích cá nhân. Một số người sử dụng ref-to-ptr nói rằng cú pháp là "sạch hơn" trong khi một số người sử dụng ptr-to-ptr, nói cú pháp ptr-to-ptr làm cho nó rõ ràng hơn với những người đang đọc những gì bạn đang làm.

Sự phức tạp này và sự thay thế dường như (có vẻ táo bạo) với các tài liệu tham khảo, thường là một cảnh báo khác về con trỏ và một lỗi của người mới, làm cho việc hiểu con trỏ trở nên khó khăn. Điều quan trọng là phải hiểu, vì mục đích hoàn thành, việc chỉ ra các tham chiếu là bất hợp pháp trong C và C ++ vì những lý do khó hiểu đưa bạn vào lvalue- rvaluengữ nghĩa.

Như một câu trả lời trước đây đã nhận xét, nhiều lần bạn sẽ có những lập trình viên nóng bỏng này nghĩ rằng họ thông minh bằng cách sử dụng ******awesome_var->lol_im_so_clever()và hầu hết chúng ta có thể có lỗi khi viết những hành động tàn bạo đó, nhưng nó không phải là mã tốt, và chắc chắn nó không thể duy trì được .

Chà, câu trả lời này hóa ra dài hơn tôi mong đợi ...


5
Tôi nghĩ rằng bạn có thể đã đưa ra câu trả lời C ++ cho câu hỏi C ở đây ... ít nhất là phần thứ hai.
gièm pha

Con trỏ tới con trỏ cũng áp dụng cho C: p
David Titarenco

1
Hả? Tôi chỉ thấy con trỏ tới con trỏ khi đi qua các mảng - ví dụ thứ hai của bạn không thực sự áp dụng cho hầu hết mã C đàng hoàng. Ngoài ra, bạn đang kéo C vào mớ hỗn độn của C ++ - các tham chiếu trong C không tồn tại.
new123456

Bạn phải đối phó với con trỏ đến con trỏ trong nhiều trường hợp hợp pháp. Ví dụ, khi làm việc với một con trỏ hàm trỏ đến một hàm trả về một con trỏ. Một ví dụ khác: một cấu trúc có thể chứa một số lượng khác nhau của các cấu trúc khác. Và nhiều thứ khác nữa...
David Titarenco

2
"con trỏ đến tài liệu tham khảo là bất hợp pháp trong C" - giống như "vắng mặt" :)
Kos

29

Tôi đổ lỗi cho chất lượng tài liệu tham khảo và những người thực hiện giảng dạy, cá nhân; hầu hết các khái niệm trong C (nhưng đặc biệt là con trỏ) chỉ đơn giản là được dạy rất tệ. Tôi tiếp tục đe dọa sẽ viết cuốn sách C của riêng mình (tựa đề Điều cuối cùng là nhu cầu thế giới là một cuốn sách khác về ngôn ngữ lập trình C ), nhưng tôi không có thời gian hay sự kiên nhẫn để làm điều đó. Vì vậy, tôi đi chơi ở đây và ném các trích dẫn ngẫu nhiên từ Tiêu chuẩn vào mọi người.

Cũng có một thực tế là khi C được thiết kế ban đầu, người ta cho rằng bạn hiểu kiến ​​trúc máy ở mức khá chi tiết chỉ vì không có cách nào để tránh nó trong công việc hàng ngày của bạn (bộ nhớ quá chật và bộ xử lý quá chậm bạn phải hiểu những gì bạn đã viết hiệu suất ảnh hưởng).


3
Đúng. 'int foo = 5; int * pfoo = & foo; Xem nó hữu ích như thế nào? OK, di chuyển dọc ... 'Tôi đã không thực sự sử dụng con trỏ cho đến khi tôi viết thư viện danh sách liên kết đôi của riêng mình.
John Lopez

2
+1. Tôi sử dụng để dạy kèm cho sinh viên CS100 và rất nhiều vấn đề của họ đã được giải quyết chỉ bằng cách vượt qua con trỏ một cách dễ hiểu.
benzado

1
+1 cho bối cảnh lịch sử. Đã bắt đầu lâu sau thời gian đó, điều này chưa bao giờ xảy ra với tôi.
Lumi

26

Có một bài viết tuyệt vời ủng hộ quan điểm cho rằng con trỏ rất khó trên trang web của Joel Spolsky - The Perils of JavaSchools .

[Tuyên bố miễn trừ trách nhiệm - Tôi không phải là người ghét Java mỗi lần .]


2
@Jason - điều đó đúng nhưng không phủ nhận lập luận.
Steve Townsend

4
Spolsky không nói rằng JavaSchools là lý do khiến mọi người thấy khó khăn. Anh ta nói rằng họ dẫn đến những người mù chữ với trình độ Khoa học Máy tính.
benzado

1
@benzado - điểm công bằng - bài viết ngắn gọn của tôi sẽ được cải thiện nếu nó đọc 'một bài viết tuyệt vời ủng hộ quan điểm cho rằng con trỏ rất khó'. Điều mà bài báo ngụ ý là "có bằng CS từ" trường học tốt "" không phải là một công cụ dự đoán thành công như một nhà phát triển như trước đây, trong khi "hiểu con trỏ" (và đệ quy) vẫn còn.
Steve Townsend

1
@Steve Townsend: Tôi nghĩ bạn đang thiếu quan điểm tranh luận của ông Spolsky.
jason

2
@Steve Townsend: Ông Spolsky đang lập luận rằng các trường Java đang nuôi dưỡng một thế hệ lập trình viên không biết con trỏ và đệ quy, không phải là con trỏ khó vì sự phổ biến của các trường Java. Như bạn đã nói "có một bài viết tuyệt vời về lý do tại sao điều này khó" và liên kết với bài viết đã nói, có vẻ như bạn có cách giải thích sau. Hãy tha thứ cho tôi nếu tôi sai.
jason

24

Hầu hết mọi thứ đều khó hiểu hơn nếu bạn không có kiến ​​thức "bên dưới". Khi tôi dạy CS, mọi thứ trở nên dễ dàng hơn rất nhiều khi tôi bắt đầu học sinh lập trình một "cỗ máy" rất đơn giản, một máy tính thập phân mô phỏng với các mã thập phân có bộ nhớ bao gồm các thanh ghi thập phân và địa chỉ thập phân. Họ sẽ đưa vào các chương trình rất ngắn, ví dụ, thêm một dãy số để có tổng số. Sau đó, họ sẽ bước một bước để xem những gì đang xảy ra. Họ có thể giữ phím "enter" và xem nó chạy "nhanh".

Tôi chắc chắn rằng hầu hết mọi người trên SO tự hỏi tại sao nó lại hữu ích để có được cơ bản như vậy. Chúng tôi quên những gì nó giống như không biết làm thế nào để lập trình. Chơi với một máy tính đồ chơi như vậy đặt các khái niệm mà không có chương trình mà bạn không thể lập trình, chẳng hạn như các ý tưởng tính toán là một quá trình từng bước, sử dụng một số lượng nhỏ các nguyên thủy cơ bản để xây dựng chương trình và khái niệm về bộ nhớ các biến là nơi lưu trữ số, trong đó địa chỉ hoặc tên của biến khác với số mà nó chứa. Có một sự khác biệt giữa thời gian bạn tham gia chương trình và thời gian chương trình "chạy". Tôi thích học lập trình như vượt qua một loạt các "cú va chạm tốc độ", chẳng hạn như các chương trình rất đơn giản, sau đó là các vòng lặp và chương trình con, sau đó là mảng, sau đó là I / O tuần tự, sau đó là con trỏ và cấu trúc dữ liệu.

Cuối cùng, khi đến C, con trỏ rất khó hiểu mặc dù K & R đã làm rất tốt khi giải thích chúng. Cách tôi học chúng ở C là biết cách đọc chúng - từ phải sang trái. Giống như khi tôi nhìn thấy int *ptrong đầu, tôi nói " pchỉ vào một int". C được phát minh như một bước tiến lên từ ngôn ngữ lắp ráp và đó là điều tôi thích về nó - nó gần với "mặt đất" đó. Con trỏ, giống như bất cứ điều gì khác, khó hiểu hơn nếu bạn không có căn cứ đó.


1
Một cách tốt để tìm hiểu điều này là lập trình vi điều khiển 8 bit. Họ rất dễ hiểu. Lấy bộ điều khiển Atmel AVR; họ thậm chí còn được hỗ trợ bởi gcc.
Xenu

@Xothy: Tôi đồng ý. Đối với tôi, đó là Intel 8008 và 8051 :-)
Mike Dunlavey

Đối với tôi, đó là một máy tính 8 bit tùy chỉnh ("Có thể") tại MIT trong thời gian mờ nhạt.
QuantumMechanic

Mike - bạn nên cho học sinh của mình một
CARDIAC

1
@Quantum: CARDIAC- tốt, chưa từng nghe về điều đó. "Có lẽ" - để tôi đoán, có phải là khi Sussman (et al) có người đọc cuốn sách Mead-Conway và lấy chip LSI của riêng họ? Đó là một chút sau thời gian của tôi ở đó.
Mike Dunlavey

17

Tôi đã không nhận được con trỏ cho đến khi tôi đọc mô tả trong K & R. Cho đến thời điểm đó, con trỏ không có ý nghĩa. Tôi đã đọc cả đống thứ mà mọi người nói "Đừng học con trỏ, chúng khó hiểu và sẽ làm tổn thương đầu bạn và gây phình động mạch", vì vậy tôi đã tránh xa nó trong một thời gian dài và tạo ra không khí khó hiểu về khái niệm khó khăn này .

Mặt khác, chủ yếu là những gì tôi nghĩ là, tại sao bạn muốn có một biến mà bạn phải trải qua các vòng để có được giá trị và nếu bạn muốn gán công cụ cho nó, bạn phải làm những điều kỳ lạ để có được giá trị vào chúng Toàn bộ quan điểm của một biến là thứ gì đó để lưu trữ một giá trị, tôi nghĩ vậy, tại sao một người nào đó muốn làm cho nó phức tạp lại nằm ngoài tôi. "Vì vậy, với một con trỏ, bạn phải sử dụng *toán tử để lấy giá trị của nó ??? Loại biến đó là gì?" , Tôi đã nghĩ. Vô nghĩa, không có ý định chơi chữ.

Lý do nó phức tạp là vì tôi không hiểu rằng một con trỏ là một địa chỉ cho một cái gì đó. Nếu bạn giải thích rằng đó là một địa chỉ, thì đó là một địa chỉ chứa địa chỉ này đến một địa chỉ khác và bạn có thể thao túng địa chỉ đó để làm những việc hữu ích, tôi nghĩ rằng nó có thể làm sáng tỏ sự nhầm lẫn.

Một lớp yêu cầu sử dụng các con trỏ để truy cập / sửa đổi các cổng trên PC, sử dụng số học con trỏ để giải quyết các vị trí bộ nhớ khác nhau và xem mã C phức tạp hơn đã sửa đổi các đối số của chúng không cho tôi biết rằng con trỏ là tốt, vô nghĩa.


4
Nếu bạn có tài nguyên hạn chế để làm việc với (RAM, ROM, CPU), chẳng hạn như trong các ứng dụng nhúng, con trỏ nhanh chóng có ý nghĩa hơn nhiều.
Nick T

+1 cho nhận xét của Nick - đặc biệt là thông qua các cấu trúc.
new123456

12

Đây là một ví dụ về con trỏ / mảng khiến tôi tạm dừng. Giả sử bạn có hai mảng:

uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];

Và mục tiêu của bạn là sao chép nội dung uint8_t từ đích nguồn bằng cách sử dụng memcpy (). Đoán xem điều nào sau đây hoàn thành mục tiêu đó:

memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));

Câu trả lời (Spoiler Alert!) Là TẤT CẢ chúng. "đích", "& đích" và "& đích [0]" đều có cùng giá trị. "& đích" là một loại khác với hai loại kia, nhưng nó vẫn có cùng giá trị. Điều tương tự cũng xảy ra đối với hoán vị của "nguồn".

Như một bên, cá nhân tôi thích phiên bản đầu tiên.


Tôi thích phiên bản đầu tiên là tốt (ít dấu chấm câu).
sigjuice

++ Tôi cũng vậy, nhưng bạn thực sự cần phải cẩn thận sizeof(source), bởi vì nếu sourcelà một con trỏ, sizeofnó sẽ không phải là điều bạn muốn. Tôi đôi khi (không phải luôn luôn) viết sizeof(source[0]) * number_of_elements_of_sourcechỉ để tránh xa lỗi đó.
Mike Dunlavey

đích, & đích và & đích [0] hoàn toàn không giống nhau - nhưng mỗi điểm sẽ thông qua một cơ chế khác nhau sẽ được chuyển đổi sang cùng một khoảng trống * khi được sử dụng trong memcpy. Tuy nhiên, khi được sử dụng làm đối số của sizeof, bạn sẽ nhận được hai kết quả khác nhau và ba kết quả khác nhau là có thể.
gnasher729

Tôi nghĩ địa chỉ của nhà điều hành là cần thiết?
MarcusJ

7

Tôi nên bắt đầu bằng cách nói rằng C và C ++ là ngôn ngữ lập trình đầu tiên tôi học được. Tôi bắt đầu với C, sau đó học C ++ ở trường rất nhiều, và sau đó quay lại C để thành thạo nó.

Điều đầu tiên làm tôi bối rối về con trỏ khi học C là đơn giản:

char ch;
char str[100];
scanf("%c %s", &ch, str);

Sự nhầm lẫn này chủ yếu bắt nguồn từ việc đã được giới thiệu sử dụng tham chiếu đến một biến cho các đối số OUT trước khi các con trỏ được giới thiệu đúng với tôi. Tôi nhớ rằng tôi bỏ qua văn bản cho vài ví dụ đầu tiên trong C cho Dummies vì họ quá đơn giản chỉ để không bao giờ có được chương trình đầu tiên tôi đã ghi vào làm việc (rất có thể vì điều này).

Điều khó hiểu về điều này là những gì &chthực sự có ý nghĩa cũng như tại saostr không cần nó.

Sau khi tôi trở nên quen thuộc với điều đó, tôi nhớ rằng mình đang bối rối về phân bổ động. Tại một số điểm, tôi nhận ra rằng việc có các con trỏ tới dữ liệu là cực kỳ hữu ích nếu không có sự phân bổ động của một số loại, vì vậy tôi đã viết một cái gì đó như:

char * x = NULL;
if (y) {
     char z[100];
     x = z;
}

để cố gắng phân bổ động một số không gian. Nó không hoạt động. Tôi không chắc rằng nó sẽ hoạt động, nhưng tôi không biết nó có thể hoạt động như thế nào.

Sau đó tôi đã biết về mallocnew, nhưng chúng thực sự giống như những máy tạo trí nhớ kỳ diệu đối với tôi. Tôi không biết gì về cách họ có thể làm việc.

Một thời gian sau, tôi được dạy đệ quy một lần nữa (trước đây tôi đã tự học nó, nhưng bây giờ đang ở trong lớp học) và tôi đã hỏi nó hoạt động như thế nào dưới mui xe - nơi các biến riêng biệt được lưu trữ. Giáo sư của tôi đã nói "trên chồng" và rất nhiều điều trở nên rõ ràng với tôi. Tôi đã nghe thuật ngữ này trước đây và đã thực hiện các ngăn xếp phần mềm trước đó. Tôi đã nghe người khác nhắc đến "chồng" từ lâu, nhưng đã quên nó.

Trong khoảng thời gian này tôi cũng nhận ra rằng việc sử dụng các mảng đa chiều trong C có thể rất khó hiểu. Tôi biết làm thế nào họ làm việc, nhưng họ rất dễ bị rối khi tôi quyết định cố gắng sử dụng chúng bất cứ khi nào tôi có thể. Tôi nghĩ rằng vấn đề ở đây chủ yếu là cú pháp (đặc biệt là chuyển đến hoặc trả lại chúng từ các hàm).

Vì tôi đã viết C ++ cho trường học trong một hoặc hai năm tới, tôi đã có nhiều kinh nghiệm sử dụng con trỏ cho cấu trúc dữ liệu. Ở đây tôi đã có một loạt rắc rối mới - trộn lẫn con trỏ. Tôi sẽ có nhiều cấp độ con trỏ (những thứ như node ***ptr;) làm tôi vấp ngã. Tôi đã bỏ qua một con trỏ số lần sai và cuối cùng tìm ra số lượng *tôi cần bằng cách dùng thử và lỗi.

Tại một số thời điểm, tôi đã học được cách một đống chương trình hoạt động (loại, nhưng đủ tốt để nó không còn khiến tôi thức đêm nữa). Tôi nhớ đọc rằng nếu bạn xem một vài byte trước khi con trỏ malloctrên một hệ thống nhất định trả về, bạn có thể thấy lượng dữ liệu thực sự được phân bổ. Tôi nhận ra rằng mã trong malloccó thể yêu cầu thêm bộ nhớ từ HĐH và bộ nhớ này không phải là một phần của các tệp thực thi của tôi. Có một ý tưởng làm việc tốtmalloc việc là một thực sự hữu ích.

Ngay sau đó tôi đã tham gia một lớp lắp ráp, nó không dạy tôi nhiều về con trỏ như hầu hết các lập trình viên có thể nghĩ. Nó đã khiến tôi suy nghĩ nhiều hơn về việc tập hợp mã của tôi có thể được dịch sang. Tôi đã luôn cố gắng viết mã hiệu quả, nhưng bây giờ tôi đã có một ý tưởng tốt hơn làm thế nào.

Tôi cũng đã tham gia một vài lớp học mà tôi phải viết một số lisp . Khi viết lisp tôi không quan tâm đến hiệu quả như khi tôi ở C. Tôi có rất ít ý tưởng mã này có thể được dịch thành gì nếu được biên dịch, nhưng tôi biết rằng nó có vẻ giống như sử dụng nhiều ký hiệu (biến) được đặt tên cục bộ mọi thứ dễ dàng hơn nhiều Tại một số thời điểm tôi đã viết một số mã xoay cây AVL trong một chút ít, rằng tôi đã có một thời gian rất khó để viết bằng C ++ vì các vấn đề về con trỏ. Tôi nhận ra rằng sự ác cảm của tôi đối với những gì tôi nghĩ là các biến cục bộ dư thừa đã cản trở khả năng của tôi để viết điều đó và một số chương trình khác trong C ++.

Tôi cũng đã tham gia một lớp trình biên dịch. Mặc dù trong lớp này, tôi đã tìm hiểu về tài liệu nâng cao và tìm hiểu về phép gán đơn tĩnh (SSA) và các biến chết, điều đó không quan trọng ngoại trừ việc nó dạy tôi rằng bất kỳ trình biên dịch tử tế nào cũng sẽ làm tốt việc xử lý các biến đó là Không còn được sử dụng. Tôi đã biết rằng nhiều biến số hơn (bao gồm cả con trỏ) với các loại chính xác và tên tốt sẽ giúp tôi giữ mọi thứ trong đầu, nhưng bây giờ tôi cũng biết rằng tránh chúng vì lý do hiệu quả thậm chí còn ngu ngốc hơn các giáo sư có tư duy tối ưu hóa vi mô của tôi đã nói tôi.

Vì vậy, đối với tôi, biết một chút về cách bố trí bộ nhớ của một chương trình đã giúp ích rất nhiều. Suy nghĩ về ý nghĩa của mã của tôi, cả về mặt tượng trưng và phần cứng, giúp tôi hiểu. Sử dụng con trỏ cục bộ có loại chính xác sẽ giúp rất nhiều. Tôi thường viết mã trông giống như:

int foo(struct frog * f, int x, int y) {
    struct leg * g = f->left_leg;
    struct toe * t = g->big_toe;
    process(t);

Vì vậy, nếu tôi làm hỏng một loại con trỏ, nó rất rõ ràng bởi lỗi trình biên dịch, vấn đề là gì. Nếu tôi đã làm:

int foo(struct frog * f, int x, int y) {
    process(f->left_leg->big_toe);

và có bất kỳ loại con trỏ sai trong đó, lỗi trình biên dịch sẽ khó khăn hơn rất nhiều để tìm ra. Tôi sẽ bị cám dỗ để dùng thử và thay đổi lỗi trong sự thất vọng của tôi, và có lẽ làm cho mọi thứ tồi tệ hơn.


1
+1. Triệt để và sâu sắc. Tôi đã quên về scanf, nhưng bây giờ khi bạn đưa nó lên, tôi nhớ có cùng một sự nhầm lẫn.
Joe White

6

Nhìn lại, có bốn điều thực sự giúp tôi cuối cùng hiểu được con trỏ. Trước đó, tôi có thể sử dụng chúng, nhưng tôi không hiểu hết về chúng. Đó là, tôi biết nếu tôi làm theo các biểu mẫu, tôi sẽ nhận được kết quả mà tôi mong muốn, nhưng tôi không hiểu đầy đủ về 'tại sao' của các biểu mẫu. Tôi nhận ra rằng đây không phải là chính xác những gì bạn đã yêu cầu, nhưng tôi nghĩ đó là một hệ quả hữu ích.

  1. Viết một thói quen lấy một con trỏ đến một số nguyên và sửa đổi số nguyên. Điều này đã cho tôi các hình thức cần thiết để xây dựng bất kỳ mô hình tinh thần nào về cách con trỏ hoạt động.

  2. Cấp phát bộ nhớ động một chiều. Tìm hiểu phân bổ bộ nhớ 1-D khiến tôi hiểu khái niệm về con trỏ.

  3. Phân bổ bộ nhớ động hai chiều. Tìm hiểu phân bổ bộ nhớ 2 chiều củng cố khái niệm đó, nhưng cũng dạy tôi rằng chính con trỏ yêu cầu lưu trữ và phải được tính đến.

  4. Sự khác nhau giữa các biến stack, biến toàn cục và bộ nhớ heap. Tìm ra những khác biệt này đã dạy tôi các loại bộ nhớ mà con trỏ trỏ tới / tham chiếu.

Mỗi trong số các mục này yêu cầu tưởng tượng những gì đang diễn ra ở cấp độ thấp hơn - xây dựng một mô hình tinh thần thỏa mãn mọi trường hợp tôi có thể nghĩ đến khi ném vào nó. Phải mất thời gian và công sức, nhưng nó cũng xứng đáng. Tôi tin rằng để hiểu được con trỏ, bạn phải xây dựng mô hình tinh thần đó về cách chúng hoạt động và cách chúng được thực hiện.

Bây giờ trở lại câu hỏi ban đầu của bạn. Dựa trên danh sách trước đó, có một số mục mà tôi gặp khó khăn trong việc nắm bắt ban đầu.

  1. Làm thế nào và tại sao một người sẽ sử dụng một con trỏ.
  2. Làm thế nào chúng khác nhau và tương tự như mảng.
  3. Hiểu nơi thông tin con trỏ được lưu trữ.
  4. Hiểu những gì và nơi nó là con trỏ đang chỉ vào.

Này, bạn có thể chỉ cho tôi một bài viết / cuốn sách / bản vẽ / vẽ nguệch ngoạc / bất cứ điều gì tôi có thể học theo cách tương tự mà bạn đã mô tả trong câu trả lời của mình không? Tôi tin chắc rằng đây là con đường để đi khi học tốt, về cơ bản là bất cứ điều gì. Hiểu biết sâu sắc và mô hình tinh thần tốt
Alexander Starbuck

1
@AlexStarbuck - Tôi không có ý nói điều này nghe có vẻ thiếu sót, nhưng phương pháp khoa học là một công cụ tuyệt vời. Vẽ cho mình một bức tranh về những gì bạn nghĩ có thể xảy ra cho một kịch bản cụ thể. Lập trình một cái gì đó để kiểm tra nó, và phân tích những gì bạn có. Nó có phù hợp với những gì bạn mong đợi? Nếu không, xác định nơi khác nhau? Lặp lại khi cần thiết, tăng dần độ phức tạp để kiểm tra cả sự hiểu biết và mô hình tinh thần của bạn.
Sparky

6

Tôi đã có "thời điểm con trỏ" của mình làm việc trên một số chương trình điện thoại ở C. Tôi đã phải viết một trình giả lập trao đổi AXE10 bằng cách sử dụng một bộ phân tích giao thức chỉ hiểu cổ điển C. Mọi thứ đều xoay quanh khi biết con trỏ. Tôi đã thử viết mã của mình mà không có chúng (hey, tôi là "con trỏ trước" đã cắt cho tôi một chút chùng) và thất bại hoàn toàn.

Chìa khóa để hiểu chúng, đối với tôi, là toán tử & (địa chỉ). Khi tôi hiểu điều đó &icó nghĩa là "địa chỉ của tôi" thì hiểu điều đó *icó nghĩa là "nội dung của địa chỉ được chỉ bởi tôi" đã đến một chút sau đó. Bất cứ khi nào tôi viết hoặc đọc mã của mình, tôi luôn lặp lại "&" nghĩa là gì và "*" nghĩa là gì và cuối cùng tôi đã đến để sử dụng chúng theo trực giác.

Thật xấu hổ, tôi đã bị ép buộc vào VB và sau đó là Java nên kiến ​​thức về con trỏ của tôi không còn sắc nét như trước đây, nhưng tôi vui vì tôi là "con trỏ bài". Mặc dù vậy, đừng yêu cầu tôi sử dụng thư viện yêu cầu tôi hiểu * * p.


Nếu &ilà địa chỉ và *ilà nội dung, là igì?
Thomas Ahle

2
Tôi sắp xếp quá tải việc sử dụng i. Đối với một biến tùy ý i, & i có nghĩa là "địa chỉ của" i, i theo nghĩa riêng của nó có nghĩa là "nội dung của & i" và * i có nghĩa là "coi nội dung của & i là địa chỉ, đi đến địa chỉ đó và gửi lại địa chỉ đó nội dung ".
Gary Rowe

5

Khó khăn chính với con trỏ, ít nhất là với tôi, là tôi đã không bắt đầu với C. Tôi bắt đầu với Java. Toàn bộ khái niệm về con trỏ thực sự xa lạ cho đến khi một vài lớp học ở trường đại học mà tôi dự kiến ​​sẽ biết C. Vì vậy, sau đó tôi đã tự dạy mình những điều cơ bản về C và cách sử dụng con trỏ theo nghĩa rất cơ bản của chúng. Thậm chí sau đó, mỗi lần tôi thấy mình đọc mã C, tôi phải tra cứu cú pháp con trỏ.

Vì vậy, trong kinh nghiệm rất hạn chế của tôi (thế giới thực 1 năm + 4 ở đại học), con trỏ làm tôi bối rối vì tôi chưa bao giờ phải thực sự sử dụng nó trong bất cứ điều gì khác ngoài môi trường lớp học. Và tôi có thể thông cảm với các sinh viên bắt đầu CS với JAVA thay vì C hoặc C ++. Như bạn đã nói, bạn đã học được con trỏ trong thời đại 'Đồ đá mới' và có lẽ đã sử dụng nó kể từ đó. Đối với chúng tôi những người mới hơn, khái niệm phân bổ bộ nhớ và thực hiện số học con trỏ thực sự xa lạ bởi vì tất cả các ngôn ngữ này đã trừu tượng hóa điều đó.

Tái bút: Sau khi đọc bài tiểu luận về Spolsky, mô tả của anh ấy về 'JavaSchools' không giống với những gì tôi đã trải qua ở trường đại học tại Cornell ('05 -'09). Tôi đã lấy các cấu trúc và lập trình chức năng (sml), hệ điều hành (C), thuật toán (bút và giấy) và một loạt các lớp khác không được dạy trong java. Tuy nhiên, tất cả các lớp giới thiệu và các môn tự chọn đều được thực hiện trong java vì có giá trị trong việc không phát minh lại bánh xe khi bạn đang cố gắng làm một cái gì đó có cấp độ cao hơn so với thực hiện hàm băm bằng con trỏ.


4
Thành thật mà nói, bạn vẫn gặp khó khăn với con trỏ, tôi không chắc rằng trải nghiệm của bạn tại Cornell hoàn toàn mâu thuẫn với bài viết của Joel. Rõ ràng là đủ bộ não của bạn được kết nối trong một tư duy Java để đưa ra quan điểm của mình.
jkerian

5
Wat? Các tham chiếu trong Java (hoặc C # hoặc Python hoặc hàng tá ngôn ngữ khác) chỉ là các con trỏ mà không có số học. Hiểu con trỏ có nghĩa là hiểu lý do tại sao void foo(Clazz obj) { obj = new Clazz(); }không có op trong khi làm thay void bar(Clazz obj) { obj.quux = new Quux(); }đổi đối số ...

1
Tôi biết các tài liệu tham khảo trong Java là gì, nhưng tôi chỉ nói rằng nếu bạn yêu cầu tôi thực hiện phản chiếu trong Java hoặc viết bất cứ điều gì có ý nghĩa trong CI thì không thể bỏ qua nó. Nó đòi hỏi rất nhiều nghiên cứu, như lần đầu tiên học nó.
shoebox639

1
Làm thế nào mà bạn có được thông qua một lớp hệ điều hành trong C mà không thông thạo C? Không có ý định xúc phạm, chỉ là tôi nhớ phải phát triển một hệ điều hành đơn giản từ đầu. Tôi đã phải sử dụng con trỏ một ngàn lần ...
Gravity

5

Đây là câu trả lời không: Sử dụng cdecl (hoặc c ++ Dec) để tìm ra nó:

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

4

Họ thêm một thứ nguyên bổ sung cho mã mà không có sự thay đổi đáng kể về cú pháp. Nghĩ về điều này:

int a;
a = 5

Chỉ có một điều cần thay đổi : a. Bạn có thể viết a = 6và kết quả là rõ ràng với hầu hết mọi người. Nhưng bây giờ hãy xem xét:

int *a;
a = &some_int;

Có hai điều aliên quan đến các thời điểm khác nhau: giá trị thực của a, con trỏ và giá trị "đằng sau" con trỏ. Bạn có thể thay đổi a:

a = &some_other_int;

... Và some_intvẫn còn ở đâu đó với cùng một giá trị. Nhưng bạn cũng có thể thay đổi điều mà nó trỏ đến:

*a = 6;

Có một khoảng cách về khái niệm giữa a = 6, chỉ có tác dụng phụ cục bộ, và *a = 6, có thể ảnh hưởng đến một loạt những thứ khác ở những nơi khác. Quan điểm của tôi ở đây không phải là khái niệm về sự gián tiếp vốn đã khó khăn, mà bởi vì bạn có thể làm cả việc tức thời, cục bộ với ahoặc điều gián tiếp với *a... đó có thể là điều khiến mọi người nhầm lẫn.


4

Tôi đã lập trình trong c ++ được 2 năm rồi chuyển sang Java (5 năm) và không bao giờ nhìn lại. Tuy nhiên, khi gần đây tôi phải sử dụng một số công cụ bản địa, tôi phát hiện ra (với sự kinh ngạc) rằng tôi đã không quên bất cứ điều gì về con trỏ và tôi thậm chí còn thấy chúng dễ sử dụng. Đây là một sự tương phản rõ nét với những gì tôi trải nghiệm 7 năm trước khi tôi lần đầu tiên cố gắng nắm bắt khái niệm này. Vì vậy, tôi đoán sự hiểu biết và thích là một vấn đề của sự trưởng thành lập trình? :)

HOẶC LÀ

Con trỏ giống như đi xe đạp, một khi bạn tìm ra cách làm việc với chúng, sẽ không quên điều đó.

Nói chung, khó nắm bắt hay không, toàn bộ ý tưởng con trỏ là RẤT giáo dục và tôi tin rằng nó nên được mọi lập trình viên hiểu cho dù anh ta có lập trình trên một ngôn ngữ có con trỏ hay không.


3

Con trỏ là khó khăn vì sự thiếu quyết đoán.


"Người ta nói rằng không có vấn đề gì trong khoa học máy tính mà không thể giải quyết được bằng một mức độ gián tiếp nữa" (dù không biết ai đã nói trước)
Archetypal Paul

Nó giống như phép thuật, trong đó sự định hướng sai lầm là điều khiến mọi người bối rối (nhưng hoàn toàn tuyệt vời)
Nick T

3

Con trỏ (cùng với một số khía cạnh khác của công việc cấp thấp), yêu cầu người dùng lấy đi phép thuật.

Hầu hết các lập trình viên cấp cao thích phép thuật.


3

Con trỏ là một cách để đối phó với sự khác biệt giữa một tay cầm với một đối tượng và chính đối tượng đó. (ok, không nhất thiết là các đối tượng, nhưng bạn biết ý tôi là gì, cũng như tâm trí của tôi ở đâu)

Tại một số điểm, bạn có thể phải đối phó với sự khác biệt giữa hai. Trong ngôn ngữ cấp cao, hiện đại, điều này trở thành sự phân biệt giữa sao chép theo giá trị và sao chép theo tham chiếu. Dù bằng cách nào, đó là một khái niệm thường rất khó để các lập trình viên nắm bắt.

Tuy nhiên, như đã được chỉ ra, cú pháp xử lý vấn đề này trong C là xấu, không nhất quán và khó hiểu. Cuối cùng, nếu bạn thực sự cố gắng hiểu nó, một con trỏ sẽ có ý nghĩa. Nhưng khi bạn bắt đầu xử lý các con trỏ tới các con trỏ, và cứ như vậy, vì nó gây khó hiểu cho tôi cũng như cho những người khác.

Một điều quan trọng khác cần nhớ về con trỏ là chúng nguy hiểm. C là ngôn ngữ của một lập trình viên bậc thầy. Nó giả định rằng bạn biết những gì bạn đang làm và do đó cung cấp cho bạn sức mạnh để thực sự làm mọi thứ rối tung lên. Mặc dù một số loại chương trình vẫn cần phải được viết bằng C, nhưng hầu hết các chương trình thì không, và nếu bạn có một ngôn ngữ cung cấp sự trừu tượng tốt hơn cho sự khác biệt giữa một đối tượng và tay cầm của nó, thì tôi khuyên bạn nên sử dụng nó.

Thật vậy, trong nhiều ứng dụng C ++ hiện đại, thường xảy ra trường hợp bất kỳ số học con trỏ yêu cầu nào được gói gọn và trừu tượng hóa. Chúng tôi không muốn các nhà phát triển thực hiện số học con trỏ khắp mọi nơi. Chúng tôi muốn một API tập trung, được thử nghiệm tốt, thực hiện số học con trỏ ở mức thấp nhất. Thay đổi mã này phải được thực hiện với sự cẩn thận và thử nghiệm rộng rãi.


3

Tôi nghĩ một lý do khiến con trỏ C khó khăn là chúng đưa ra một số khái niệm không thực sự tương đương; Tuy nhiên, bởi vì tất cả chúng đều được thực hiện bằng cách sử dụng các con trỏ, mọi người có thể gặp khó khăn trong việc tháo gỡ các khái niệm.

Trong C, con trỏ được sử dụng để, những thứ khác:

  • Xác định cấu trúc dữ liệu đệ quy

Trong C, bạn xác định danh sách các số nguyên được liên kết như thế này:

struct node {
  int value;
  struct node* next;
}

Con trỏ chỉ ở đó vì đây là cách duy nhất để xác định cấu trúc dữ liệu đệ quy trong C, khi khái niệm thực sự không liên quan gì đến một chi tiết cấp thấp như địa chỉ bộ nhớ. Hãy xem xét tương đương sau trong Haskell, không yêu cầu sử dụng con trỏ:

data List = List Int List | Null

Khá đơn giản - một danh sách trống hoặc được hình thành từ một giá trị và phần còn lại của danh sách.

  • Lặp lại trên chuỗi và mảng

Đây là cách bạn có thể áp dụng một hàm foocho mọi ký tự của chuỗi trong C:

char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }

Mặc dù cũng sử dụng một con trỏ như một trình vòng lặp, ví dụ này có rất ít điểm chung với cái trước đó. Tạo một trình vòng lặp mà bạn có thể tăng là một khái niệm khác với việc xác định cấu trúc dữ liệu đệ quy. Không khái niệm nào đặc biệt gắn liền với ý tưởng về một địa chỉ bộ nhớ.

  • Đạt được đa hình

Đây là một chữ ký chức năng thực tế được tìm thấy trong glib :

typedef struct g_list GList;

void  g_list_foreach    (GList *list,
                 void (*func)(void *data, void *user_data),
                         void* user_data);

Ái chà! Đó là một câu void*nói rất hay. Và tất cả chỉ là để khai báo một hàm lặp qua một danh sách có thể chứa bất kỳ loại điều nào, áp dụng một hàm cho mỗi thành viên. So sánh nó với cách mapđược khai báo trong Haskell:

map::(a->b)->[a]->[b]

Điều đó đơn giản hơn nhiều: maplà một hàm lấy một hàm chuyển đổi athành a bvà áp dụng nó vào một danh sách ađể tạo ra một danh sách b. Giống như trong hàm C g_list_foreach, mapkhông cần biết bất cứ điều gì trong định nghĩa riêng của nó về các loại mà nó sẽ được áp dụng.

Tóm lại:

Tôi nghĩ rằng các con trỏ C sẽ ít gây nhầm lẫn hơn nếu mọi người lần đầu tiên biết về các cấu trúc dữ liệu đệ quy, các trình lặp, đa hình, v.v. như các khái niệm riêng biệt, và sau đó học cách sử dụng các con trỏ để thực hiện các ý tưởng đó trong C , thay vì trộn lẫn tất cả các ý tưởng này các khái niệm cùng nhau thành một chủ đề duy nhất của "con trỏ".


Sử dụng sai c != NULLtrong ví dụ "Hello world" của bạn ... ý bạn là *c != '\0'.
Olaf Seibert

2

Tôi nghĩ rằng nó đòi hỏi một nền tảng vững chắc, có thể từ cấp độ máy, với phần giới thiệu về một số mã máy, lắp ráp và cách thể hiện các mục và cấu trúc dữ liệu trong RAM. Phải mất một ít thời gian, một số bài tập về nhà hoặc thực hành giải quyết vấn đề và một số suy nghĩ.

Nhưng nếu một người biết ngôn ngữ cấp cao lúc đầu (không có gì sai - thợ mộc sử dụng rìu. Một người cần tách nguyên tử sử dụng thứ khác. Chúng ta cần những người là thợ mộc, và chúng ta có những người nghiên cứu nguyên tử) và người biết ngôn ngữ cấp cao này được giới thiệu 2 phút về con trỏ, và sau đó khó có thể mong đợi anh ta hiểu các thuật ngữ con trỏ, con trỏ tới con trỏ, mảng con trỏ tới chuỗi kích thước thay đổi và mảng ký tự, v.v. Một nền tảng vững chắc cấp thấp có thể giúp đỡ rất nhiều.


2
Con trỏ Groking không đòi hỏi sự hiểu biết về mã máy hoặc lắp ráp.
jason

Yêu cầu, không. Nhưng những người hiểu lắp ráp có thể sẽ tìm thấy con trỏ dễ dàng hơn nhiều, vì họ đã thực hiện hầu hết (nếu không phải tất cả) các kết nối tinh thần cần thiết.
cHao

2

Vấn đề tôi luôn gặp phải (chủ yếu là tự học) là "khi nào" sử dụng một con trỏ. Tôi có thể quấn đầu xung quanh cú pháp để xây dựng một con trỏ nhưng tôi cần biết trong trường hợp nào nên sử dụng một con trỏ.

Tôi có phải là người duy nhất có suy nghĩ này không? ;-)


Tôi hiểu rồi Phản ứng của tôi kinda đối phó với điều đó.
J. Polfer

2

Ngày xửa ngày xưa ... Chúng tôi có bộ vi xử lý 8 bit và mọi người đã viết lắp ráp. Hầu hết các bộ xử lý bao gồm một số loại địa chỉ gián tiếp được sử dụng cho các bảng nhảy và hạt nhân. Khi các ngôn ngữ cấp cao hơn xuất hiện, chúng tôi thêm một lớp trừu tượng mỏng và gọi chúng là các con trỏ. Trong những năm qua, chúng tôi đã ngày càng rời xa phần cứng. Điều này không nhất thiết phải là một điều xấu. Chúng được gọi là ngôn ngữ cấp cao hơn vì một lý do. Tôi càng có thể tập trung vào những gì tôi muốn làm thay vì các chi tiết về cách nó được thực hiện tốt hơn.


2

Có vẻ như nhiều sinh viên gặp vấn đề với khái niệm về sự gián tiếp, đặc biệt là khi họ gặp khái niệm về sự gián tiếp lần đầu tiên. Tôi nhớ từ hồi còn là một sinh viên trong số hơn 100 sinh viên của khóa học của tôi, chỉ có một số ít người thực sự hiểu con trỏ.

Khái niệm về sự gián tiếp không phải là thứ mà chúng ta thường sử dụng trong cuộc sống thực, và do đó, đây là một khái niệm khó nắm bắt.


2

Gần đây tôi đã có khoảnh khắc nhấp chuột, và tôi đã ngạc nhiên rằng tôi đã tìm thấy nó khó hiểu. Nhiều hơn mọi người đã nói về nó rất nhiều, đến nỗi tôi cho rằng một số phép thuật đen tối đang diễn ra.

Cách tôi đã nhận nó là điều này. Hãy tưởng tượng rằng tất cả các biến được xác định được cung cấp không gian bộ nhớ tại thời gian biên dịch (trên ngăn xếp). Nếu bạn muốn một chương trình có thể xử lý các tệp dữ liệu lớn như âm thanh hoặc hình ảnh, bạn sẽ không muốn có một lượng bộ nhớ cố định cho các cấu trúc tiềm năng này. Vì vậy, bạn đợi cho đến khi thời gian chạy để chỉ định một lượng bộ nhớ nhất định để giữ dữ liệu này (trên heap).

Khi bạn có dữ liệu trong bộ nhớ, bạn không muốn sao chép dữ liệu đó xung quanh bus bộ nhớ của mình mỗi khi bạn muốn chạy một thao tác trên nó. Giả sử bạn muốn áp dụng bộ lọc cho dữ liệu hình ảnh của mình. Bạn có một con trỏ bắt đầu ở phía trước dữ liệu bạn đã gán cho hình ảnh và một hàm chạy ngang qua dữ liệu đó, thay đổi vị trí đó. Nếu bạn không biết những gì bạn đang làm, có thể bạn sẽ tạo ra các bản sao dữ liệu, khi bạn chạy nó trong suốt quá trình hoạt động.

Ít nhất đó là cách tôi nhìn thấy nó vào lúc này!


Sau khi suy nghĩ, bạn có thể xác định một lượng bộ nhớ cố định để giữ hình ảnh / âm thanh / video, giả sử trên một thiết bị giới hạn bộ nhớ, nhưng sau đó bạn sẽ phải đối phó với một loại giải pháp truyền phát vào và ra khỏi bộ nhớ.
Chris Barry

1

Nói như một người mới C ++ ở đây:

Hệ thống con trỏ mất một lúc để tôi tiêu hóa không nhất thiết là do khái niệm mà vì cú pháp C ++ liên quan đến Java. Một vài điều tôi thấy khó hiểu là:

(1) Khai báo biến:

A a(1);

so với

A a = A(1);

so với

A* a = new A(1); 

và rõ ràng

A a(); 

là một khai báo hàm và không phải là một khai báo biến. Trong các ngôn ngữ khác, về cơ bản chỉ có một cách để khai báo một biến.

(2) Dấu và được sử dụng theo một số cách khác nhau. Nếu nó là

int* i = &a;

thì & a là một địa chỉ bộ nhớ.

OTOH, nếu nó là

void f(int &a) {}

thì & a là một tham số truyền qua tham chiếu.

Mặc dù điều này có vẻ tầm thường, nhưng nó có thể gây nhầm lẫn cho người dùng mới - Tôi đến từ Java và Java là ngôn ngữ có cách sử dụng toán tử thống nhất hơn

(3) Mối quan hệ con trỏ mảng

Một điều hơi khó hiểu một chút là con trỏ

int* i

có thể là một con trỏ đến một int

int *i = &n; // 

hoặc là

có thể là một mảng cho một int

int* i = new int[5];

Và sau đó chỉ để làm cho mọi thứ lộn xộn hơn, con trỏ và mảng không thể thay thế cho nhau trong mọi trường hợp và con trỏ không thể được chuyển qua dưới dạng tham số mảng.

Điều này tổng hợp một số sự thất vọng cơ bản mà tôi có với C / C ++ và các con trỏ của nó, mà IMO, được kết hợp rất nhiều bởi thực tế là C / C ++ có tất cả các đặc điểm ngôn ngữ cụ thể này.


C ++ 2011 đã cải thiện mọi thứ khá nhiều khi có liên quan đến khai báo biến.
gnasher729

0

Cá nhân tôi không hiểu con trỏ ngay cả sau khi tốt nghiệp và sau khi làm việc đầu tiên. Điều duy nhất tôi biết là bạn cần nó cho danh sách liên kết, cây nhị phân và để truyền mảng vào các hàm. Đây là tình huống ngay cả ở công việc đầu tiên của tôi. Chỉ khi tôi bắt đầu trả lời phỏng vấn, tôi mới hiểu rằng khái niệm con trỏ sâu sắc và có tiềm năng sử dụng và tiềm năng rất lớn. Sau đó, tôi bắt đầu đọc K & R và viết chương trình thử nghiệm riêng. Toàn bộ mục tiêu của tôi là hướng đến công việc.
Tại thời điểm này tôi thấy rằng con trỏ thực sự không xấu cũng không khó nếu chúng được dạy theo cách tốt. Thật không may khi tôi học C khi tốt nghiệp, giáo viên không biết về con trỏ và thậm chí các bài tập cũng sử dụng ít con trỏ hơn. Ở cấp độ sau đại học, việc sử dụng con trỏ thực sự chỉ tạo ra các cây nhị phân và danh sách liên kết. Suy nghĩ này rằng bạn không cần hiểu đúng về con trỏ để làm việc với chúng, giết chết ý tưởng học chúng.


0

Con trỏ .. hah .. tất cả về con trỏ trong đầu tôi là nó cung cấp một địa chỉ bộ nhớ trong đó các giá trị thực của bất cứ thứ gì tham chiếu của nó .. vì vậy không có phép thuật nào về nó .. nếu bạn học một số cách lắp ráp, bạn sẽ không gặp nhiều khó khăn khi học con trỏ hoạt động như thế nào .. thôi nào các bạn ... ngay cả trong Java mọi thứ đều là tài liệu tham khảo ..


0

Vấn đề chính mọi người không hiểu tại sao họ cần con trỏ. Bởi vì họ không rõ ràng về stack và heap. Thật tốt khi bắt đầu từ trình biên dịch 16 bit cho x86 với chế độ bộ nhớ nhỏ. Nó giúp nhiều người có ý tưởng về stack, heap và "địa chỉ". Và byte :) Các lập trình viên hiện đại đôi khi không thể cho bạn biết bạn cần bao nhiêu byte để giải quyết không gian 32 bit. Làm thế nào họ có thể có được ý tưởng về con trỏ?

Khoảnh khắc thứ hai là ký hiệu: bạn khai báo con trỏ là *, bạn nhận địa chỉ là & và điều này không dễ hiểu đối với một số người.

Và điều cuối cùng tôi thấy là vấn đề lưu trữ: họ hiểu đống và chồng nhưng không thể có ý tưởng về "tĩnh".

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.