Sự khác biệt giữa Tham chiếu C # và Con trỏ là gì?


85

Tôi không hoàn toàn hiểu sự khác biệt giữa tham chiếu C # và con trỏ. Cả hai đều chỉ đến một nơi trong ký ức phải không? Sự khác biệt duy nhất tôi có thể tìm ra là con trỏ không thông minh, không thể trỏ đến bất kỳ thứ gì trên heap, được miễn thu gom rác và chỉ có thể tham chiếu cấu trúc hoặc kiểu cơ sở.

Một trong những lý do tôi hỏi là có một nhận thức rằng mọi người cần phải hiểu rõ về con trỏ (từ C, tôi đoán vậy) để trở thành một lập trình viên giỏi. Rất nhiều người học ngôn ngữ cấp cao hơn bỏ lỡ điều này và do đó có điểm yếu này.

Tôi chỉ không hiểu những gì quá phức tạp về một con trỏ? Về cơ bản nó chỉ là một tham chiếu đến một nơi trong bộ nhớ phải không? Nó có thể trả lại vị trí của nó và tương tác trực tiếp với đối tượng ở vị trí đó?

Tôi đã bỏ lỡ một điểm lớn?


1
Câu trả lời ngắn gọn là có, bạn đã bỏ lỡ một điều gì đó quan trọng một cách hợp lý, và đó là lý do cho "... nhận thức rằng mọi người cần hiểu những con số". Gợi ý: C # không phải là ngôn ngữ duy nhất hiện có.
jdigital

Câu trả lời:


50

Các tham chiếu C # có thể và sẽ được di dời bởi bộ thu gom rác nhưng các con trỏ bình thường là tĩnh. Đây là lý do tại sao chúng tôi sử dụng fixedtừ khóa khi mua một con trỏ đến một phần tử mảng, để ngăn nó di chuyển.

CHỈNH SỬA: Về mặt khái niệm, có. Chúng ít nhiều giống nhau.


Không có lệnh nào khác ngăn tham chiếu C # có đối tượng mà tham chiếu đó di chuyển bởi GC?
Richard

Xin lỗi, tôi nghĩ đó là một cái gì đó khác vì bài đăng đề cập đến một con trỏ.
Richard

Vâng, một GCHandle.Alloc, hoặc một Marshal.AllocHGlobal (ngoài thời gian cố định)
ctacke

Nó cố định trong C #, pin_ptr trong C ++ / CLI
Mehrdad Afshari

Marshal.AllocHGlobal sẽ hoàn toàn không phân bổ bộ nhớ trong đống được quản lý và đương nhiên nó không bị thu gom rác.
Mehrdad Afshari

130

Có một sự khác biệt nhỏ, nhưng cực kỳ quan trọng, giữa một con trỏ và một tham chiếu. Một con trỏ trỏ đến một vị trí trong bộ nhớ trong khi một tham chiếu trỏ đến một đối tượng trong bộ nhớ. Con trỏ không phải là "loại an toàn" theo nghĩa là bạn không thể đảm bảo tính chính xác của bộ nhớ mà chúng trỏ vào.

Lấy ví dụ như đoạn mã sau

int* p1 = GetAPointer();

Đây là kiểu an toàn theo nghĩa GetAPointer phải trả về kiểu tương thích với int *. Tuy nhiên, vẫn không có gì đảm bảo rằng * p1 sẽ thực sự trỏ đến một int. Nó có thể là một char, double hoặc chỉ một con trỏ vào bộ nhớ ngẫu nhiên.

Tuy nhiên, một tham chiếu trỏ đến một đối tượng cụ thể. Các đối tượng có thể được di chuyển trong bộ nhớ nhưng tham chiếu không thể bị vô hiệu (trừ khi bạn sử dụng mã không an toàn). Về mặt này, tham chiếu an toàn hơn nhiều so với con trỏ.

string str = GetAString();

Trong trường hợp này str có một trong hai trạng thái 1) nó trỏ đến không có đối tượng nào và do đó là null hoặc 2) nó trỏ đến một chuỗi hợp lệ. Đó là nó. CLR đảm bảo đây là trường hợp. Nó không thể và sẽ không cho một con trỏ.


14
Lời giải thích tuyệt vời.
iTayb

13

Tham chiếu là một con trỏ "trừu tượng": bạn không thể tính toán số học với một tham chiếu và bạn không thể chơi bất kỳ thủ thuật cấp thấp nào với giá trị của nó.


8

