你如何解释C#/ Java开发人员的C ++指针?

我是一名试图学习C ++的C#/ Java开发人员。 当我尝试学习指针的概念时,我很惊讶以前我必须处理这个概念。 如何仅使用.NET或Java开发人员熟悉的概念来解释指针? 我是否真的从来没有处理过这个问题,它只是隐藏在我身上,还是我一直都在使用它而没有把它当作它?

C ++中的Java对象

Java对象相当于C ++共享指针。

C ++指针就像没有内置垃圾收集的Java对象。

C ++对象。

C ++有三种分配对象的方法:

  • 静态存储持续时间对象
    • 这些是在启动时(主要之前)创建的,并在主要退出后死亡。
      对此有一些技术性的警告,但这是基础知识。
  • 自动存储持续时间对象
    • 这些是在超出范围时声明和销毁时创建的。
      我相信这些就像C#结构
  • Dynamic Storage Duration对象

    • 这些是通过new和最接近C#/ Java对象( AKA指针 )创建的
      技术指针需要通过delete手动销毁。 但这被认为是不好的做法,在正常情况下,它们被放在控制其寿命的自动存储持续时间对象(通常称为智能指针)中。 当智能指针超出范围时,它将被销毁,并且其析构函数可以在指针上调用delete 。 智能指针可以作为细粒垃圾收集器。

      最接近Java的是shared_ptr,这是一个智能指针,它保持指针用户数的计数,并在没有人使用时删除它。

你在C#中一直“使用指针”,它只是对你隐藏。

我认为解决问题的最好方法是考虑计算机的工作方式。 忘掉.NET的所有奇特之处:你有内存,它只保存字节值,还有处理器,它只对这些字节值做了些事情。

给定变量的值存储在内存中,因此与内存地址相关联。 编译器不是必须一直使用内存地址,而是让您从中读取并使用名称写入。

此外,您可以选择将值解释为您希望在其中查找其他值的内存地址。 这是一个指针。

例如,假设我们的内存包含以下值:

 Address [0] [1] [2] [3] [4] [5] [6] [7] Data 5 3 1 8 2 7 9 4 

让我们定义一个变量x ,编译器选择将其置于地址2.可以看出x的值是1。

现在让我们定义一个指针, p ,编译器选择将其放在地址7处p值为4p 指向的值是地址4处的值,即值2 。 获取该值称为解除引用

需要注意的一个重要概念是,就内存而言,没有类型的东西:只有字节值。 您可以根据需要选择解释这些字节值。 例如,取消引用char指针只会获得表示ASCII码的1个字节,但取消引用int指针可能会得到4个字节,构成32位值。

查看另一个示例,您可以使用以下代码在C中创建一个字符串:

 char *str = "hello, world!"; 

它的作用如下:

  • 在我们的堆栈帧中放置一些变量的字节,我们称之为str
  • 该变量将保存一个内存地址,我们希望将其解释为一个字符。
  • 将字符串的第一个字符的地址复制到变量中。
  • (字符串“hello,world!”将存储在可执行文件中,因此在程序加载时将加载到内存中)

如果你要查看str的值,你会得到一个整数值,它表示字符串第一个字符的地址。 但是,如果我们取消引用指针(即,查看它指向的内容),我们将得到字母’h’。

如果你增加指针, str++; ,它现在将指向下一个字符。 请注意, 缩放指针算术。 这意味着当你对指针进行算术运算时,效果会乘以它认为指向的类型的大小。 因此,假设您的系统中int为4字节宽,以下代码实际上会向指针添加4:

 int *ptr = get_me_an_int_ptr(); ptr++; 

如果你最终走过字符串的末尾,那就不知道你会指出什么; 但是你的程序仍将尽职尽责地将其解释为一个字符,即使该值实际上应该代表一个整数。 您可能正在尝试访问未分配给您的程序的内存,但您的程序将被操作系统杀死。

最后一个有用的提示:数组和指针算术是一回事,它只是语法糖。 如果你有一个变量char *array ,那么

 array[5] 

完全等同于

 *(array + 5) 

指针是对象的地址。

嗯,从技术上讲,指针是对象的地址。 指针对象是一个能够存储指针值的对象(变量,可以根据需要调用它),就像int对象是能够存储整数值的对象一样。

[C ++中的“对象”包括类类型的实例,以及内置类型(和数组等)的实例。 一个int变量是C ++中的一个对象,如果你不喜欢那么那么难过,因为你必须忍受它;-)]

指针也有静态类型,告诉程序员和编译器它是什么类型的对象的地址。

什么是地址? 它是带有数字和字母的0x事件之一,您有时可能会在调试器中看到它 。 对于大多数体系结构,我们可以将内存(RAM,过度简化)视为一个大的字节序列。 对象存储在存储器区域中。 对象的地址是该对象占用的第一个字节的索引。 因此,如果您有地址,硬件可以获得存储在对象中的任何内容。

使用指针的后果在某些方面与在Java和C#中使用引用的后果相同 – 您间接地引用了一个对象。 因此,您可以在函数调用之间复制指针值,而无需复制整个对象。 您可以通过一个指针更改对象,使用指向同一对象的其他代码位将看到更改。 与许多不同的对象相比,共享不可变对象可以节省内存,这些对象都拥有自己所需的相同数据的副本。

