Làm thế nào để giải thích con trỏ C (khai báo so với toán tử đơn nguyên) cho người mới bắt đầu?


141

Tôi đã có niềm vui gần đây để giải thích con trỏ cho người mới bắt đầu lập trình C và vấp phải khó khăn sau đây. Có vẻ như đây không phải là vấn đề nếu bạn đã biết cách sử dụng con trỏ, nhưng hãy thử xem ví dụ sau với một tâm trí rõ ràng:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Đối với người mới bắt đầu tuyệt đối, đầu ra có thể gây ngạc nhiên. Trong dòng 2 anh ấy / cô ấy vừa tuyên bố * thanh là & foo, nhưng ở dòng 4 thì hóa ra * thanh thực sự là foo thay vì & foo!

Sự nhầm lẫn, bạn có thể nói, xuất phát từ sự mơ hồ của biểu tượng *: Trong dòng 2, nó được sử dụng để khai báo một con trỏ. Trong dòng 4, nó được sử dụng như một toán tử đơn nguyên tìm nạp giá trị mà con trỏ trỏ tới. Hai điều khác nhau, phải không?

Tuy nhiên, "lời giải thích" này không giúp ích gì cho người mới bắt đầu cả. Nó giới thiệu một khái niệm mới bằng cách chỉ ra sự khác biệt tinh tế. Đây không thể là cách đúng đắn để dạy nó.

Vậy, Kernighan và Ritchie đã giải thích nó như thế nào?

Toán tử unary * là toán tử indirection hoặc dereferences; khi được áp dụng cho một con trỏ, nó truy cập vào đối tượng mà con trỏ trỏ tới. [Càng]

Việc khai báo ip con trỏ, int *ipđược dự định là một bản ghi nhớ; nó nói rằng biểu thức *iplà một int. Cú pháp khai báo cho một biến bắt chước cú pháp của các biểu thức trong đó biến có thể xuất hiện .

int *ipnên được đọc như " *ipsẽ trả lại một int"? Nhưng tại sao sau đó không chuyển nhượng sau khi khai báo theo mô hình đó? Điều gì xảy ra nếu một người mới bắt đầu muốn khởi tạo biến? int *ip = 1(đọc: *ipsẽ trả lại một intint1) sẽ không hoạt động như mong đợi. Mô hình khái niệm không có vẻ mạch lạc. Am i thiếu cái gì ở đây?


Chỉnh sửa: Nó đã cố gắng tóm tắt các câu trả lời ở đây .


15
Giải thích tốt nhất là bằng cách vẽ những thứ trên một tờ giấy và kết nối chúng bằng mũi tên;)
Maroun

16
Khi tôi phải giải thích cú pháp con trỏ, tôi luôn nhấn mạnh vào thực tế rằng *trong một khai báo là một mã thông báo có nghĩa là "khai báo một con trỏ", trong biểu thức đó là toán tử quy ước và hai biểu thức này đại diện cho những điều khác nhau có cùng một biểu tượng (giống như toán tử nhân - cùng ký hiệu, nghĩa khác nhau). Thật khó hiểu, nhưng bất cứ điều gì khác với tình trạng thực tế sẽ còn tồi tệ hơn.
Matteo Italia

40
có thể viết nó như int* barlàm cho rõ ràng hơn rằng ngôi sao thực sự là một phần của loại, không phải là một phần của định danh. Tất nhiên điều này đưa bạn vào những vấn đề khác nhau với những thứ không trực quan như thế nào int* a, b.
Niklas B.

9
Tôi đã luôn nghĩ rằng lời giải thích của K & R là ngớ ngẩn và không cần thiết. Ngôn ngữ sử dụng cùng một biểu tượng cho hai thứ khác nhau và chúng ta chỉ cần đối phó với nó. *có thể có hai ý nghĩa khác nhau tùy thuộc vào ngữ cảnh. Giống như cùng một chữ cái có thể được phát âm khác nhau tùy thuộc vào từ đó khiến cho việc học nói nhiều ngôn ngữ trở nên khó khăn. Nếu mỗi khái niệm / hoạt động đơn lẻ có biểu tượng riêng, chúng ta sẽ cần bàn phím lớn hơn nhiều, vì vậy các biểu tượng được tái chế khi có ý nghĩa để làm như vậy.
Nghệ thuật

8
Tôi đã gặp vấn đề tương tự nhiều lần khi dạy C cho người khác và theo kinh nghiệm của tôi, nó có thể được giải quyết theo cách mà hầu hết mọi người ở đây đã đề xuất. Đầu tiên, giải thích khái niệm về một con trỏ không có cú pháp C. Sau đó, dạy cú pháp và nhấn mạnh vào dấu hoa thị như một phần của kiểu ( int* p), đồng thời cảnh báo học sinh của bạn không sử dụng nhiều khai báo trong cùng một dòng khi con trỏ có liên quan. Khi học sinh đã hoàn toàn hiểu khái niệm con trỏ, hãy giải thích cho học sinh rằng int *pcú pháp là tương đương và sau đó giải thích vấn đề bằng nhiều khai báo.
Theodoros Chatzigiannakis

Câu trả lời:


43

Để học sinh của bạn hiểu ý nghĩa của * biểu tượng trong các bối cảnh khác nhau, trước tiên họ phải hiểu rằng các bối cảnh thực sự khác nhau. Một khi họ hiểu rằng các bối cảnh là khác nhau (tức là sự khác biệt giữa phía bên trái của một bài tập và một biểu thức chung), đó không phải là một bước nhảy vọt về nhận thức để hiểu sự khác biệt là gì.

Trước hết giải thích rằng việc khai báo một biến không thể chứa toán tử (chứng minh điều này bằng cách chỉ ra rằng việc đặt một -hoặc +ký hiệu trong khai báo biến chỉ đơn giản là gây ra lỗi). Sau đó tiếp tục chỉ ra rằng một biểu thức (tức là ở phía bên phải của một bài tập) có thể chứa các toán tử. Hãy chắc chắn rằng học sinh hiểu rằng một biểu thức và một khai báo biến là hai bối cảnh hoàn toàn khác nhau.

