理解指针有什么障碍,克服它们有什么可以做的?

为什么C或C ++中许多新的,甚至是老年的大学水平的学生都指出这种混乱的主要原因? 是否有任何工具或思维过程帮助您理解指针在变量,函数和更高级别上的工作方式?

有什么好的做法可以使人们达到“啊,我明白了”的水平,而不会让他们陷入整体概念? 基本上,钻类似的情况。


指针是一个概念,对于许多人来说,最初可能会引起混淆,特别是当涉及到复制指针值并仍然引用相同的内存块时。

我发现最好的比喻是将指针视为一张纸上有一个房子地址的纸片,以及它引用的内存块作为实际的房子。 因此可以很容易地解释各种操作。

我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释。 我选择了Delphi,因为我的其他主要编程语言C#并没有以相同的方式显示内存泄漏等内容。

如果您只想学习指针的高级概念,那么您应该在下面的解释中忽略标记为“内存布局”的部分。 它们旨在举例说明操作之后内存的样子,但它们本质上更低级。 但是,为了准确解释缓冲区溢出如何真正起作用,重要的是我添加了这些图。

免责声明:对于所有意图和目的,此解释和示例内存布局大大简化。 有更多的开销和更多的细节,你需要知道你是否需要在低级别的基础上处理内存。 但是,对于解释内存和指针的意图来说,它足够准确。


假设下面使用的THouse类如下所示:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

初始化房屋对象时,赋予构造函数的名称将被复制到专用字段FName中。 有一个原因被定义为一个固定大小的数组。

在内存中,会有一些与房屋分配有关的开销,我将在下面说明这一点:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

“tttt”区域是开销,对于不同类型的运行时和语言,通常会有更多这样的区域,比如8或12个字节。 无论存储器分配器还是核心系统例程,任何值都不会被存储在这个区域中的任何值所改变,否则可能会导致程序崩溃。


分配内存

让一个企业家建造你的房子,并给你房子的地址。 与现实世界相反,内存分配无法告诉分配的位置,但会找到足够空间的合适位置,并将地址报告给分配的内存。

换句话说,企业家会选择现货。

THouse.Create('My house');

内存布局:

---[ttttNNNNNNNNNN]---
    1234My house

用地址保留一个变量

把这个地址写在你的新房子里,放在一张纸上。 本文将作为你参考你的房子。 没有这张纸,你就迷路了,找不到房子,除非你已经在那里。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

内存布局:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My 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

释放内存

拆除房子。 如果您愿意,您可以稍后再重新使用该纸张作为新地址,或者将其清除以忘记不再存在的房屋地址。

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)

摇晃的指针

你告诉你的企业家摧毁房子,但你忘了从你的纸上抹去地址。 当你看到那张纸时,你已经忘记了房子已经不在那里,并且去看望它,结果不合格(另见下面有关无效参考的部分)。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

在打电话给.Free之后使用h可能会起作用,但那只是纯粹的运气。 在关键操作过程中,它很可能会在客户地方失败。

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

正如你所看到的,h仍指向内存中数据的残余,但由于它可能不完整,所以像以前一样使用它可能会失败。


内存泄漏

你失去了这张纸,找不到房子。 房子仍然站在某个地方,而当你以后想要建造一座新房子时,你不能重复使用那个地方。

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             <--+

正如你所看到的,旧数据在内存中保持不变,并且不会被内存分配器重用。 分配器会跟踪哪些内存区域已被使用,除非您释放它,否则不会重用它们。


释放内存但保留(现在无效)引用

拆除房子,擦掉其中一张纸,但你还有另外一张纸,上面有旧地址,当你去地址时,你不会找到房子,但你可能会发现类似于废墟的东西一个。

也许你甚至会找到一所房子,但这不是你最初给这个地址的房子,因此任何使用它的尝试都可能会失败。

有时你甚至可能会发现邻居地址上有一个相当大的房子,它占据了三个地址(主要街道1-3),并且你的地址到了房子的中间。 任何试图将这个大型3房屋的那部分作为一个小房子来对待也可能会失败。

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仍然有旧的,过时的地址。 进入不再站立的房屋可能会或可能不会工作。

这是上面悬挂指针的变体。 查看其内存布局。


缓冲区溢出

你将更多的东西放进房子里,而不是放在邻居家或院子里。 当后来邻居家的主人回家时,他会发现他会认为他自己的各种事情。

这是我选择固定大小数组的原因。 为了设置舞台,假设我们分配的第二间房子出于某种原因将被放置在第一个房屋之前。 换句话说,第二宫的地址比第一宫低。 而且,它们分配在一起。

因此,这个代码:

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内部名称的部分名称可能不会改变,但当您尝试使用破碎的对象时,覆盖该对象的开销很可能会崩溃,覆盖存储到对象中其他对象的链接。


链接列表

当你在一张纸上看到一个地址时,你到达一个房子,在那个房子里有另一张纸,上面有一个新地址,下一个房子在这个链上,等等。

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添加为对象中的链接,用下图中的四个LLLL标注):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

基本上来说,什么是内存地址?

内存地址基本上只是一个数字。 如果你认为内存是一大串字节,那么第一个字节的地址为0,下一个字节的地址为1,依此类推。 这是简化的,但足够好。

所以这个内存布局:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

可能有这两个地址(最左边的是地址0):

  • h1 = 4
  • h2 = 23
  • 这意味着我们上面的链接列表可能看起来像这样:

        h1 (=4)                 h2 (=28)
        v                       v
    ---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
        1234Home      0028      5678Cabin     0000
                       |        ^              |
                       +--------+              * (no link)
    

    通常将“无处指向”地址存储为零地址。


    基本而言,什么是指针?

    指针只是一个保存内存地址的变量。 通常你可以让编程语言给你它的编号,但是大多数编程语言和运行时都试图隐藏下面有一个数字的事实,只是因为数字本身并没有给你带来任何意义。 最好把一个指针看作一个黑盒子,也就是说。 只要它能够工作,你并不真正了解或关心它是如何实现的。


    在我的第一个Comp Sci课程中,我们做了以下练习。 当然,这是一个有大约200名学生的演讲厅......

    教授在董事会上写道: int john;

    约翰站起来

    教授写道: int *sally = &john;

    莎莉站起来,指向约翰

    教授: int *bill = sally;

    比尔站起来,指着约翰

    教授: int sam;

    山姆站起来

    教授: bill = &sam;

    比尔现在指向萨姆。

    我想你应该已经明白了。 我认为我们花了大约一个小时来做​​这件事,直到我们完成了指针分配的基础知识。


    一个比喻我发现有用的解释指针是超链接。 大多数人可以理解,网页上的链接“指向”互联网上的另一个页面,如果您可以复制并粘贴该超链接,则它们都会指向同一个原始网页。 如果你去编辑原始页面,然后按照这些链接(指针),你会得到新的更新页面。

    链接地址: http://www.djcxy.com/p/31645.html

    上一篇: What are the barriers to understanding pointers and what can be done to overcome them?

    下一篇: Why scoped pointers in boost