C#引用和指针有什么区别?

95

我不太理解C#引用和指针之间的区别。它们都指向内存中的位置,对吧?我唯一能想到的区别是,指针不如引用聪明,不能指向堆上的任何内容,免于垃圾回收,并且只能引用结构体或基本类型。

我问这个问题的原因之一是,有一种看法认为人们需要很好地理解指针(来自C语言),才能成为一个好的程序员。许多学习高级语言的人都错过了这一点,因此他们有这种弱点。

我就是不明白指针有什么复杂的?它基本上只是指向内存中的一个位置,对吧?它可以返回其位置并直接与该位置的对象交互?

我错过了重要的一点吗?


1
简短回答是,是的,您已经错过了一些相当重要的东西,这就是“……人们需要理解指针”的原因。提示:C#不是唯一的编程语言。 - jdigital
10个回答

149

指针和引用之间存在微小但极其重要的区别。指针指向内存中的一个位置,而引用指向内存中的一个对象。指针在类型安全方面不具备保障,因为你无法保证它们指向的内存的正确性。

以以下代码为例:

int* p1 = GetAPointer();

在类型安全方面,GetAPointer必须返回与int*兼容的类型。但是仍然不能保证*p1实际上将指向int。它可能是char、double或仅是指向随机内存的指针。

但是,引用指向特定对象。对象可以在内存中移动,但引用不会失效(除非使用不安全代码)。在这方面,引用比指针更安全。

string str = GetAString();
在这种情况下,str有两种状态:1)它指向空对象,因此为null;2)它指向一个有效的字符串。就是这样。CLR保证这种情况存在。它不会对指针进行此类保证。

57

C# 引用可以被垃圾收集器重定位,但普通指针是静态的。因此,在获取数组元素的指针时,我们使用 fixed 关键字来防止其被移动。

编辑:从概念上讲,是的。它们基本上是相同的。


有没有其他命令可以防止C#引用被GC移动其所引用的对象? - Richard
哦,抱歉,我以为是其他东西,因为帖子提到了指针。 - Richard
2
是的,一个 GCHandle.Alloc 或者 Marshal.AllocHGlobal(超出 fixed) - ctacke
在C#中已经修复了,而在C++/CLI中则使用了pin_ptr。 - Mehrdad Afshari
1
Marshal.AllocHGlobal 不会在托管堆中分配内存,因此它不受垃圾回收的影响。 - Mehrdad Afshari

15

引用是一个“抽象”的指针:您不能对引用进行算术运算,也不能使用其值进行任何低级技巧。


10
引用和指针的主要区别在于,指针是一组位,其内容仅在被活跃地用作指针时才有意义,而引用不仅封装了一组位,还包括一些元数据,以让底层框架知道其存在。如果存在指向内存中某个对象的指针,但该对象已被删除而指针未被清除,则除非尝试访问其指向的内存,否则指针的持续存在不会造成任何损害。如果没有尝试使用指针,则不存在关于其存在的任何问题。相比之下,基于引用的框架如.NET或JVM要求系统始终能够识别每个现有的对象引用,并且每个现有的对象引用必须始终为null或者标识其正确类型的对象。
请注意,每个对象引用实际上封装了两种类型的信息:(1)它所标识的对象的字段内容,和(2)指向同一对象的其他引用的集合。虽然没有机制可以快速地确定到一个对象存在哪些引用,但是存在到一个对象的其他引用的集合通常是引用封装的最重要的东西(特别是当像锁标记这样的类型Object被用作东西时)。虽然系统为每个对象保留了一些位数据以供GetHashCode使用,但对象除了存在到它们的引用集合外没有真正的身份。如果X持有一个对象的唯一引用,将X替换为具有相同字段内容的新对象的引用将没有可识别的影响,甚至连这种影响也不能保证。

6
指针指向内存地址空间中的位置,引用指向数据结构。数据结构会被垃圾回收器(用于压缩内存空间)不时地移动(好吧,不是那么频繁),此外,像你所说的,没有引用的数据结构会在一段时间后被垃圾回收。
此外,指针只能在不安全的上下文中使用。

6
首先,我认为你需要在语义上定义一个“指针”。你是指你可以使用fixed在不安全的代码中创建的指针吗?你是指从本机调用或Marshal.AllocHGlobal获取的IntPtr吗?你是指GCHandle吗?它们实质上都是相同的东西-存储某个东西(类、数字、结构等)的内存地址的表示。顺便说一句,它们肯定可以在堆上。
指针(以上所有版本)是固定的项。GC不知道该地址上有什么,因此无法管理对象的内存或生命周期。这意味着您失去了垃圾收集系统的所有好处。您必须手动管理对象内存,并且可能会发生泄漏。
另一方面,引用基本上是GC知道的“托管指针”。它仍然是对象的地址,但现在GC知道目标的详细信息,因此它可以移动它,进行压缩,完成,处理和所有其他很好的托管环境所做的事情。
实际上,主要区别在于您如何以及为什么使用它们。在托管语言中的绝大多数情况下,您将使用对象引用。指针变得方便,用于执行交互操作和极少数需要快速处理的情况。
编辑:实际上,这里有一个很好的例子,说明何时可能在托管代码中使用“指针”-在这种情况下,它是一个GCHandle,但完全可以使用AllocHGlobal或通过在字节数组或结构上使用fixed来完成相同的操作。我倾向于使用GCHandle,因为它对我来说更像“.NET”。