Khi họ hiểu rằng các bối cảnh là khác nhau, bạn có thể tiếp tục giải thích rằng khi *biểu tượng nằm trong một khai báo biến ở phía trước định danh biến, điều đó có nghĩa là 'khai báo biến này là một con trỏ'. Sau đó, bạn có thể giải thích rằng khi được sử dụng trong một biểu thức (với tư cách là toán tử đơn nguyên), *ký hiệu là 'toán tử quy ước' và nó có nghĩa là 'giá trị tại địa chỉ của' chứ không phải là ý nghĩa trước đó.

Để thực sự thuyết phục học sinh của bạn, giải thích rằng những người tạo ra C có thể đã sử dụng bất kỳ biểu tượng nào có nghĩa là toán tử quy ước (nghĩa là họ có thể đã sử dụng @thay thế) nhưng vì lý do gì họ đã đưa ra quyết định thiết kế để sử dụng *.

Nói chung, không có cách nào để giải thích rằng bối cảnh là khác nhau. Nếu học sinh không hiểu bối cảnh là khác nhau, họ không thể hiểu tại sao *biểu tượng có thể có nghĩa khác nhau.


80

Lý do tại sao tốc ký:

int *bar = &foo;

trong ví dụ của bạn có thể gây nhầm lẫn là dễ đọc sai nó tương đương với:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

khi nó thực sự có nghĩa là:

int *bar;
bar = &foo;

Được viết ra như thế này, với khai báo biến và gán được phân tách, không có khả năng gây nhầm lẫn như vậy và việc sử dụng song song khai báo được mô tả trong trích dẫn K & R của bạn hoạt động hoàn hảo:

  • Dòng đầu tiên khai báo một biến bar, đó *barlà một int.

  • Dòng thứ hai gán địa chỉ của foođể bar, làm cho *bar(một int) một bí danh cho foo(cũng là một int).

Khi giới thiệu cú pháp con trỏ C cho người mới bắt đầu, ban đầu có thể hữu ích khi sử dụng kiểu khai báo con trỏ này từ các bài tập và chỉ giới thiệu cú pháp tốc ký kết hợp (với các cảnh báo thích hợp về khả năng nhầm lẫn của nó) khi các khái niệm cơ bản về con trỏ sử dụng trong C đã được nội hóa đầy đủ.


4
Tôi bị cám dỗ typedef. typedef int *p_int;có nghĩa là một biến loại p_intcó thuộc tính *p_intlà một int. Rồi chúng ta có p_int bar = &foo;. Việc khuyến khích bất cứ ai tạo ra dữ liệu chưa được khởi tạo và sau đó gán cho nó như một vấn đề của thói quen mặc định dường như ... giống như một ý tưởng tồi.
Yakk - Adam Nevraumont

6
Đây chỉ là phong cách tổn thương não của tuyên bố C; nó không cụ thể cho con trỏ. xem xét int a[2] = {47,11};, đó không phải là khởi tạo của phần tử (không tồn tại) a[2]eiher.
Marc van Leeuwen

5
@MarcvanLeeuwen Đồng ý với tổn thương não. Tốt nhất, *nên là một phần của loại, không bị ràng buộc với biến và sau đó bạn có thể viết int* foo_ptr, bar_ptrđể khai báo hai con trỏ. Nhưng nó thực sự khai báo một con trỏ và một số nguyên.
Barmar

1
Nó không chỉ là về khai báo / bài tập "tốc ký". Toàn bộ vấn đề xuất hiện trở lại vào thời điểm bạn muốn sử dụng các con trỏ làm đối số hàm.
armin

30

Viết tắt

Thật tuyệt khi biết sự khác biệt giữa khai báo và khởi tạo. Chúng tôi khai báo các biến là các loại và khởi tạo chúng với các giá trị. Nếu chúng ta làm cả hai cùng một lúc, chúng ta thường gọi nó là một định nghĩa.

1. int a; a = 42;

int a;
a = 42;

Chúng tôi tuyên bố một inttên một . Sau đó, chúng tôi khởi tạo nó bằng cách cho nó một giá trị 42.

2. int a = 42;

Chúng tôi khai báointđặt tên a và cung cấp cho nó giá trị 42. Nó được khởi tạo với 42. Một định nghĩa.

3. a = 43;

Khi chúng tôi sử dụng các biến chúng tôi nói chúng tôi hoạt động trên chúng. a = 43là một hoạt động chuyển nhượng. Ta gán số 43 cho biến a.

Bằng cách nói

int *bar;

chúng tôi tuyên bố thanh là một con trỏ đến một int. Bằng cách nói

int *bar = &foo;

chúng tôi khai báo thanh và khởi tạo nó với địa chỉ của foo .

Sau khi chúng ta đã khởi tạo thanh, chúng ta có thể sử dụng cùng một toán tử, dấu hoa thị, để truy cập và hoạt động trên giá trị của foo . Không có toán tử, chúng ta truy cập và vận hành theo địa chỉ mà con trỏ đang trỏ tới.

Bên cạnh đó tôi để hình ảnh lên tiếng.

Một ASCIIMATION đơn giản hóa về những gì đang diễn ra. (Và đây là phiên bản người chơi nếu bạn muốn tạm dừng, v.v.)

          ASCIIMATION


22

Câu lệnh thứ 2 int *bar = &foo;có thể được xem bằng hình ảnh trong bộ nhớ như,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Bây giờ barlà một con trỏ của loại intchứa địa chỉ &của foo. Sử dụng toán tử đơn nguyên, *chúng tôi trì hoãn để lấy giá trị chứa trong 'foo' bằng cách sử dụng con trỏ bar.

EDIT : Cách tiếp cận của tôi với người mới bắt đầu là giải thích memory addressvề một biến tức là

