为什么指针是许多新的,甚至是大学的C级或C++级学生的困惑的主导因素?是否有任何工具或思维过程可以帮助您理解指针在变量、函数和其他级别的工作方式?
有什么好的实践方法可以让某人达到"啊哈,我明白了"的水平,而不让他们陷入整体概念的泥潭?基本上,像演习一样的场景.
为什么指针是许多新的,甚至是大学的C级或C++级学生的困惑的主导因素?是否有任何工具或思维过程可以帮助您理解指针在变量、函数和其他级别的工作方式?
有什么好的实践方法可以让某人达到"啊哈,我明白了"的水平,而不让他们陷入整体概念的泥潭?基本上,像演习一样的场景.
指针是一个概念,对许多人来说,一开始可能会令人困惑,尤其是在复制指针值并仍然引用同一内存块时.
我发现最好的类比是把指针想像成一张纸,上面写着家庭地址,它引用的记忆挡路就是实际的房子.因此,各种操作都可以很容易地解释清楚.
我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释.我之所以 Select Delphi,是因为我的另一种主要编程语言C#不会以同样的方式显示内存泄漏之类的问题.
如果您只想学习指针的高级概念,那么您应该忽略下面说明中标有"内存布局"的部分.它们的目的是举例说明操作后的内存可能是什么样子,但它们本质上更低级.但是,为了准确解释缓冲区溢出是如何真正工作的,我添加了这些图表是很重要的.
Disclaimer: For all intents and purposes, this explanation and the example memory layouts are vastly simplified. There's more overhead and a lot more details you would need to know if you need to deal with memory on a low-level basis. However, for the intents of explaining memory and pointers, it is accurate enough.
假设下面使用的house类如下所示:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
初始化house对象时,给构造函数的名称会复制到私有字段FName中.它被定义为固定大小的数组是有原因的.
在内存中,会有一些与房屋分配相关的开销,下面我将这样说明:
---[ttttNNNNNNNNNN]--- ^ ^ | | | +- the FName array | +- overhead
"tttt"区域是开销,对于各种类型的运行时和语言,比如8或12字节,通常会有更多的开销.除了内存分配器或核心系统 routine 之外,存储在该区域中的任何值都不能被任何更改,否则可能会导致程序崩溃.
Allocate memory
找一位企业家来建造你的房子,并给你房子的地址.与现实世界不同的是,内存分配不能被告知分配到哪里,但会找到一个有足够空间的合适位置,并将地址报告给分配的内存.
换句话说,企业家会 Select 地点.
THouse.Create('My house');
内存布局:
---[ttttNNNNNNNNNN]--- 1234My house
Keep a variable with the address个
把你新家的地址写在一张纸上.这篇论文将作为你家的参考资料.没有这张纸,你就迷路了,找不到房子,除非你已经在里面了.
var
h: THouse;
begin
h := THouse.Create('My house');
...
内存布局:
h v ---[ttttNNNNNNNNNN]--- 1234My house
Copy pointer value
把地址写在一张新纸上就行了.你现在有两张纸可以把你带到同一所房子,而不是两个分开的房子.任何试图按照一张纸上的地址,重新排列那栋房子的家具的try ,都会让人觉得the other house件家具是以同样的方式修改的,除非你能明确地发现它实际上只是一栋房子.
这通常是我向人们解释最困难的概念,两个指针并不意味着两个对象或内存块.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1 v ---[ttttNNNNNNNNNN]--- 1234My house ^ h2
Freeing the memory个
拆毁房子.如果你愿意的话,你可以在以后重新使用这张纸来创建一个新的地址,或者清除它,忘记已经不存在的房子的地址.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
在这里,我首先建造了这所房子,并得到了它的地址.然后我对房子做些什么(使用它,代码,留给读者作为练习),然后我释放它.最后,我从变量中清除地址.
内存布局:
h <--+ v +- before free ---[ttttNNNNNNNNNN]--- | 1234My house <--+ h (now points nowhere) <--+ +- after free ---------------------- | (note, memory might still xx34My house <--+ contain some data)
Dangling pointers
你让你的创业者毁掉房子,但你忘了把地址从纸上抹掉.当你稍后看这张纸时,你忘记了房子已经不在了,于是go 拜访它,结果失败了(另请参见下面关于无效引用的部分).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
打完电话后用h
打到.Free
,might就行了,但那纯粹是运气.最有可能的情况是,在客户位置,关键操作进行到一半时出现故障.
h <--+ v +- before free ---[ttttNNNNNNNNNN]--- | 1234My house <--+ h <--+ v +- after free ---------------------- | xx34My house <--+
如您所见,h仍然指向内存中的数据残留物,但是 因为它可能不完整,所以像以前一样使用它可能会失败.
Memory leak
你丢了那张纸,找不到房子了.然而,这座房子仍然矗立在某个地方,当你后来想要建造一座新房子时,你不能重复使用那个地方.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
在这里,我们用新房子的地址重写了h
变量的内容,但旧的仍然存在...在某处在这个密码之后,没有办法到达那所房子,它将被留在原地.换句话说,分配的内存将一直保持分配状态,直到应用程序关闭,此时操作系统会将其拆除.
首次分配后的内存布局:
h v ---[ttttNNNNNNNNNN]--- 1234My house
第二次分配后的内存布局:
h v ---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN] 1234My house 5678My house
获取此方法的一个更常见的方法是忘记释放某些内容,而不是像上面那样覆盖它.用德尔菲术语来说,这将通过以下方法实现:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
在这个方法执行之后,在我们的变量中没有房子的地址存在,但是房子仍然在外面.
内存布局:
h <--+ v +- before losing pointer ---[ttttNNNNNNNNNN]--- | 1234My house <--+ h (now points nowhere) <--+ +- after losing pointer ---[ttttNNNNNNNNNN]--- | 1234My house <--+
如您所见,旧数据将完好无损地保留在内存中,并且不会 由内存分配器重用.分配器跟踪哪些 内存区域已被使用,并且不会重复使用它们,除非您 放了它.
Freeing the memory but keeping a (now invalid) reference
拆除房子,擦掉其中一张纸,但你还有另一张纸,上面有旧地址,当你go 那个地址时,你不会找到一座房子,但你可能会找到类似于其中一座的废墟的东西.
也许你甚至会找到一所房子,但它并不是你最初得到的地址,因此任何试图把它当作属于你的使用都可能会失败得很惨.
有时你甚至会发现附近的一个地址有一栋相当大的房子,占据了三个地址(主街1-3),而你的地址就在房子的中间.任何试图将三地址大房子的这一部分视为一个单独的小房子的try ,也可能会以可怕的失败告终.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
这里的房子被拆除了,通过h1
年的参考,虽然h1
年也被清除,但h2
年仍然有旧的,过时的,地址.进入不再矗立的房屋可能会或可能不会起作用.
这是上方悬挂指针的变体.查看其内存布局.
Buffer overrun
你搬进房子里的东西超过了你的承受能力,溢出到邻居的房子或院子里.当隔壁房子的主人稍后回家时,他会发现各种各样的东西,他会认为是他自己的.
这就是我 Select 固定大小数组的原因.要设置stage,假设 由于某种原因,我们分配的第二套房子将放在 记忆中的第一个.换句话说,第二宫将有一个较低的 而不是第一个地址.而且,它们是紧挨着分配的.
因此,该代码:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
首次分配后的内存布局:
h1 v -----------------------[ttttNNNNNNNNNN] 5678My house
第二次分配后的内存布局:
h2 h1 v v ---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN] 1234My other house somewhereouse ^---+--^ | +- overwritten
最常导致崩溃的部分是当您覆盖重要部分时 您存储的数据中确实不应该随意更改的数据.例如 H1-House的部分名称被更改可能不是问题, 在使程序崩溃,但覆盖 当您try 使用损坏的对象时,对象很可能会崩溃, 就像覆盖存储到的链接一样 对象中的其他对象.
Linked lists
当你沿着一张纸上的地址走,你就到了一所房子,在那所房子里有另一张纸,上面有一个新的地址,链中的下一个房子,依此类推.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
在这里,我们创建了一个从我们的家到我们的小屋的链接.我们可以顺着链条走,直到一栋房子没有NextHouse
个参考号,这意味着它是最后一个.要访问我们所有的房子,我们可以使用以下代码:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
内存布局(添加NextHouse作为对象中的链接,用
h1 h2 v v ---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL] 1234Home + 5678Cabin + | ^ | +--------+ * (no link)
In basic terms, what is a memory address?
内存地址基本上只是一个数字.如果你想到记忆
所以这个内存布局:
h1 h2 v v ---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN] 1234My house 5678My house
可能有以下两个地址(最左边的是地址0):
这意味着我们上面的链接列表实际上可能是这样的:
h1 (=4) h2 (=28) v v ---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL] 1234Home 0028 5678Cabin 0000 | ^ | +--------+ * (no link)
通常将"无处指向"的地址存储为零地址.
In basic terms, what is a pointer?个
指针只是一个保存内存地址的变量.您通常可以询问编程人员