C ++也有一些称为“引用”的东西,它们与间接共享这些属性,但与Java中的引用不同。 它们也不是C ++中的指针(这是另一个问题 )。

“我对以前必须处理这个概念的想法感到震惊”

不必要。 语言可能在function上是等效的,因为它们都计算出图灵机可以计算的相同function,但这并不意味着编程中的每个有价值的概念都明确地存在于每种语言中。

但是,如果你想用Java或C#模拟C内存模型,我想你会创建一个非常大的字节数组。 指针将是数组中的索引。 从指针加载int将涉及从该索引开始占用4个字节,并将它们乘以256的连续幂以获得总数(就像从Java中的字节流反序列化int时所发生的那样)。 如果这听起来像是一件荒谬的事情,那么这是因为你之前没有处理过这个概念,但是这就是你的硬件一直在做的回应你的Java和C#代码[*]。 如果您没有注意到它,那么这是因为这些语言很好地创建了其他抽象供您使用。

字面上,Java语言最接近“对象的地址”是java.lang.Object中的默认hashCode ,根据文档,“通常通过将对象的内部地址转换为整数来实现”。 但在Java中,您无法使用对象的哈希码来访问该对象。 您当然不能在哈希码中添加或减少一个小数字,以便访问原始对象内或附近的内存。 你不能犯错误,你认为你的指针指向你想要它的对象,但实际上它指的是一些完全不相关的内存位置,你的值将要乱涂乱画。 在C ++中,你可以做所有这些事情。

[*]好吧,不是乘以和添加4个字节来获得int,甚至不是移位和ORing,而是从4个字节的内存中“加载”一个int。

C#中的引用与C ++中的指针行为相同,没有所有混乱的语法。

考虑以下C#代码:

 public class A { public int x; } public void AnotherFunc(A a) { ax = 2; } public void SomeFunc() { A a = new A(); ax = 1; AnotherFunc(a); // ax is now 2 } 

由于类是引用类型,我们知道我们将现有的A实例传递给AnotherFunc (与复制的值类型不同)。

在C ++中,我们使用指针将其显式化:

 class A { public: int x; }; void AnotherFunc(A* a) // notice we are pointing to an existing instance of A { a->x = 2; } void SomeFunc() { A a; ax = 1; AnotherFunc(&a); // ax is now 2 } 

“如何仅使用.NET或Java开发人员熟悉的概念来解释指针?”我建议有两个不同的事情需要学习。

第一个是如何使用指针和堆分配的内存来解决具体问题。 使用适当的样式,例如,使用shared_ptr <>,这可以以类似于Java的方式完成。 shared_ptr <>与Java对象句柄有很多共同之处。

其次,我建议指针通常是一个基本上较低级别的概念,Java,以及在较小程度上C#故意隐藏。 使用C ++编程而不移动到该级别将保证一系列问题。 您需要根据底层内存布局进行思考,并将指针视为指向特定存储块的字面指针。

试图从更高概念的角度来理解这个较低层次将是一条奇怪的道路。

得到两张大幅方格纸,一些剪刀和一位朋友来帮助你。

纸张上的每个正方形代表一个字节。

一张是堆栈。

另一张是堆。 把堆给你的朋友 – 他是记忆经理。

你将假装是一个C程序,你需要一些记忆。 运行程序时,从堆栈和堆中删除块以表示内存分配。

准备?

 void main() { int a; /* Take four bytes from the stack. */ int *b = malloc(sizeof(int)); /* Take four bytes from the heap. */ a = 1; /* Write on your first little bit of graph paper, WRITE IT! */ *b = 2; /* Get writing (on the other bit of paper) */ b = malloc(sizeof(int)); /* Take another four bytes from the heap. Throw the first 'b' away. Do NOT give it back to your friend */ free(b); /* Give the four bytes back to your friend */ *b = 3; /* Your friend must now kill you and bury the body */ } /* Give back the four bytes that were 'a' */ 

尝试一些更复杂的程序。

解释堆栈和堆之间的区别以及对象的位置。

结构体(包括C ++和C#)等值类型会进入堆栈。 引用类型(类实例)放在堆上。 指针(或引用)指向该特定实例的堆上的内存位置。

参考类型是关键词。 在C ++中使用指针就像在C#中使用ref关键字一样。

托管应用程序可以轻松处理这些内容,因此.NET开发人员可以免除麻烦和困惑。 很高兴我不再做C了。

对我而言,关键是了解记忆的运作方式。 变量存储在内存中。 可以将变量放入内存的位置编号。 指针是保存此数字的变量。

任何了解类和结构之间语义差异的C#程序员都应该能够理解指针。 即,在价值与参考语义(用.NET术语)方面的解释应该得到重点; 我不会通过尝试用ref (或out )来解释复杂的事情。

在C#中,对类的所有引用大致相当于C ++世界中的指针。 对于值类型(结构,整数等),情况并非如此。

C#:

 void func1(string parameter) void func2(int parameter) 

C ++:

 void func1(string* parameter) void func2(int parameter) 

在C#中使用ref关键字传递参数等同于在C ++中通过引用传递参数。

C#:

 void func1(ref string parameter) void func2(ref int parameter) 

C ++:

 void func1((string*)& parameter) void func2(int& parameter) 

如果参数是一个类,那就像通过引用传递指针一样。