Memory Address:Mỗi biến có một địa chỉ liên quan đến nó được cung cấp bởi HĐH. Trong int a;, &alà địa chỉ của biến a.

Tiếp tục giải thích các loại biến cơ bản trong C,

Types of variables: Các biến có thể chứa các giá trị của các loại tương ứng nhưng không chứa địa chỉ.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Như đã nói ở trên, ví dụ

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Có thể gán b = anhưng không b = &a, vì biến bcó thể giữ giá trị nhưng không phải địa chỉ, do đó chúng tôi yêu cầu Con trỏ .

Pointer or Pointer variables :Nếu một biến chứa một địa chỉ, nó được gọi là biến con trỏ. Sử dụng *trong khai báo để thông báo rằng nó là một con trỏ.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable

3
Vấn đề là đọc int *ip"ip là một con trỏ (*) của kiểu int" bạn gặp rắc rối khi đọc một cái gì đó như x = (int) *ip.
armin

2
@abw Đó là một cái gì đó hoàn toàn khác, do đó là dấu ngoặc đơn. Tôi không nghĩ mọi người sẽ gặp khó khăn trong việc hiểu sự khác biệt giữa khai báo và đúc.
bzeaman

@abw Trong x = (int) *ip;, lấy giá trị bằng con trỏ hội nghị ipvà truyền giá trị inttừ bất kỳ loại nào ip.
Sunil Bojanapally

1
@BennoZeeman Bạn đã đúng: đúc và khai báo là hai điều khác nhau. Tôi đã cố gắng gợi ý về vai trò khác nhau của dấu hoa thị: 1st "đây không phải là int, nhưng một con trỏ tới int" 2nd "sẽ cung cấp cho bạn int, nhưng không phải là con trỏ tới int".
armin

2
@abw: Đó là lý do tại sao việc giảng dạy int* bar = &foo;làm cho tải có ý nghĩa hơn. Vâng, tôi biết nó gây ra vấn đề khi bạn khai báo nhiều con trỏ trong một khai báo. Không, tôi không nghĩ rằng vấn đề đó cả.
Các cuộc đua nhẹ nhàng trong quỹ đạo

17

Nhìn vào các câu trả lời và nhận xét ở đây, dường như có một thỏa thuận chung rằng cú pháp trong câu hỏi có thể gây nhầm lẫn cho người mới bắt đầu. Hầu hết trong số họ đề xuất một cái gì đó dọc theo những dòng này:

  • Trước khi hiển thị bất kỳ mã nào, hãy sử dụng sơ đồ, phác thảo hoặc hình động để minh họa cách hoạt động của con trỏ.
  • Khi trình bày cú pháp, hãy giải thích hai vai trò khác nhau của biểu tượng dấu hoa thị . Nhiều hướng dẫn bị thiếu hoặc trốn tránh phần đó. Sự nhầm lẫn xảy ra ("Khi bạn phá vỡ một khai báo con trỏ khởi tạo thành một khai báo và một lần gán sau, bạn phải nhớ xóa *" - comp.lang.c FAQ ) Tôi hy vọng tìm thấy một cách tiếp cận khác, nhưng tôi đoán đây là con đường để đi.

Bạn có thể viết int* barthay vì int *barđể làm nổi bật sự khác biệt. Điều này có nghĩa là bạn sẽ không tuân theo cách tiếp cận "sử dụng bắt chước khai báo" của K & R, nhưng Stroustrup C ++ :

Chúng tôi không tuyên bố *barlà một số nguyên. Chúng tôi tuyên bố barlà một int*. Nếu chúng ta muốn khởi tạo một biến mới được tạo trong cùng một dòng, rõ ràng là chúng ta đang xử lý bar, không phải*bar .int* bar = &foo;

Hạn chế:

  • Bạn phải cảnh báo học sinh của mình về vấn đề khai báo nhiều con trỏ (int* foo, bar vs int *foo, *bar).
  • Bạn phải chuẩn bị cho họ một thế giới bị tổn thương . Nhiều lập trình viên muốn nhìn thấy dấu hoa thị liền kề với tên của biến và họ sẽ mất nhiều thời gian để chứng minh phong cách của họ. Và nhiều hướng dẫn kiểu thực thi ký hiệu này một cách rõ ràng (kiểu mã hóa nhân Linux, Hướng dẫn kiểu C của NASA, v.v.).

Chỉnh sửa: Một cách tiếp cận khác đã được đề xuất, là đi theo cách "bắt chước" của K & R, nhưng không có cú pháp "tốc ký" (xem tại đây ). Ngay khi bạn bỏ qua việc khai báo và chuyển nhượng trong cùng một dòng , mọi thứ sẽ trông mạch lạc hơn nhiều.

Tuy nhiên, sớm hay muộn học sinh sẽ phải đối phó với các con trỏ như các đối số chức năng. Và con trỏ như kiểu trả về. Và con trỏ đến các chức năng. Bạn sẽ phải giải thích sự khác biệt giữa int *func();int (*func)();. Tôi nghĩ sớm hay muộn mọi thứ sẽ sụp đổ. Và có lẽ sớm hơn là tốt hơn sau này.


16

Có một lý do tại sao ủng hộ phong cách K & R int *pvà ủng hộ phong cách Stroustrup int* p; cả hai đều hợp lệ (và có nghĩa là cùng một thứ) trong mỗi ngôn ngữ, nhưng như Stroustrup đặt nó:

Sự lựa chọn giữa "int * p;" và "int * p;" không phải là về đúng và sai, mà là về phong cách và sự nhấn mạnh. C nhấn mạnh biểu thức; tuyên bố thường được coi là ít hơn một điều ác cần thiết. C ++, mặt khác, có một sự nhấn mạnh về các loại.

Bây giờ, vì bạn đang cố gắng dạy C ở đây, điều đó sẽ gợi ý bạn nên nhấn mạnh các biểu thức hơn các kiểu đó, nhưng một số người có thể dễ dàng mò mẫm một điểm nhấn nhanh hơn so với người khác, và đó là về ngôn ngữ.