这里可能有一个小问题,也许你不应该在这里说“托管指针”--即使用引号括起来--因为它与IL中的对象引用是完全不同的东西。虽然C++/CLI中有托管指针的语法,但它们通常无法从C#中访问。在IL中,它们是通过(i.e.) ldloca和ldarga指令获得的。 - Glenn Slayden

6
我认为开发人员理解指针的概念——即理解间接引用是很重要的,但这并不意味着他们必须使用指针。同样重要的是要理解引用的概念与指针的概念略有不同,尽管只是微妙的区别,但引用的实现几乎总是一个指针。
也就是说,保存引用的变量只是一个指向对象的指针大小的内存块。然而,这个变量不能像指针变量那样使用。在C#(以及C、C++等语言)中,指针可以像数组一样索引,但引用却不能。在C#中,垃圾回收器跟踪引用,指针则不能。在C++中,指针可以重新分配,而引用则不能。从语法和语义上看,指针和引用有相当大的不同,但从机械上看,它们是相同的。

数组的概念听起来很有趣,这基本上是在你无法使用引用时,可以告诉指针偏移内存位置,就像使用数组一样。我想不出什么时候会有用,但还是很有趣的。 - Richard
如果p是一个int(指向int的指针),那么(p + 1)是由p + 4个字节(int的大小)标识的地址。而p [1]与(p + 1)相同(即它“取消引用”了p之后4个字节的地址)。相比之下,在C#中,使用数组引用[]运算符执行函数调用。 - P Daddy

5

指针可以指向应用程序地址空间中的任何字节。 引用受.NET环境严格限制、控制和管理。


2

与指针相比,引用的最大优点是更加简单易读。当你简化某些东西时,通常会使其更易于使用,但代价是失去了低级别操作所具有的灵活性和控制(正如其他人所提到的)。

指针经常因为“丑陋”而受到批评。

class* myClass = new class();

现在每次使用它时,您都需要先取消引用它,方法是

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

尽管使用指针作为参数会降低可读性并增加复杂性,但人们仍然需要经常使用它来修改实际对象(而不是传递值),以及获得不必复制大型对象的性能优势。
对我来说,这就是引用首先被“创造”的原因,以提供与指针相同的好处,但没有所有那些指针语法。现在,您可以传递实际对象(而不仅仅是其值),并且您有一种更易读、正常的与对象交互的方式。
MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

C++中的引用与C# / Java中的引用不同,一旦将值分配给它,就不能重新分配它(并且必须在声明时分配)。这与使用const指针(不能重新指向另一个对象的指针)相同。
Java和C#是非常高级的现代语言,清理了多年来在C / C++中积累的许多混乱,而指针无疑是需要“清理”的其中之一。
至于你关于知道指针使你成为更强大的程序员的评论,这在大多数情况下是正确的。如果您知道“如何”工作,而不仅仅是使用它而不知道,我会说这通常可以给您带来优势。优势的大小总是会有所变化。毕竟,使用不知道如何实现的东西是面向对象编程和接口的许多美妙之一。
在这个特定的示例中,了解指针对于引用有什么帮助?了解C#引用不是对象本身,而是指向对象的重要概念。
#1:您不是按值传递 首先,当您使用指针时,您知道指针仅保存一个地址,就是这样。变量本身几乎为空,这就是将其作为参数传递的原因。除了性能提升外,您正在使用实际对象,因此所做的任何更改都不是临时的。
#2:多态/接口 当您有一个引用是接口类型并指向对象时,您只能调用该接口的方法,即使该对象可能具有许多其他功能。对象也可以以不同方式实现相同的方法。
如果您充分理解这些概念,那么我认为您没有使用指针会错过太多内容。 C ++通常被用作学习编程的语言,因为有时候弄脏手很好。此外,与较低级别的方面一起工作会使您欣赏现代语言的舒适性。我从C ++开始,现在是C#程序员,我确实觉得使用原始指针帮助我更好地了解了底层发生了什么。
我不认为每个人都需要从指针开始,但重要的是他们理解为什么使用引用而不是值类型,最好的方法是查看其祖先指针。

1
就我个人而言,如果大多数使用 . 的地方改用 ->,那么C#将会是一种更好的语言。但是 foo.bar(123) 与对静态方法 fooClass.bar(ref foo, 123) 的调用是同义的。这将允许诸如 myString.Append("George") 这样的事情;[这将修改 变量 myString],并更明显地显示了 myStruct.field = 3;myClassObject->field = 3;之间的差异。 - supercat

1
关于指针的复杂性,不在于它们是什么,而在于你可以用它们做什么。当你拥有一个指向指向指针的指针时,这才真正开始变得有趣。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接