Sự khác biệt chính giữa tham chiếu và con trỏ là con trỏ là tập hợp các bit mà nội dung của nó chỉ quan trọng khi nó đang được sử dụng tích cực như một con trỏ, trong khi một tham chiếu không chỉ gói gọn một tập hợp các bit mà còn một số siêu dữ liệu giữ khung cơ bản được thông báo về sự tồn tại của nó. Nếu một con trỏ tồn tại đối với một số đối tượng trong bộ nhớ và đối tượng đó bị xóa nhưng con trỏ không bị xóa, sự tồn tại tiếp tục của con trỏ sẽ không gây ra bất kỳ tác hại nào trừ khi hoặc cho đến khi cố gắng truy cập vào bộ nhớ mà nó trỏ tới. Nếu không cố gắng sử dụng con trỏ, sẽ không có gì quan tâm đến sự tồn tại của nó. Ngược lại, các khuôn khổ dựa trên tham chiếu như .NET hoặc JVM yêu cầu hệ thống luôn có thể xác định mọi tham chiếu đối tượng đang tồn tại và mọi tham chiếu đối tượng đang tồn tại phải luônnull hoặc xác định một đối tượng thuộc loại thích hợp của nó.

Lưu ý rằng mỗi tham chiếu đối tượng thực sự đóng gói hai loại thông tin: (1) nội dung trường của đối tượng mà nó xác định, và (2) tập hợp các tham chiếu khác đến cùng một đối tượng. Mặc dù không có bất kỳ cơ chế nào mà hệ thống có thể nhanh chóng xác định tất cả các tham chiếu tồn tại cho một đối tượng, tập hợp các tham chiếu khác tồn tại cho một đối tượng thường có thể là thứ quan trọng nhất được bao bọc bởi một tham chiếu (điều này đặc biệt đúng khi những thứ thuộc loại Objectđược sử dụng như những thứ như mã thông báo khóa). Mặc dù hệ thống giữ một vài bit dữ liệu cho mỗi đối tượng để sử dụng GetHashCode, các đối tượng không có danh tính thực ngoài tập hợp các tham chiếu tồn tại với chúng. Nếu Xgiữ tham chiếu duy nhất còn tồn tại đến một đối tượng, thay thếXvới một tham chiếu đến một đối tượng mới có cùng nội dung trường sẽ không có tác dụng nhận dạng nào ngoại trừ việc thay đổi các bit được trả về GetHashCode()và thậm chí tác động đó không được đảm bảo.


5

Con trỏ trỏ đến một vị trí trong không gian địa chỉ bộ nhớ. Các tham chiếu trỏ đến một cấu trúc dữ liệu. Các cấu trúc dữ liệu đều di chuyển mọi lúc (tốt, không phải thường xuyên, nhưng thỉnh thoảng) bởi bộ thu gom rác (để nén không gian bộ nhớ). Ngoài ra, như bạn đã nói, các cấu trúc dữ liệu không có tham chiếu sẽ được thu thập rác sau một thời gian.

Ngoài ra, con trỏ chỉ có thể sử dụng được trong ngữ cảnh không an toàn.


5

Tôi nghĩ rằng điều quan trọng là các nhà phát triển phải hiểu khái niệm về một con trỏ — nghĩa là, hiểu được hướng dẫn. Điều đó không có nghĩa là họ nhất thiết phải sử dụng con trỏ. Cũng cần hiểu rằng khái niệm tham chiếu khác với khái niệm con trỏ , mặc dù chỉ một cách tinh tế, nhưng việc triển khai một tham chiếu hầu như luôn luôn một con trỏ.

Có nghĩa là, một biến chứa một tham chiếu chỉ là một khối bộ nhớ có kích thước bằng con trỏ giữ một con trỏ đến đối tượng. Tuy nhiên, biến này không thể được sử dụng giống như cách mà một biến con trỏ có thể được sử dụng. Trong C # (và C, và C ++, ...), một con trỏ có thể được lập chỉ mục giống như một mảng, nhưng một tham chiếu thì không. Trong C #, một tham chiếu được theo dõi bởi bộ thu gom rác, một con trỏ không thể được. Trong C ++, một con trỏ có thể được gán lại, một tham chiếu không thể. Về mặt cú pháp và ngữ nghĩa, con trỏ và tham chiếu khá khác nhau, nhưng về mặt cơ học, chúng giống nhau.


Mảng nghe có vẻ thú vị, về cơ bản đó có phải là nơi bạn có thể yêu cầu con trỏ bù đắp vị trí bộ nhớ giống như một mảng trong khi bạn không thể làm điều này với một tham chiếu không? Không thể nghĩ khi nào điều đó sẽ hữu ích nhưng không kém phần thú vị.
Richard