Do đó, một số người sẽ thấy dễ dàng hơn khi bắt đầu với ý tưởng rằng một thứ int*là một thứ khác với một intvà đi từ đó.

Nếu ai đó nhanh chóng tìm ra cách nhìn vào nó mà int* barkhông phải barlà một thứ không phải là một int, mà là một con trỏ tới int, thì họ sẽ nhanh chóng thấy rằng nó *barđang làm gì đó đểbar , và phần còn lại sẽ làm theo. Khi bạn đã hoàn thành, bạn có thể giải thích lý do tại sao các lập trình viên C có xu hướng thích int *bar.

Hay không. Nếu có một cách mà mọi người lần đầu tiên hiểu khái niệm bạn sẽ không gặp vấn đề gì ngay từ đầu, và cách tốt nhất để giải thích nó cho một người sẽ không nhất thiết là cách tốt nhất để giải thích nó cho người khác.


1
Tôi thích lập luận của Stroustrup, nhưng tôi tự hỏi tại sao anh ấy chọn biểu tượng & để biểu thị các tham chiếu - một cạm bẫy có thể khác.
armin

1
@abw Tôi nghĩ rằng anh ấy đã thấy sự đối xứng nếu chúng ta có thể làm int* p = &athì chúng ta có thể làm int* r = *p. Tôi khá chắc chắn rằng anh ấy đã trình bày nó trong Thiết kế và Tiến hóa của C ++ , nhưng đã lâu rồi tôi mới đọc nó và tôi dại dột gửi bản sao của mình cho ai đó.
Jon Hanna

3
Tôi đoán bạn có nghĩa là int& r = *p. Và tôi cá là người vay vẫn đang cố tiêu hóa cuốn sách.
armin

@abw, vâng đó chính xác là những gì tôi muốn nói. Than ôi lỗi chính tả trong các bình luận không làm tăng lỗi biên dịch. Cuốn sách thực sự là khá nhanh chóng đọc.
Jon Hanna

4
Một trong những lý do tôi ủng hộ cú pháp của Pascal (như được mở rộng phổ biến) so với C là điều đó Var A, B: ^Integer;làm rõ rằng loại "con trỏ tới số nguyên" áp dụng cho cả hai AB. Sử dụng một K&Rphong cách int *a, *bcũng khả thi; nhưng một tuyên bố như int* a,b;, tuy nhiên, trông như thể abcả hai đều được tuyên bố là int*, nhưng trong thực tế, nó tuyên bố anhư một int*bnhư một int.
supercat

9

tl; dr:

Q: Làm thế nào để giải thích con trỏ C (khai báo so với toán tử đơn nguyên) cho người mới bắt đầu?

A: không. Giải thích các con trỏ cho người mới bắt đầu và chỉ cho họ cách biểu diễn các khái niệm con trỏ của họ theo cú pháp C sau.


Tôi đã có niềm vui gần đây để giải thích con trỏ cho người mới bắt đầu lập trình C và vấp phải khó khăn sau đây.

IMO cú pháp C không tệ, nhưng cũng không tuyệt vời: đó không phải là trở ngại lớn nếu bạn đã hiểu về con trỏ, cũng không có bất kỳ trợ giúp nào trong việc học chúng.

Do đó: bắt đầu bằng cách giải thích các gợi ý và đảm bảo rằng họ thực sự hiểu chúng:

  • Giải thích chúng với sơ đồ hộp và mũi tên. Bạn có thể làm điều đó mà không cần địa chỉ hex, nếu chúng không liên quan, chỉ hiển thị các mũi tên chỉ vào hộp khác hoặc biểu tượng nul nào đó.

  • Giải thích với mã giả: chỉ cần ghi địa chỉ của foogiá trị được lưu trữ trên thanh .

  • Sau đó, khi người mới của bạn hiểu con trỏ là gì, và tại sao, và làm thế nào để sử dụng chúng; sau đó hiển thị ánh xạ vào cú pháp C.

Tôi nghi ngờ lý do văn bản K & R không cung cấp mô hình khái niệm là vì họ đã hiểu con trỏ và có lẽ giả định rằng mọi lập trình viên có năng lực khác tại thời điểm đó cũng vậy. Bản ghi nhớ chỉ là một lời nhắc nhở về ánh xạ từ khái niệm được hiểu rõ, đến cú pháp.


Thật; Bắt đầu với lý thuyết trước, cú pháp đến sau (và không quan trọng). Lưu ý rằng lý thuyết sử dụng bộ nhớ không phụ thuộc vào ngôn ngữ. Mô hình hộp và mũi tên này sẽ giúp bạn thực hiện các tác vụ trong bất kỳ ngôn ngữ lập trình nào.
oɔɯǝɹ

Xem ở đây để biết một số ví dụ (mặc dù google cũng sẽ giúp) eskimo.com/~scs/cgroup/notes/sx10a.html
oɔɯǝɹ

7

Vấn đề này hơi khó hiểu khi bắt đầu học C.

