为什么指针是许多新的,甚至是大学的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=23

这意味着我们上面的链接列表实际上可能是这样的:

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

通常将"无处指向"的地址存储为零地址.


In basic terms, what is a pointer?

指针只是一个保存内存地址的变量.您通常可以询问编程人员

C++相关问答推荐

新的memaligning函数有什么用?

有关字符数组指针和次指针以及qsort函数中的cmp函数的问题

如何将FileFilter添加到FileDialog GTK 4

从内联程序集调用Rust函数和调用约定

由Go调用E.C.引起的内存快速增长

两个连续的语句是否按顺序排列?

使用额外的公共参数自定义printf

从C文件中删除注释

仅从限制指针参数声明推断非混叠

Fprintf正在写入多个 struct 成员,并且数据过剩

运行时错误:在索引数组时加载类型为';char';`的空指针

安全倒计时循环

从BIOS(8086)中读取刻度需要多少?

S,在 struct 中创建匿名静态缓冲区的最佳方式是什么?

计算SIZE_MAX元素的长数组的大小

';malloc():损坏的顶部大小';分配超过20万整数后

10 个字节对于这个 C 程序返回后跳行的能力有什么意义

从管道读取数据时丢失

使用共享变量同步多线程 C 中的函数

使用复合文字数组初始化的指针数组