Nếu p là một int * (một con trỏ tới một int), thì (p + 1) là địa chỉ được xác định bởi p + 4 byte (kích thước của một int). Và p [1] cũng giống như * (p + 1) (nghĩa là nó "bỏ tham chiếu" địa chỉ 4 byte trước p). Ngược lại, với tham chiếu mảng (trong C #), toán tử [] thực hiện một cuộc gọi hàm.
P Daddy

5

Đầu tiên, tôi nghĩ bạn cần xác định một "Con trỏ" trong huyết thanh học của mình. Ý của bạn là con trỏ bạn có thể tạo trong mã không an toàn với cố định ? Ý của bạn là IntPtr mà bạn có thể nhận được từ một cuộc gọi bản địa hoặc Marshal.AllocHGlobal ? Ý bạn là GCHandle ? Về cơ bản, tất cả đều giống nhau - đại diện cho địa chỉ bộ nhớ nơi lưu trữ thứ gì đó - có thể là lớp, số, cấu trúc, bất cứ thứ gì. Và đối với hồ sơ, chúng chắc chắn có thể ở trên đống.

Một con trỏ (tất cả các phiên bản trên) là một mục cố định. GC không biết có gì ở địa chỉ đó, và do đó không có khả năng quản lý bộ nhớ hoặc tuổi thọ của đối tượng. Điều đó có nghĩa là bạn mất tất cả các lợi ích của một hệ thống thu gom rác. Bạn phải quản lý thủ công bộ nhớ đối tượng và bạn có khả năng bị rò rỉ.

Mặt khác, một tham chiếu là một "con trỏ được quản lý" mà GC biết. Nó vẫn là địa chỉ của một đối tượng, nhưng bây giờ GC biết chi tiết về mục tiêu, vì vậy nó có thể di chuyển nó xung quanh, thực hiện các giao dịch, hoàn thiện, xử lý và tất cả những thứ hay ho khác mà môi trường được quản lý làm.

Sự khác biệt chính, thực sự, là ở cách thức và lý do bạn sử dụng chúng. Đối với phần lớn các trường hợp trong ngôn ngữ được quản lý, bạn sẽ sử dụng tham chiếu đối tượng. Con trỏ trở nên tiện dụng để thực hiện tương tác và hiếm khi cần làm việc thực sự nhanh chóng.

Chỉnh sửa: Trên thực tế, đây là một ví dụ điển hình về thời điểm bạn có thể sử dụng "con trỏ" trong mã được quản lý - trong trường hợp này đó là GCHandle, nhưng điều tương tự có thể đã được thực hiện với AllocHGlobal hoặc bằng cách sử dụng cố định trên một mảng byte hoặc cấu trúc. Tôi có xu hướng thích GCHandle becas vì nó cảm thấy ".NET" hơn đối với tôi.


Một phân minh nhỏ rằng có lẽ bạn không nên nói "con trỏ được quản lý" ở đây - ngay cả với dấu ngoặc kép đáng sợ - bởi vì đây là một cái gì đó khá khác với tham chiếu đối tượng, trong IL. Mặc dù có cú pháp cho con trỏ được quản lý trong C ++ / CLI, nhưng chúng thường không thể truy cập được từ C #. Trong IL, chúng được thu bằng (tức là) ldloca và ldarga.
Glenn Slayden

5

Một con trỏ có thể trỏ đến bất kỳ byte nào trong không gian địa chỉ của ứng dụng. Một tham chiếu bị ràng buộc chặt chẽ và được kiểm soát và quản lý bởi môi trường .NET.


1

Vấn đề về con trỏ khiến chúng hơi phức tạp không phải là chúng là gì, mà là bạn có thể làm gì với chúng. Và khi bạn có một con trỏ tới một con trỏ tới một con trỏ. Đó là khi nó thực sự bắt đầu vui vẻ.


1

Một trong những lợi ích lớn nhất của tham chiếu qua con trỏ là tính đơn giản và dễ đọc hơn. Như mọi khi, khi bạn đơn giản hóa thứ gì đó, bạn sẽ làm cho nó dễ sử dụng hơn nhưng với cái giá phải trả là sự linh hoạt và khả năng kiểm soát mà bạn có được với những thứ cấp thấp (như những người khác đã đề cập).

Con trỏ thường bị chỉ trích là 'xấu xí'.

class* myClass = new class();

Bây giờ mỗi khi bạn sử dụng nó, bạn phải tham khảo nó trước tiên bằng cách

myClass->Method() or (*myClass).Method()

Mặc dù mất đi một số khả năng đọc và tăng thêm độ phức tạp, mọi người vẫn cần sử dụng con trỏ thường xuyên làm tham số để bạn có thể sửa đổi đối tượng thực tế (thay vì chuyển theo giá trị) và để đạt được hiệu suất khi không phải sao chép các đối tượng lớn.

Đối với tôi, đây là lý do tại sao các tham chiếu được 'sinh ra' ngay từ đầu để cung cấp lợi ích tương tự như con trỏ nhưng không có tất cả cú pháp con trỏ đó. Bây giờ bạn có thể chuyển đối tượng thực tế (không chỉ giá trị của nó) VÀ bạn có một cách tương tác bình thường, dễ đọc hơn với đối tượng.

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

Tham chiếu C ++ khác với tham chiếu C # / Java ở chỗ khi bạn gán một giá trị cho nó, bạn không thể gán lại nó (và nó phải được gán khi nó được khai báo). Điều này cũng giống như việc sử dụng con trỏ const (một con trỏ không thể được trỏ lại đến đối tượng khác).

Java và C # là những ngôn ngữ hiện đại, cấp độ rất cao đã dọn dẹp rất nhiều mớ hỗn độn đã tích tụ trong C / C ++ qua nhiều năm và con trỏ chắc chắn là một trong những thứ cần được 'dọn dẹp'.

Theo như nhận xét của bạn về việc biết con trỏ giúp bạn trở thành một lập trình viên mạnh mẽ hơn, thì điều này đúng trong hầu hết các trường hợp. Nếu bạn biết 'cách nào đó' hoạt động thay vì chỉ sử dụng nó mà không biết, tôi sẽ nói rằng điều này thường có thể mang lại cho bạn một lợi thế. Bao nhiêu của một cạnh sẽ luôn thay đổi. Rốt cuộc, việc sử dụng một thứ gì đó mà không biết nó được thực hiện như thế nào là một trong những nét đẹp của OOP và Interfaces.

Trong ví dụ cụ thể này, những gì biết về con trỏ sẽ giúp bạn tham khảo? Hiểu rằng một tham chiếu C # KHÔNG phải là bản thân đối tượng mà trỏ đến đối tượng là một khái niệm rất quan trọng.

# 1: Bạn KHÔNG chuyển qua giá trị Tốt cho người mới bắt đầu khi bạn sử dụng một con trỏ, bạn biết rằng con trỏ chỉ chứa một địa chỉ, đó là nó. Bản thân biến gần như trống rỗng và đó là lý do tại sao nó rất hay để chuyển làm đối số. Ngoài việc đạt được hiệu suất, bạn đang làm việc với đối tượng thực tế nên bất kỳ thay đổi nào bạn thực hiện không phải là tạm thời

# 2: Đa hình / Giao diện Khi bạn có một tham chiếu là một kiểu giao diện và nó trỏ đến một đối tượng, bạn chỉ có thể gọi các phương thức của giao diện đó mặc dù đối tượng có thể có nhiều khả năng hơn. Các đối tượng cũng có thể triển khai các phương thức giống nhau theo cách khác nhau.

Nếu bạn hiểu rõ những khái niệm này thì tôi không nghĩ rằng bạn đang thiếu quá nhiều việc không sử dụng con trỏ. C ++ thường được sử dụng như một ngôn ngữ để học lập trình vì đôi khi bạn làm bẩn tay thì rất tốt. Ngoài ra, làm việc với các khía cạnh cấp thấp hơn khiến bạn đánh giá cao sự thoải mái của một ngôn ngữ hiện đại. Tôi bắt đầu với C ++ và bây giờ là một lập trình viên C # và tôi cảm thấy làm việc với con trỏ thô đã giúp tôi hiểu rõ hơn về những gì đang diễn ra.

Tôi không nghĩ rằng tất cả mọi người đều cần phải bắt đầu với con trỏ, nhưng điều quan trọng là họ hiểu tại sao các tham chiếu được sử dụng thay vì các kiểu giá trị và cách tốt nhất để hiểu điều đó là nhìn vào tổ tiên của nó, con trỏ.


1
Cá nhân tôi nghĩ C # sẽ là một ngôn ngữ tốt hơn nếu hầu hết các nơi sử dụng đều sử .dụng ->, nhưng foo.bar(123)đồng nghĩa với một lệnh gọi đến phương thức tĩnh fooClass.bar(ref foo, 123). Điều đó sẽ cho phép những thứ như myString.Append("George"); [sẽ sửa đổi biến myString ], và tạo ra sự khác biệt rõ ràng hơn về ý nghĩa giữa myStruct.field = 3;myClassObject->field = 3;.
supercat
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.