Dưới đây là các nguyên tắc cơ bản có thể giúp bạn bắt đầu:

  1. Chỉ có một vài loại cơ bản trong C:

    • char: một giá trị nguyên với kích thước 1 byte.

    • short: một giá trị nguyên với kích thước 2 byte.

    • long: một giá trị nguyên với kích thước 4 byte.

    • long long: một giá trị nguyên với kích thước 8 byte.

    • float: một giá trị không nguyên với kích thước 4 byte.

    • double: một giá trị không nguyên với kích thước 8 byte.

    Lưu ý rằng kích thước của mỗi loại thường được xác định bởi trình biên dịch chứ không phải theo tiêu chuẩn.

    Các loại nguyên short, longlong long thường được theo sau bởi int.

    Tuy nhiên, đây không phải là điều bắt buộc và bạn có thể sử dụng chúng mà không cần int .

    Ngoài ra, bạn chỉ có thể nêu int , nhưng điều đó có thể được diễn giải khác nhau bởi các trình biên dịch khác nhau.

    Vì vậy, để tóm tắt điều này:

    • shortgiống như short intnhưng không nhất thiết phải giống nhưint .

    • longgiống như long intnhưng không nhất thiết phải giống nhưint .

    • long longgiống như long long intnhưng không nhất thiết phải giống nhưint .

    • Trên một trình biên dịch nhất định, intshort inthoặc long inthoặc long long int.

  2. Nếu bạn khai báo một biến của một số loại, thì bạn cũng có thể khai báo một biến khác trỏ đến nó.

    Ví dụ:

    int a;

    int* b = &a;

    Vì vậy, về bản chất, đối với mỗi loại cơ bản, chúng ta cũng có một loại con trỏ tương ứng.

    Ví dụ: shortshort*.

    Có hai cách để "nhìn" biến b (đó là điều có thể gây nhầm lẫn cho hầu hết người mới bắt đầu) :

    • Bạn có thể coi bnhư một biến của loại int*.

    • Bạn có thể coi *bnhư một biến của loại int.

    Do đó, một số người sẽ tuyên bố int* b, trong khi những người khác sẽ tuyên bố int *b.

    Nhưng thực tế của vấn đề là hai tuyên bố này là giống hệt nhau (các không gian là vô nghĩa).

    Bạn có thể sử dụng blàm con trỏ tới giá trị số nguyên hoặc *blàm giá trị số nguyên nhọn thực tế.

    Bạn có thể nhận (đọc) giá trị nhọn : int c = *b.

    Và bạn có thể đặt (ghi) giá trị nhọn : *b = 5.

  3. Một con trỏ có thể trỏ đến bất kỳ địa chỉ bộ nhớ nào và không chỉ đến địa chỉ của một số biến mà bạn đã khai báo trước đó. Tuy nhiên, bạn phải cẩn thận khi sử dụng các con trỏ để lấy hoặc đặt giá trị nằm ở địa chỉ bộ nhớ nhọn.

    Ví dụ:

    int* a = (int*)0x8000000;

    Ở đây, chúng ta có biến atrỏ đến địa chỉ bộ nhớ 0x8000000.

    Nếu địa chỉ bộ nhớ này không được ánh xạ trong không gian bộ nhớ của chương trình của bạn, thì mọi thao tác đọc hoặc ghi sử dụng *arất có thể sẽ khiến chương trình của bạn gặp sự cố, do vi phạm quyền truy cập bộ nhớ.

    Bạn có thể thay đổi giá trị của một cách an toàn a, nhưng bạn nên rất cẩn thận thay đổi giá trị của *a.

  4. Loại void*là đặc biệt trong thực tế là nó không có "loại giá trị" tương ứng có thể được sử dụng (nghĩa là bạn không thể khai báo void a). Loại này chỉ được sử dụng làm con trỏ chung cho địa chỉ bộ nhớ, mà không chỉ định loại dữ liệu cư trú trong địa chỉ đó.


7

Có lẽ bước qua nó chỉ một chút nữa sẽ dễ dàng hơn:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Yêu cầu họ cho bạn biết những gì họ mong đợi đầu ra ở mỗi dòng, sau đó cho họ chạy chương trình và xem những gì bật lên. Giải thích các câu hỏi của họ (phiên bản khỏa thân trong đó chắc chắn sẽ nhắc nhở một vài người - nhưng bạn có thể lo lắng về phong cách, sự nghiêm ngặt và tính di động sau này). Sau đó, trước khi tâm trí của họ chuyển sang ủ rũ vì suy nghĩ quá mức hoặc họ trở thành một thây ma sau bữa ăn trưa, hãy viết một hàm lấy một giá trị, và cùng một hàm lấy một con trỏ.

Theo kinh nghiệm của tôi, nó đã vượt qua điều đó "tại sao điều này lại in theo cách đó?" bướu, và sau đó ngay lập tức cho thấy lý do tại sao điều này hữu ích trong các tham số chức năng bằng cách chơi đồ chơi bằng tay (như một khúc dạo đầu cho một số tài liệu K & R cơ bản như phân tích chuỗi / xử lý mảng) làm cho bài học không chỉ có ý nghĩa mà còn gắn bó.

Bước tiếp theo là để họ giải thích cho bạni[0] liên quan đến nó như thế nào &i. Nếu họ có thể làm điều đó, họ sẽ không quên điều đó và bạn có thể bắt đầu nói về các cấu trúc, thậm chí đi trước thời đại một chút, chỉ để nó chìm vào.

Các khuyến nghị ở trên về hộp và mũi tên cũng tốt, nhưng nó cũng có thể dẫn đến việc thảo luận đầy đủ về cách thức hoạt động của bộ nhớ - đó là một cuộc nói chuyện phải xảy ra tại một số điểm, nhưng có thể làm mất tập trung ngay lập tức : làm thế nào để giải thích ký hiệu con trỏ trong C.


Đây là một bài tập tốt. Nhưng vấn đề tôi muốn đưa ra là một cú pháp cụ thể có thể có tác động đến mô hình tinh thần mà các sinh viên xây dựng. Hãy xem xét điều này : int foo = 1;. Bây giờ điều này là OK : int *bar; *bar = foo;. Điều này không ổn:int *bar = foo;
armin

1
@abw Điều duy nhất có ý nghĩa là bất cứ điều gì học sinh tự nói với mình. Điều đó có nghĩa là "nhìn thấy một, làm một, dạy một". Bạn không thể bảo vệ hoặc dự đoán cú pháp hoặc phong cách nào họ sẽ thấy ngoài rừng (thậm chí cả repos cũ của bạn!), Vì vậy bạn phải thể hiện đủ các hoán vị rằng các khái niệm cơ bản được hiểu độc lập với phong cách - và sau đó bắt đầu dạy họ tại sao một số phong cách nhất định đã được giải quyết. Giống như dạy tiếng Anh: thành ngữ cơ bản, thành ngữ, phong cách, phong cách cụ thể trong một bối cảnh nhất định. Không dễ, thật không may. Dù thế nào đi nữa, chúc may mắn!
zxq9

6

Loại biểu thức *barint; do đó, loại của biến (và biểu thức) barint *. Do biến có kiểu con trỏ, nên trình khởi tạo của nó cũng phải có kiểu con trỏ.

Có sự không nhất quán giữa khởi tạo và gán biến con trỏ; đó chỉ là thứ phải học một cách khó khăn.


3
Nhìn vào các câu trả lời ở đây tôi có cảm giác rằng nhiều lập trình viên có kinh nghiệm thậm chí không thể nhìn thấy vấn đề nữa. Tôi đoán đó là sản phẩm phụ của "học cách sống không nhất quán".
armin

3
@abw: quy tắc khởi tạo khác với quy tắc chuyển nhượng; đối với các kiểu số học vô hướng, sự khác biệt là không đáng kể, nhưng chúng quan trọng đối với các kiểu con trỏ và tổng hợp. Đó là điều bạn sẽ cần phải giải thích cùng với mọi thứ khác.
John Bode

5

Tôi thà đọc nó như là lần đầu tiên *áp dụng cho intnhiều hơn bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value

2
Sau đó, bạn phải giải thích tại sao int* a, bkhông làm những gì họ nghĩ nó làm.
Pharap

4
Đúng, nhưng tôi không nghĩ rằng int* a,bnên sử dụng tất cả. Để có khả năng hiển thị tốt hơn, cập nhật, v.v ... chỉ nên có một khai báo biến trên mỗi dòng và không bao giờ nhiều hơn. Đó là một cái gì đó để giải thích cho người mới bắt đầu, ngay cả khi trình biên dịch có thể xử lý nó.
grorel

Đó là ý kiến ​​của một người đàn ông. Có hàng triệu lập trình viên ngoài kia hoàn toàn ổn với việc khai báo nhiều hơn một biến trên mỗi dòng và thực hiện nó hàng ngày như một phần công việc của họ. Bạn không thể che giấu sinh viên khỏi những cách làm việc khác, tốt hơn là chỉ cho họ tất cả các lựa chọn thay thế và để họ quyết định họ muốn làm theo cách nào bởi vì nếu họ đã đi làm, họ sẽ phải theo một phong cách nhất định họ có thể hoặc không thể thoải mái với. Đối với một lập trình viên, tính linh hoạt là một đặc điểm rất tốt để có.
Pharap

1
Tôi đồng ý với @grorel. Nó dễ dàng hơn để nghĩ về *một phần của loại hình, và chỉ đơn giản là không khuyến khích int* a, b. Trừ khi bạn thích nói rằng đó *alà loại intchứ không phải alà một con trỏ tới int...
Kevin Ushey 16/12/14

@grorel đúng: int *a, b;không nên sử dụng. Khai báo hai biến với các loại khác nhau trong cùng một tuyên bố là thực tiễn khá kém và là một ứng cử viên nặng ký cho các vấn đề bảo trì. Có lẽ nó khác với những người trong chúng ta làm việc trong trường nhúng, nơi một int*và một intthường có kích thước khác nhau và đôi khi được lưu trữ ở các vị trí bộ nhớ hoàn toàn khác nhau. Đó là một trong nhiều khía cạnh của ngôn ngữ C sẽ được dạy tốt nhất là 'nó được cho phép, nhưng đừng làm điều đó'.
Evil Dog Pie

5
int *bar = &foo;

Question 1: Là bargì?

Ans: Đây là một biến con trỏ (để gõ int). Một con trỏ phải trỏ đến một số vị trí bộ nhớ hợp lệ và sau đó sẽ được hủy đăng ký (* bar) bằng cách sử dụng một toán tử đơn nguyên *để đọc giá trị được lưu trữ trong vị trí đó.

Question 2: Là &foogì?

Ans: foo là một biến loại int.which được lưu trữ ở một số vị trí bộ nhớ hợp lệ và vị trí đó chúng ta lấy nó từ toán tử, &vì vậy bây giờ những gì chúng ta có là một vị trí bộ nhớ hợp lệ &foo.

Vì vậy, cả hai đặt cùng nhau, tức là những gì con trỏ cần là một vị trí bộ nhớ hợp lệ và có được do &foođó việc khởi tạo là tốt.

Bây giờ con trỏ barđang trỏ đến vị trí bộ nhớ hợp lệ và giá trị được lưu trữ trong đó có thể được lấy ra từ đó*bar


5

Bạn nên chỉ ra một người mới bắt đầu rằng * có ý nghĩa khác nhau trong khai báo và biểu thức. Như bạn đã biết, * trong biểu thức là một toán tử đơn nguyên và * Trong khai báo không phải là một toán tử và chỉ là một loại cú pháp kết hợp với loại để cho trình biên dịch biết rằng đó là một loại con trỏ. tốt hơn là nói một người mới bắt đầu, "* có ý nghĩa khác. Để hiểu ý nghĩa của *, bạn nên tìm nơi * được sử dụng"


4

Tôi nghĩ rằng ma quỷ ở trong không gian.

Tôi sẽ viết (không chỉ cho người mới bắt đầu, mà còn cho bản thân tôi): int * bar = & foo; thay vì int * bar = & foo;

điều này sẽ làm rõ mối quan hệ giữa cú pháp và ngữ nghĩa là gì


4

Nó đã được lưu ý rằng * có nhiều vai trò.

Có một ý tưởng đơn giản khác có thể giúp người mới bắt đầu nắm bắt mọi thứ:

Hãy nghĩ rằng "=" cũng có nhiều vai trò.

Khi gán được sử dụng trên cùng một dòng với khai báo, hãy nghĩ về nó như một lệnh gọi của hàm tạo, không phải là một phép gán tùy ý.

Khi bạn thấy:

int *bar = &foo;

Hãy nghĩ rằng nó gần tương đương với:

int *bar(&foo);

Dấu ngoặc đơn được ưu tiên hơn dấu hoa thị, vì vậy "& foo" dễ dàng được gán cho "thanh" hơn là "thanh".


4

Tôi đã thấy câu hỏi này vài ngày trước, và sau đó tình cờ đọc được lời giải thích về khai báo loại của Go trên Blog Go . Nó bắt đầu bằng cách đưa ra một tài khoản khai báo loại C, có vẻ như là một tài nguyên hữu ích để thêm vào chủ đề này, mặc dù tôi nghĩ rằng đã có nhiều câu trả lời đầy đủ hơn.

C đã có một cách tiếp cận khác thường và thông minh để cú pháp khai báo. Thay vì mô tả các loại bằng cú pháp đặc biệt, người ta viết một biểu thức liên quan đến mục được khai báo và cho biết loại biểu thức đó sẽ có. Như vậy

int x;

khai báo x là một int: biểu thức 'x' sẽ có kiểu int. Nói chung, để tìm ra cách viết loại biến mới, hãy viết một biểu thức liên quan đến biến đó đánh giá thành loại cơ bản, sau đó đặt loại cơ bản ở bên trái và biểu thức ở bên phải.

Do đó, các tờ khai

int *p;
int a[3];

trạng thái rằng p là một con trỏ tới int vì '* p' có kiểu int và a là một mảng int vì a [3] (bỏ qua giá trị chỉ mục cụ thể, bị trừng phạt là kích thước của mảng) có loại int.

(Nó tiếp tục mô tả cách mở rộng sự hiểu biết này đến các con trỏ hàm, v.v.)

Đây là một cách mà tôi chưa từng nghĩ về nó trước đây, nhưng có vẻ như đây là một cách hạch toán khá đơn giản cho việc quá tải cú pháp.


3

Nếu vấn đề là cú pháp, có thể hữu ích để hiển thị mã tương đương với mẫu / sử dụng.

template<typename T>
using ptr = T*;

Điều này sau đó có thể được sử dụng như

ptr<int> bar = &foo;

Sau đó, so sánh cú pháp bình thường / C với cách tiếp cận chỉ C ++ này. Điều này cũng hữu ích để giải thích con trỏ const.


2
Đối với người mới bắt đầu nó sẽ khó hiểu hơn rất nhiều.
Karsten

Mặc dù tôi là bạn sẽ không hiển thị định nghĩa của ptr. Chỉ cần sử dụng nó để khai báo con trỏ.
MI3Guy

3

Nguồn gốc của sự nhầm lẫn xuất phát từ thực tế là *biểu tượng có thể có ý nghĩa khác nhau trong C, tùy thuộc vào thực tế mà nó được sử dụng. Để giải thích con trỏ cho người mới bắt đầu, ý nghĩa của *biểu tượng trong ngữ cảnh khác nhau cần được giải thích.

Trong tờ khai

int *bar = &foo;  

các *biểu tượng là không phải là nhà điều hành gián tiếp . Thay vào đó, nó giúp chỉ định kiểu barthông báo cho trình biên dịch barcon trỏ tới mộtint . Mặt khác, khi nó xuất hiện trong một câu lệnh, *ký hiệu (khi được sử dụng như một toán tử đơn nguyên ) thực hiện cảm ứng. Do đó, tuyên bố

*bar = &foo;

sẽ là sai khi nó gán địa chỉ của foođối tượng bartrỏ đến chứ không phải cho barchính nó.


3

"có thể viết nó dưới dạng int * bar làm cho rõ ràng hơn rằng ngôi sao thực sự là một phần của loại, không phải là một phần của định danh." Vì vậy tôi làm. Và tôi nói rằng, nó giống như Type, nhưng chỉ cho một tên con trỏ.

"Tất nhiên điều này đưa bạn vào những vấn đề khác nhau với những thứ không trực quan như int * a, b."


2

Ở đây bạn phải sử dụng, hiểu và giải thích logic trình biên dịch, không phải logic của con người (tôi biết, bạn là một con người, nhưng ở đây bạn phải bắt chước máy tính ...).

Khi bạn viết

int *bar = &foo;

các nhóm biên dịch như

{ int * } bar = &foo;

Đó là: đây là một biến mới, tên của nó là bar, kiểu của nó là con trỏ tới int và giá trị ban đầu của nó là &foo.

Và bạn phải thêm: các =biểu thị trên một khởi không phải là một sự giả tạo, trong khi trong các biểu thức sau *bar = 2; một affectation

Chỉnh sửa mỗi bình luận:

Chú ý: trong trường hợp khai báo nhiều, *chỉ liên quan đến biến sau:

int *bar = &foo, b = 2;

thanh là một con trỏ tới int được khởi tạo bởi địa chỉ của foo, b là một int được khởi tạo thành 2 và trong

int *bar=&foo, **p = &bar;

thanh trong con trỏ tĩnh đến int và p là con trỏ tới con trỏ tới int được khởi tạo cho địa chỉ hoặc thanh.


2
Trên thực tế trình biên dịch không nhóm nó như thế: int* a, b;tuyên bố a là con trỏ tới an int, nhưng b là an int. Các *biểu tượng chỉ có hai ý nghĩa khác nhau: Trong một tuyên bố, nó cho thấy một kiểu con trỏ, và trong một biểu thức đó là các nhà điều hành dereference unary.
tmlen

@tmlen: Điều tôi muốn nói là trong quá trình khởi tạo, *in rattached đến loại, do đó con trỏ được khởi tạo trong khi ảnh hưởng đến giá trị nhọn bị ảnh hưởng. Nhưng ít nhất bạn đã cho tôi một chiếc mũ đẹp :-)
Serge Ballesta

0

Về cơ bản Con trỏ không phải là một dấu hiệu mảng. Người mới bắt đầu dễ dàng nghĩ rằng con trỏ trông giống như mảng. hầu hết các ví dụ chuỗi sử dụng

"char * pstr" trông giống như

"char str [80]"

Nhưng, Điều quan trọng, Con trỏ được coi là số nguyên ở cấp độ thấp hơn của trình biên dịch.

Hãy xem ví dụ ::

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Kết quả sẽ như thế này 0x2a6b7ed0 là địa chỉ của str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Vì vậy, về cơ bản, Hãy nhớ rằng Con trỏ là một loại Số nguyên. trình bày Địa chỉ.


-1

Tôi sẽ giải thích rằng ints là các đối tượng, cũng như float, v.v. Con trỏ là một loại đối tượng có giá trị đại diện cho một địa chỉ trong bộ nhớ (do đó tại sao một con trỏ mặc định là NULL).

Khi bạn lần đầu tiên khai báo một con trỏ, bạn sử dụng cú pháp tên con trỏ kiểu. Nó được đọc như là một "con trỏ số nguyên được gọi là tên có thể trỏ đến địa chỉ của bất kỳ đối tượng số nguyên nào". Chúng tôi chỉ sử dụng cú pháp này trong quá trình giải mã, tương tự như cách chúng tôi khai báo một int là 'int num1' nhưng chúng tôi chỉ sử dụng 'num1' khi chúng tôi muốn sử dụng biến đó, không phải 'int num1'.

int x = 5; // một đối tượng số nguyên có giá trị là 5

int * ptr; // một số nguyên có giá trị NULL theo mặc định

Để tạo một con trỏ trỏ đến một địa chỉ của một đối tượng, chúng ta sử dụng ký hiệu '&' có thể được đọc là "địa chỉ của".

ptr = & x; // giá trị bây giờ là địa chỉ của 'x'

Vì con trỏ chỉ là địa chỉ của đối tượng, để có được giá trị thực được giữ tại địa chỉ đó, chúng ta phải sử dụng ký hiệu '*' mà khi được sử dụng trước một con trỏ có nghĩa là "giá trị tại địa chỉ được chỉ bởi".

std :: cout << * ptr; // in ra giá trị tại địa chỉ

Bạn có thể giải thích ngắn gọn rằng ' ' là một 'toán tử' trả về các kết quả khác nhau với các loại đối tượng khác nhau. Khi được sử dụng với một con trỏ, toán tử '' không có nghĩa là "nhân với" nữa.

Nó giúp vẽ sơ đồ cho thấy một biến có tên và giá trị như thế nào và một con trỏ có địa chỉ (tên) và một giá trị và cho thấy giá trị của con trỏ sẽ là địa chỉ của int.


-1

Một con trỏ chỉ là một biến được sử dụng để lưu trữ địa chỉ.

Bộ nhớ trong máy tính được tạo thành từ các byte (Một byte bao gồm 8 bit) được sắp xếp theo thứ tự. Mỗi byte có một số được liên kết với nó giống như chỉ mục hoặc chỉ mục trong một mảng, được gọi là địa chỉ của byte. Địa chỉ của byte bắt đầu từ 0 đến một nhỏ hơn kích thước bộ nhớ. Ví dụ: trong 64 MB RAM, có 64 * 2 ^ 20 = 67108864 byte. Do đó, địa chỉ của các byte này sẽ bắt đầu từ 0 đến 67108863.

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

Hãy xem điều gì xảy ra khi bạn khai báo một biến.

dấu int;

Như chúng ta biết một int chiếm 4 byte dữ liệu (giả sử chúng ta đang sử dụng trình biên dịch 32 bit), vì vậy trình biên dịch dự trữ 4 byte liên tiếp từ bộ nhớ để lưu trữ một giá trị nguyên. Địa chỉ của byte đầu tiên trong 4 byte được phân bổ được gọi là địa chỉ của các dấu biến. Giả sử địa chỉ của 4 byte liên tiếp là 5004, 5005, 5006 và 5007 thì địa chỉ của các dấu biến sẽ là 5004. nhập mô tả hình ảnh ở đây

Khai báo biến con trỏ

Như đã nói, một con trỏ là một biến lưu trữ một địa chỉ bộ nhớ. Cũng giống như bất kỳ biến nào khác, trước tiên bạn cần khai báo một biến con trỏ trước khi bạn có thể sử dụng nó. Đây là cách bạn có thể khai báo một biến con trỏ.

Cú pháp: data_type *pointer_name;

data_type là loại con trỏ (còn được gọi là loại cơ sở của con trỏ). con trỏ tên là tên của biến, có thể là bất kỳ định danh C hợp lệ nào.

Hãy lấy một số ví dụ:

int *ip;

float *fp;

int * ip có nghĩa là ip là một biến con trỏ có khả năng trỏ đến các biến có kiểu int. Nói cách khác, một ip biến con trỏ chỉ có thể lưu trữ địa chỉ của các biến kiểu int. Tương tự, biến con trỏ fp chỉ có thể lưu địa chỉ của một biến kiểu float. Loại biến (còn được gọi là loại cơ sở) ip là một con trỏ tới int và loại fp là một con trỏ để nổi. Một biến con trỏ của kiểu con trỏ tới int có thể được biểu diễn một cách tượng trưng là (int *). Tương tự, một biến con trỏ của kiểu con trỏ tới float có thể được biểu diễn dưới dạng (float *)

Sau khi khai báo một biến con trỏ, bước tiếp theo là gán một số địa chỉ bộ nhớ hợp lệ cho nó. Bạn không bao giờ nên sử dụng biến con trỏ mà không gán một số địa chỉ bộ nhớ hợp lệ cho nó, bởi vì ngay sau khi khai báo, nó chứa giá trị rác và nó có thể được trỏ đến bất kỳ đâu trong bộ nhớ. Việc sử dụng một con trỏ không được gán có thể cho kết quả không thể đoán trước. Nó thậm chí có thể khiến chương trình bị sập.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Nguồn: thecguru cho đến nay là lời giải thích đơn giản nhưng chi tiết nhất mà tôi từng tìm thấy.

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.