理解指针的障碍是什么?如何克服这些障碍?[关闭]


449

为什么对于许多新的甚至老的C或C ++大学水平的学生来说,指针是导致混乱的主要因素?是否有任何工具或思维过程可以帮助您了解指针在变量,函数以及更高级别上的工作方式?

有什么好的做法可以使某人达到“啊哈,我明白了”的程度,而又不会使他们陷入整体观念的泥潭?基本上,像钻探一样的场景。


20
这个问题的论点是指针很难理解。这个问题没有提供证据表明指针比其他任何东西都更难理解。
bmargulies 2010年

14
也许我错过了一些东西(因为我用GCC语言编写了代码),但我一直认为内存中的指针是否为Key-> Value结构。由于在程序中传递大量数据非常昂贵,因此您可以创建结构(值)并传递其指针/引用(键),因为键是较大结构的较小表示形式。困难的部分是当您需要比较两个指针/引用(比较键或值)时,这需要更多的工作才能分解到结构(值)中包含的数据中。
伊万·普赖斯

2
@ Wolfpack'08“在我看来,地址中的内存永远都是int。” -那么在您看来,什么都没有类型,因为它们全都是内存中的位。“实际上,指针的类型是指针所指向的var的类型”-不,指针的类型是指向指针所指向的var的类型的指针-这是自然的,应该是显而易见的。
Jim Balter

2
我一直想知道,到底变量(和函数)只是内存块而指针是存储内存地址的变量,到底有什么难理解的呢?这种可能太实用的思维模型可能不会打动所有抽象概念的迷,但是它完全有助于理解指针的工作方式。
Christian Rau

8
简而言之,学生可能不会理解,因为他们无法正确理解或根本无法理解计算机的总体内存,特别是C“内存模型”的工作方式。本书《从头开始编程》提供了关于这些主题的很好的课程。
Abbafei 2013年

Answers:


745

指针是一个概念,很多人一开始可能会感到困惑,尤其是在复制指针值并仍然引用同一内存块的时候。

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

我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释。我之所以选择Delphi,是因为我的另一种主要编程语言C#不会以相同的方式展现诸如内存泄漏之类的东西。

如果仅希望学习指针的高级概念,则应忽略下面说明中标有“内存布局”的部分。它们旨在举例说明操作后的存储器外观,但是它们本质上是更底层的。但是,为了准确地解释缓冲区溢出是如何工作的,重要的是添加了这些图。

免责声明:出于所有意图和目的,此解释和示例内存布局都得到了极大简化。如果需要低级处理内存,则需要更多的开销和更多的细节。但是,出于解释内存和指针的目的,它足够准确。


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

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

初始化house对象时,将给构造函数的名称复制到私有字段FName中。将其定义为固定大小的数组是有原因的。

在内存中,房屋分配会带来一些开销,我将在下面举例说明:

--- [ttttNNNNNNNNNN] ---
     ^^
     | |
     | +-FName数组
     |
     +-开销

“ tttt”区域是开销,对于各种类型的运行时和语言,例如8或12个字节,通常会有更多的开销。至关重要的是,除内存分配器或核心系统例程外,任何存储在该区域中的值都不得更改,否则您有崩溃程序的风险。


分配内存

找一个企业家来盖房子,然后给你地址。与现实世界相反,不能告诉内存分配在哪里分配,而是会找到一个有足够空间的合适位置,并将地址报告回分配的内存。

换句话说,企业家将选择地点。

THouse.Create('My house');

内存布局:

--- [ttttNNNNNNNNNN] ---
    1234我的房子

保留地址变量

把地址写到你的新房子里,写在纸上。本文将作为您房屋的参考。没有这张纸,您会迷路,无法找到房子,除非您已经在其中。

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

内存布局:

    H
    v
--- [ttttNNNNNNNNNN] ---
    1234我的房子

复制指针值

只需将地址写在一张新纸上。现在,您有两张纸可以将您带到同一个房子,而不是两个单独的房子。任何试图从一张纸上的地址开始,然后重新布置该房屋的家具的尝试,都会使另一房屋看起来已经以相同的方式进行了修改,除非您可以明确地发现它实际上只是一所房屋。

注意这通常是我向人们解释最多的问题,两个指针并不意味着两个对象或内存块。

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    11
    v
--- [ttttNNNNNNNNNN] ---
    1234我的房子
    ^
    h2

释放内存

拆房子。然后,您以后可以根据需要将纸张重新用于新的地址,或者清除它以忘记该地址的地址不再存在。

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

在这里,我首先建造房屋,并掌握其地址。然后我对房子做一些事情(使用它,...代码,作为练习留给读者),然后释放它。最后,我从变量中清除地址。

内存布局:

    h <-+
    v +-在免费之前
--- [ttttNNNNNNNNNN] --- |
    1234我的房子<-+

    h(现在指向无处)<-+
                                +-免费后
---------------------- | (请注意,内存可能仍然
    xx34我的房子<-+包含一些数据)

悬空指针

您告诉企业家要摧毁房屋,但是却忘记从纸上擦除地址。稍后查看纸片时,您已经忘记了这所房子不再存在,而是去参观它,结果失败了(另请参见下面有关无效引用的部分)。

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

h在调用之后使用.Free 可能有效,但这纯粹是运气。在关键操作过程中,它很可能会在客户位置失败。

    h <-+
    v +-在免费之前
--- [ttttNNNNNNNNNN] --- |
    1234我的房子<-+

    h <-+
    v +-免费后
---------------------- |
    xx34我的房子<-+

如您所见,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] ---
    1234我的房子

第二次分配后的内存布局:

                       H
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234我的房子5678我的房子

获取此方法的更常见方法是忘记释放某些内容,而不是像上面那样覆盖它。用Delphi术语,这将通过以下方法发生:

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 +-在丢失指针之前
--- [ttttNNNNNNNNNN] --- |
    1234我的房子<-+

    h(现在指向无处)<-+
                                +-失去指针后
--- [ttttNNNNNNNNNN] --- |
    1234我的房子<-+

如您所见,旧数据保留在内存中,并且不会由内存分配器重用。分配器跟踪已使用的内存区域,除非您释放它,否则不会重复使用它们。


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

拆除房屋,擦除其中的一张纸,但是您还有另一张上面有旧地址的纸,当您转到该地址时,找不到房屋,但您可能会发现类似于废墟的东西一。

也许您甚至会找到一所房子,但它并不是您最初指定的住所,因此,任何试图使用它(好像它属于您)的尝试都可能会失败。

有时,您甚至可能会发现相邻地址上设置了一个相当大的房子,该房子占据了三个地址(大街1-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

第一次分配后的内存布局:

                        11
                        v
----------------------- [ttttNNNNNNNNNN]
                        5678我的房子

第二次分配后的内存布局:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
    1234我的其他房子在某处
                        ^ --- +-^
                            |
                            +-覆盖

最常导致崩溃的部分是当您覆盖存储的数据的重要部分时,这些部分实际上不应随意更改。例如,就程序崩溃而言,更改h1-house名称的一部分可能不是问题,但是当您尝试使用损坏的对象时,覆盖对象的开销很可能会崩溃,覆盖存储到对象中其他对象的链接。


链表

当您在一张纸上跟随一个地址时,您会到达一所房子,在那座房子上还有另一张纸,上面有新的地址,用于链中的下一个房子,依此类推。

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
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234首页+ 5678客舱+
                   | ^ |
                   + -------- + *(无链接)

从根本上讲,什么是内存地址?

从基本的角度来说,内存地址只是一个数字。如果您将内存视为一个大字节数组,则第一个字节的地址为0,下一个字节的地址为1,依此类推。这是简化的,但足够好。

所以这个内存布局:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234我的房子5678我的房子

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

  • h1 = 4
  • h2 = 23

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

    h1(= 4)h2(= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234首页0028 5678机舱0000
                   | ^ |
                   + -------- + *(无链接)

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


从根本上讲,什么是指针?

指针只是一个保存内存地址的变量。通常,您可以要求编程语言为您提供编号,但是大多数编程语言和运行时都试图掩盖一个事实,即数字本身对您没有任何意义。最好将指针视为黑盒,即。只要它有效,您就不会真正知道或关心它的实际实现方式。


59
缓冲区溢出很有趣。“邻居回家了,他的头骨裂开了,滑向了你的垃圾,并起诉了你。”
gtd

12
当然,这是对概念的很好的解释。但是,这个概念并不是让我感到困惑的东西,因此整篇文章都有些浪费。
布列塔尼

10
但是只是问了一下,对指针有什么困惑?
Lasse V. Karlsen

11
自从您写出答案以来,我已经多次重访了这篇文章。您对代码的说明非常好,非常感谢您重新访问它以添加/完善更多的想法。喝彩!
David McGraw

3
一页文本(无论长度有多长)都不可能总结出内存管理,引用,指针等的每一个细微差别。既然有465人对此表示赞成,那么我想说它已经足够了起始页上的信息。还有更多要学习的吗?当然可以,什么时候不可以?
Lasse V. Karlsen

153

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

教授在黑板上写道: int john;

约翰站起来

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

莎莉站起来,指着约翰

教授: int *bill = sally;

比尔站起来,指着约翰

教授: int sam;

山姆站起来

教授: bill = &sam;

比尔现在指向山姆。

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


5
我不认为我做错了。我的意图是将指向变量的值从John更改为Sam。与人代表起来有点困难,因为看起来您正在更改两个指针的值。
特里克

24
但是,这令人困惑的原因是,这并不像我们想象的那样,约翰不像约翰从座位上站起来,然后山姆坐下了一样。这更像是山姆走过来,将手伸到约翰身上,然后将山姆程序编程克隆到约翰的体内,就像雨果编织了矩阵一样。
布列塔尼

59
更像是山姆坐在约翰的座位上,然后约翰在房间里漂浮,直到撞到关键的地方并造成了段错误。
just_wes 2010年

2
我个人认为此示例不必要地复杂。我的教授告诉我指着一盏灯,然后说:“你的手是指向灯光物体的指针”。
Celeritas

此类示例的问题在于指向X和X的指针不相同。而且这不会被人们描绘出来。
Isaac Nequittepas

124

我发现一个有助于解释指针的类比是超链接。大多数人都可以理解,网页上的链接“指向”互联网上的另一页,如果您可以复制并粘贴该超链接,则它们都将指向同一原始网页。如果您去编辑该原始页面,然后单击这些链接(指针)中的任何一个,您将获得该新的更新页面。


15
我真的很喜欢 不难看出,两次写超链接不会使两个网站出现(就像int *a = b不制作的两个副本一样*b)。
熟练地

4
这实际上是非常直观的,每个人都应该能够关联。尽管在很多情况下这种类比都无法解决。非常适合快速入门。+1
Brian Wigginton

指向打开两次的页面的链接通常会创建该页面的两个几乎完全独立的实例。我认为超链接可能更像是构造函数,而不是指针。
Utkan Gezer 2014年

@ThoAppelsin不一定正确,例如,如果您访问的是静态html网页,那么您正在访问服务器上的单个文件。
dramzy

5
您想得太多了。超链接指向服务器上的文件,这就是类推的程度。
2015年

48

指针似乎让很多人感到困惑的原因是,它们在计算机体系结构中大多没有或几乎没有背景。由于许多人似乎对计算机(机器)的实际实现方式一无所知,因此在C / C ++中工作似乎有些陌生。

钻研是要求他们实现一个简单的基于字节码的虚拟机(以他们选择的任何语言,python都可以很好地实现这一点),其指令集专注于指针操作(加载,存储,直接/间接寻址)。然后要求他们为该指令集编写简单的程序。

除了简单的加法之外,任何需要更多的东西都将涉及指针,他们肯定会得到它。


2
有趣。不过不知道如何开始。有什么资源可以分享吗?
卡洛里斯2009年

1
我同意。例如,我学会了在C之前进行汇编编程,并且知道寄存器的工作方式,学习指针很容易。实际上,没有太多的学习,这一切都是很自然的。
米兰巴布斯科夫2009年

以一个基本的CPU为例,说一些可以运行割草机或洗碗机的东西并加以实现。或ARM或MIPS的非常基本的子集。两者都有一个非常简单的ISA。
丹尼尔·戈德堡

1
值得指出的是,唐纳德·努斯本人曾提倡/实践过这种教育方法。克努斯的《计算机编程艺术》描述了一种简单的假设体系结构,并要求学生以一种针对该体系结构的假设汇编语言来实施解决方案。一旦切实可行,一些阅读Knuth书籍的学生实际上将其体系结构实现为VM(或使用现有的实现),并实际运行其解决方案。如果有时间的话,IMO是学习的好方法。
WeirdlyCheezy 2012年

4
@Luke我不认为了解那些根本无法掌握指针的人(或者更确切地说,通常是间接的)不是那么容易。您基本上是假设那些不了解C指针的人将能够开始学习汇编,了解计算机的基础体系结构并在了解了指针的情况下回到C语言。这对许多人来说可能是正确的,但根据一些研究,似乎有些人天生就无法掌握间接性,即使是从原则上也是如此(我仍然很难相信,但也许我对我的“学生”很幸运”)。
a安2015年

27

为什么对于许多使用C / C ++语言的新手甚至旧的大学水平的学生,指针是导致混乱的主要因素?

值的占位符概念-变量-映射到我们在学校中学到的东西-代数。在不了解计算机内部内存的物理布局的情况下,您无法绘制出现有的并行图形,并且在C / C ++ /字节通信级别处理低级图形之前,没有人考虑过这种图形处理。 。

是否有任何工具或思维过程可以帮助您了解指针在变量,函数以及更高级别上的工作方式?

地址框。我记得当我学习将BASIC编程到微型计算机时,里面有很多漂亮的书,里面有游戏,有时您必须将值戳入特定的地址。他们有一堆箱子的图片,并用0、1、2 ...递增标记,并解释说这些箱子只能装一个小东西(一个字节),而且其中有很多-一些计算机多达65535!他们彼此相邻,并且都有一个地址。

有什么好的做法可以使某人达到“啊哈,我明白了”的程度,而又不会使他们陷入整体观念的泥潭?基本上,像钻探一样的场景。

要演习吗?制作一个结构:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

与上面相同的示例,但C语言除外:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

输出:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

也许通过示例可以解释一些基本知识?


+1表示“不了解内存的物理放置方式”。我是从汇编语言背景来到C的,指针的概念非常自然和容易。而且我见过只有较高语言背景的人很难弄清楚。更糟糕的是,语法令人困惑(函数指针!),因此同时学习概念和语法是麻烦的根源。
布伦丹

4
我知道这是一篇旧文章,但是如果将提供的代码输出添加到该文章中,那将是很棒的。
乔什(Josh)2013年

是的,它类似于代数(尽管代数的“变量”是不变的,因此具有额外的可理解性)。但是我认识的大约一半的人在实践中没有掌握代数。它只是不为他们计算。他们知道获得结果的所有这些“等式”和处方,但是他们有些随机且笨拙地应用它们。而且他们不能出于自己的目的扩展它们-对于他们来说这只是一个不可更改,不可组合的黑匣子。如果您了解代数并能够有效地使用它,那么即使在程序员中,您也已经领先于一切。
a安2015年

24

首先,我很难理解指针的原因是,许多解释都包含大量关于引用传递的废话。所有这些都使问题变得混乱。当使用指针参数时,您仍在按值传递。但是该值恰好是地址而不是int。

其他人已经链接到本教程,但是我可以重点介绍我开始理解指针的时刻:

C语言中的指针和数组教程:第3章-指针和字符串

int puts(const char *s);

暂时,忽略const.传递给的参数puts()是一个指针,一个指针的值(因为C中的所有参数都按值传递),而指针的值就是它指向的地址,或者, 一个地址。因此,puts(strA);如我们所见,当我们写时,我们正在传递strA [0]的地址。

当我阅读这些单词的那一刻,乌云散开,一束阳光笼罩着我,使我理解了指针。

即使您是VB .NET或C#开发人员(就我而言),并且从不使用不安全的代码,仍然值得了解指针的工作方式,否则您将不了解对象引用的工作方式。然后,您将拥有一个常见但容易出错的概念,即将对象引用传递给方法会复制该对象。


让我想知道拥有指针的意义是什么。在我遇到的大多数代码块中,指针仅在其声明中可见。
Wolfpack'08 2012年

@ Wolfpack'08 ...什么?您在看什么代码?
凯尔·斯特兰德

@KyleStrand让我看看。
Wolfpack'15年

19

我发现Ted Jensen的“ C语言中的指针和数组教程”是学习指针的绝佳资源。它分为10节课,首先说明什么是指针(以及它们的作用),最后以函数指针结束。http://home.netcom.com/~tjensen/ptr/cpoint.htm

从那里继续前进,Beej的《网络编程指南》讲授Unix套接字API,您可以从中开始做一些真正有趣的事情。http://beej.us/guide/bgnet/


1
我第二次Ted Jensen的教程。它可以将指针分解为一个细节级别,细节并不过分,我读过的书都没有。极有帮助!:)
Dave Gallagher 2010年

12

指针的复杂性超出了我们可以轻松教导的范围。让学生互相指点并使用带有住所地址的纸质都是很好的学习工具。他们在介绍基本概念方面做得很好。确实,学习基本概念对于成功使用指针至关重要。但是,在生产代码中,进入比这些简单演示所封装的复杂得多的场景是很常见的。

我参与的系统中,我们的结构指向其他结构,而结构指向其他结构。这些结构中的一些还包含嵌入式结构(而不​​是指向其他结构的指针)。这是指针真正令人困惑的地方。如果您具有多个间接级别,那么您将以如下代码开始结尾:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

它会很快变得令人迷惑(想象更多的行,并可能会有更多的关卡)。放入指针数组和节点到节点指针(树,链接列表)的数组,情况还会变得更糟。我已经看到一些真正优秀的开发人员一旦开始在这样的系统上工作,他们就会迷失方向,甚至是那些非常了解基础知识的开发人员。

指针的复杂结构也不一定表示编码不好(尽管它们可以)。组合是良好的面向对象编程的重要组成部分,在带有原始指针的语言中,组合不可避免地会导致多层间接。此外,系统经常需要使用第三方库,这些库的结构在样式或技术上都不匹配。在这种情况下,自然会出现复杂性(尽管可以肯定,我们应该尽可能地与之抗争)。

我认为大学可以帮助学生学习指针的最好的方法就是使用良好的示范,并结合需要指针使用的项目。一个困难的项目对指针的理解将比一千个示范做更多的事情。演示可以使您有一个浅浅的了解,但是要深刻掌握指针,您必须真正使用它们。


10

我以为我会在这个列表上添加一个类比,当我作为计算机科学导师解释指针(回溯过去)时,我发现这非常有帮助;首先,让我们:


设置阶段

考虑一个有3个车位的停车场,这些车位编号:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

从某种意义上说,这就像内存位置,它们是连续且连续的,有点像数组。现在没有汽车,所以就像一个空数组(parking_lot[3] = {0})。


添加数据

停车场永远不会空着多久……如果这样做的话,那将毫无意义,而且没人会建任何停车场。假设随着一天的过去,地段上有3辆车,一辆蓝车,一辆红车和一辆绿车:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

这些车都是同一类型(轿车),所以想到这一点的一种方式是,我们的车有一些类型的数据(说的int),但他们有不同的值(blueredgreen,这可能是一种颜色enum


输入指针

现在,如果我带您进入这个停车场,并要求您为我找到一辆蓝色的汽车,您伸出一根手指,用它指向点1处的蓝色汽车。这就像获取指针并将其分配给内存地址一样(int *finger = parking_lot

您的手指(指针)不是我问题的答案。看你的手指会告诉我什么,但如果我看你在哪里手指指向(解引用指针),我能找到我一直在寻找汽车(数据)。


重新分配指针

现在,我可以请您找到一辆红色的汽车,然后您可以将手指重定向到一辆新汽车。现在,您的指针(与以前相同)向我显示了相同类型(汽车)的新数据(可以找到红色汽车的停车位)。

指针实际上并没有改变,仍然是您的手指,只是显示给我的数据已改变。(“停车场”地址)


双指针(或指向指针的指针)

这也适用于多个指针。我可以问一下指针在哪里,它指向红色的汽车,您可以用另一只手并用手指指向第一根手指。(这就像int **finger_two = &finger

现在,如果我想知道蓝色的汽车在哪里,我可以按照第一个手指的方向到第二个手指,再到汽车(数据)。


悬空的指针

现在,假设您感觉非常像一座雕像,并且想要无限期地握着手指向红色汽车。如果那辆红色汽车开走了怎么办?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

您的指针仍指向红色汽车的位置但不再指向该位置。假设有一辆新车驶入那里……一辆橙色车。现在,如果我再次问您“红色汽车在哪里”,您仍然指向那里,但现在您错了。那不是一辆红色的汽车,那是橙色的。


指针算术

好的,所以您仍然指向第二个停车位(现在已被橙色汽车占用)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

好吧,我现在有一个新问题……我想知道下一个停车位的汽车颜色。您可以看到您指向第2个点,因此只需加1即可指向下一个点。(finger+1),因为我想知道那里的数据,所以您必须检查该点(而不仅仅是手指),以便可以顺着指针(*(finger+1))看到那里有辆绿色的汽车(该位置的数据)


只是不要使用“双指针”这个词。指针可以指向任何东西,因此显然您可以拥有指向其他指针的指针。它们不是双指针。
gnasher729 2014年

我认为这错了点,即“手指”本身,继续您的类比,每个“占用一个停车位”。我不确定人们在理解类比的高层时是否有任何困难,而是理解指针是易变的东西,它们占据了内存位置,并且它如何有用,似乎在逃避人们。
Emmet

1
@Emmet-我不同意WRT指针还有很多,但我读了一个问题:"without getting them bogged down in the overall concept"作为高级理解。和你的观点:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"-你会很惊讶有多少人理解,甚至指针到这个水平
麦克

将汽车手指比喻延伸到坐在一辆汽车中指向另一辆汽车的人(用一个或多个手指-以及遗传异常,可以使每个手指指向任何方向!)有什么优点吗?弯腰指向土地旁边的荒地,作为“未初始化的指针”;或者整只手张开,指向一行空间,作为“固定大小[5]的指针数组”,或者弯腰成手掌的“空指针”指向某个地方,那里永远都没有汽车)... 8
SlySven

您的解释是不错的,对初学者来说是一个很好的解释。
Yatendra Rathore'17年

9

我认为指针作为一个概念并不是特别棘手-大多数学生的心理模型都映射到这样的东西,并且一些快速的方框草图可能会有所帮助。

困难,至少是我过去所经历的以及其他人所遇到的困难,是,不必要地使C / C ++中的指针管理变得困难。



8

指针的问题不是概念。它涉及执行和语言。当教师认为困难的是指针的概念,而不是行话,或C和C ++造成混乱的混乱时,还会产生其他混乱。如此大量的精力却无法解释这个概念(就像在这个问题的公认答案中一样),并且这几乎浪费在像我这样的人身上,因为我已经理解了所有这些。这只是在解释问题的错误部分。

为了让您了解我的来历,我是一个非常了解指针的人,并且我可以用汇编语言熟练地使用它们。因为在汇编语言中,它们不称为指针。它们被称为地址。当涉及到在C中进行编程和使用指针时,我犯了很多错误,并且感到非常困惑。我仍然没有解决这个问题。让我举一个例子。

当api说:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

要什么

它可能想要:

代表缓冲区地址的数字

(为此,我说doIt(mybuffer)还是doIt(*myBuffer)?)

一个数字,表示地址到缓冲区的地址

(那是doIt(&mybuffer)doIt(mybuffer)doIt(*mybuffer)?)

一个数字,代表地址到缓冲区的地址

(也许那是doIt(&mybuffer)。或它是doIt(&&mybuffer)?甚至doIt(&&&mybuffer)

依此类推,所涉及的语言并不清楚,因为它涉及的单词“指针”和“引用”对我的含义和清晰度不如“ x将地址指向y”和“此函数需要一个地址为y“。答案还取决于到底是什么“ mybuffer”,以及它打算做什么。该语言不支持在实践中遇到的嵌套级别。就像我必须向创建新缓冲区的函数中传递“指针”一样,它会修改指针以指向缓冲区的新位置。它是否确实要使用指针或指向该指针的指针,因此它知道在哪里可以修改指针的内容。大多数时候,我只需要猜测“

“指针”太重载了。指针是指向值的地址吗?还是将地址保存为值的变量。当一个函数想要一个指针时,它是想要指针变量所保存的地址,还是想要指针变量的地址?我糊涂了。


我看到过这样的解释:如果您看到类似的指针声明double *(*(*fn)(int))(char),则求值结果*(*(*fn)(42))('x')将为double。您可以剥离评估层以了解中间类型必须是什么。
Bernd Jendrissek

@BerndJendrissek不确定我是否遵循。那么评估的结果是 (*(*fn)(42))('x') 什么?
列塔尼

您得到一件事(我们称之为x),如果您进行评估*x,您将获得双倍奖励。
2013年

@BerndJendrissek这应该解释一些有关指针的内容吗?我不明白 你想说什么?我剥离了一层,没有获得任何有关任何中间类型的新信息。它能解释什么特定功能将要接受?它与什么有什么关系?
列塔尼

也许这个解释中的信息(不是我的,我希望我能找到我第一次看到它的地方)是要少看些什么fn 而多些你能做的事情fn
贝恩德Jendrissek

8

我认为理解指针的主要障碍是糟糕的老师。

几乎每个人都被教导关于指针的谎言:它们不过是内存地址,或者它们使您可以指向任意位置

当然,它们很难理解,既危险又半神奇。

都不是真的。指针实际上是相当简单的概念,只要您坚持使用C ++语言必须对它们说的话,并且不要使它们具有“通常”在实践中起作用的属性,但是该语言不能保证这些属性,因此也不属于指针的实际概念。

几个月前,我曾在博客中写过对此的解释-希望它能对某人有所帮助。

(请注意,在有人对我me之以鼻之前,是的,C ++标准确实指出了指针 代表内存地址。但是它并没有说“指针是内存地址,除了内存地址外什么都没有,并且可以与内存互换使用或想到”地址”。区别很重要)


毕竟,即使C的“值”为零,空指针也不会指向内存中的零地址。这是一个完全独立的概念,如果您处理错误,可能最终会解决(和取消引用)您原本不希望的事情。在某些情况下,它甚至可能是内存中的零地址(特别是现在地址空间通常是平坦的),但是在其他情况下,优化编译器可能会忽略它作为未定义的行为,或者访问与内存相关的其他部分对于给定的指针类型,其值为“零”。随之而来的是狂欢。
a安

不必要。您需要能够在头脑中为计算机建模,以使指针有意义(并调试其他程序)。并非每个人都能做到。
托尔比约恩Ravn的安徒生

5

我认为使指针学习起来很棘手的是,直到您对指针感到满意为止,即“在此存储位置处是一组表示int,double,character等的位”。

当您第一次看到指针时,您并没有真正获得该存储位置的内容。“你是什么意思,它有一个地址?”

我不同意“要么得到要么不得到”的观念。

当您开始发现它们的实际用途时(例如不将大型结构传递给函数),它们变得更容易理解。


5

很难理解的原因不是因为它是一个困难的概念,而是因为语法不一致

   int *mypointer;

您首先了解到,变量创建的最左边部分定义了变量的类型。指针声明在C和C ++中不能像这样工作。相反,他们说该变量指向左侧的类型。在这种情况下:*mypointer 指向一个int。

直到我尝试在C#中使用指针(不安全)时,我才完全掌握指针,它们以完全相同的方式工作,但具有逻辑和一致的语法。指针本身就是类型。这里mypointer 一个指向int的指针。

  int* mypointer;

甚至不让我开始使用函数指针...


2
实际上,您的两个片段都是有效的C。第一个更常见是很多年的C风格问​​题。例如,第二个在C ++中更为常见。
RBerteig

1
第二个片段实际上不能与更复杂的声明一起使用。一旦意识到指针声明的右侧部分向您显示了必须执行的操作才能获得其类型为左侧原子类型说明符的语法,语法就不会那么“不一致”。
2013年

2
int *p;有一个简单的含义:*p是一个整数。int *p, **pp表示:*p**pp是整数。
Miles Rout

@MilesRout:但这就是问题所在。*p**pp不是整数,因为你永远不初始化ppp*pp以点什么。我了解为什么有些人喜欢在此语法上坚持使用语法,特别是因为某些极端情况和复杂情况要求您这样做(尽管如此,在我所知道的所有情况下,您都可以琐碎地解决此问题)...但是我认为,这些案例并不比教导正确的对齐方式会误导新手这一事实更为重要。更何况丑陋!:)
轻轨赛将于

@LightnessRacesinOrbit教学右对齐绝不会引起误解。这是教授它的唯一正确方法。不教它是误导。
Miles Rout 2015年

5

我只知道C ++时可以使用指针。我有点知道在某些情况下该做什么,而试行/错误该不该做什么。但是让我完全理解的是汇编语言。如果您对编写的汇编语言程序进行了认真的指令级调试,那么您应该能够理解很多内容。


4

我喜欢内部地址的比喻,但我一直认为地址是邮箱本身。这样,您可以形象化取消引用指针(打开邮箱)的概念。

例如,下面是一个链接列表:1)从带有地址的纸张开始2)转到纸张上的地址3)打开邮箱以查找带有下一个地址的新纸张

在线性链接列表中,最后一个邮箱(列表结尾)中没有任何内容。在循环链接列表中,最后一个邮箱中有第一个邮箱的地址。

请注意,第3步是发生取消引用的地方,并且当地址无效时您将崩溃或出错。假设您可以走到无效地址的邮箱,想象一下那里有一个黑洞或东西,这会使世界变得反过来:)


邮箱编号类比的一个令人讨厌的复杂之处在于,虽然丹尼斯·里奇(Dennis Ritchie)发明的语言根据字节的地址和存储在这些字节中的值来定义行为,但C标准定义的语言邀请“优化”实现使用行为的模型,它比较复杂,但是以模棱两可,矛盾和不完整的方式定义了模型的各个方面。
超级猫

3

我认为人们对此感到困惑的主要原因是因为通常没有以一种有趣且引人入胜的方式来教授它。我希望看到一位讲师从人群中招募10名志愿者,并给他们每人一个1米的直尺,让他们以一定的配置站立并使用直尺互相指点。然后通过使人们四处移动(以及他们指向标尺的位置)来显示指针算法。这是一种简单但有效(最重要的是令人难忘的)方式来展示概念,而又不会使技术陷入困境。

一旦您接触到C和C ++,对于某些人来说似乎会越来越难。我不确定这是否是因为他们最终将他们没有正确掌握的理论付诸实践,或者是因为在这些语言中指针操作本来就更难。我不太记得自己的转换,但是我知道 Pascal中的指针,然后移到C并完全迷失了。


2

我不认为指针本身会造成混淆。大多数人都可以理解这个概念。现在您可以考虑多少个指针,或者您适应多少个间接级别。让人们处于优势地位并不需要太多。它们可能会被程序中的错误意外更改,这也使它们在代码出错时很难调试。


2

我认为这实际上可能是语法问题。指针的C / C ++语法似乎不一致,并且比需要的语法更复杂。

具有讽刺意味的是,实际上帮助我理解指针的一件事是在c ++ 标准模板库中遇到了迭代器的概念。。具有讽刺意味的是,我只能假设迭代器被认为是指针的一般化。

有时,直到学会忽略树木,您才看不到森林。


问题主要出在C声明语法中。但是,如果(*p)可以使用指针的话,肯定会更容易(p->),因此,我们p->->x*p->x
可以避免

@MSalters哦,天哪,你在开玩笑吧?那里没有矛盾之处。a->b简单地意味着(*a).b
Miles Rout 2014年

@Miles:确实,按照这种逻辑,* p->x手段* ((*a).b)*p -> x手段(*(*p)) -> x。混合使用前缀和后缀运算符会导致模棱两可的解析。
MSalters 2014年

@MSalters否,因为空格不相关。这就像说,1+2 * 3应该是9
万里溃败

2

混淆来自在“指针”概念中混合在一起的多个抽象层。程序员不会被Java / Python中的普通引用所迷惑,但是指针的不同之处在于它们公开了底层内存体系结构的特征。

干净地分离抽象层是一个好原则,而指针则不能这样做。


1
有趣的是,C指针实际上并未公开底层内存体系结构的任何特性。Java引用和C指针之间的唯一区别是,您可以具有涉及指针的复杂类型(例如int ***或char *()(void *)),存在数组的指针算法和指向结构成员的指针, void *和数组/指针对偶性。除此之外,它们的工作原理相同。
jpalecek

好点子。做到这一点的是指针算法和缓冲区溢出的可能性-通过突破当前相关的内存区域来突破抽象。
约书亚·福克斯

@jpalecek:很容易理解指针如何在以底层体系结构记录其行为的实现中工作。说foo[i]意味着要去某个地方,向前走一段距离,然后看看那里有什么。使事情复杂化的是标准为了纯粹编译器的利益而添加的更加复杂的额外抽象层,但是对事物进行建模的方式不适合程序员和编译器需求。
超级猫

2

我喜欢用数组和索引来解释它的方式-人们可能不熟悉指针,但是他们通常知道索引是什么。

因此,我想像一下,RAM是一个数组(而您只有10个字节的RAM):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

然后,指向变量的指针实际上只是该变量在RAM中的索引(第一个字节)。

因此,如果您有一个指针/索引unsigned char index = 2,则该值显然是第三个元素或数字4。指向指针的指针就是您将该数字用作索引本身的地方,例如RAM[RAM[index]]

我会在纸张列表上绘制一个数组,然后用它来显示诸如指向同一内存的许多指针,指针算术,指针指针之类的东西。


1

邮政信箱号。

这是一条信息,可让您访问其他内容。

(而且,如果您对邮政信箱号码进行算术运算,则可能会遇到问题,因为字母放在错误的信箱中。如果有人移动到另一种状态(没有转发地址),那么您将有一个悬空指针。另一方面-如果邮局转发邮件,则您有一个指向指针的指针。)


1

通过迭代器来掌握它不是一个坏方法。但是不断寻找,您会看到Alexandrescu开始抱怨它们。

许多前C ++开发人员(在转储该语言之前从未理解过迭代器是现代的指针)跳到C#并仍然相信他们拥有不错的迭代器。

嗯,问题在于所有迭代器都与运行时平台(Java / CLR)试图实现的目标完全相悖:新的,简单的,每个人都是开发人员的用法。可能不错,但是他们在紫色书中曾说过一次,甚至在C语言之前和之前都说过:

间接

这是一个非常强大的概念,但如果您始终这样做,则永远不会如此。.迭代器非常有用,因为它们有助于抽象算法,这是另一个示例。编译时是算法的地方,非常简单。您知道代码+数据,或者使用其他C#语言:

IEnumerable + LINQ + Massive Framework = 300MB糟糕的运行时间接代价,通过引用类型实例的堆拖动应用程序。

“勒珀特很便宜。”


4
这和什么有什么关系?
尼尔·威廉姆斯

...除了“静态链接是有史以来最好的事情”和“我不了解与以前学到的东西有什么不同”之外,您想说什么?
a安

六安,你可能不知道通过在2000年拆解JIT可以学到什么?它最终指向一个来自指针表的跳转表,如ASM在2000年在线所示,因此,不了解任何其他内容可能会产生另一种含义:仔细阅读是一项必不可少的技能,请重试。
rama-jka toti

1

上面的一些回答断言“指针并不难”,但并没有直接解决“指针难”的问题。来自。几年前,我曾辅导过一年级的CS学生(仅一年,因为我很清楚),所以我很清楚指针的想法并不难。很难理解为什么以及何时需要指针

我认为您不能将这个问题(为什么以及何时使用指针)与解释更广泛的软件工程问题区分开来。为什么每个变量都不应该是全局变量,为什么每个变量都应该将相似的代码分解为函数(要做到这一点,请使用指针将其行为专门用于其调用位置)。


0

我看不出指针有什么令人困惑的地方。它们指向内存中的一个位置,即它存储内存地址。在C / C ++中,您可以指定指针指向的类型。例如:

int* my_int_pointer;

说my_int_pointer包含指向包含int的位置的地址。

指针的问题在于它们指向内存中的某个位置,因此很容易跳入您不应该进入的某个位置。作为证明,着眼于C / C ++应用程序中由于缓冲区溢出而产生的众多安全漏洞(增加指针)超过分配的边界)。


0

只是为了使事情更加混乱,有时您必须使用句柄而不是指针。句柄是指向指针的指针,以便后端可以移动内存中的内容以对堆进行碎片整理。如果指针在例行程序中更改,则结果是不可预测的,因此您首先必须锁定句柄以确保任何地方都不会移动。

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5比我讲得更连贯。:-)


-1:句柄不是指向指针的指针;它们在任何意义上都不是指针。不要混淆他们。
伊恩·高德比

“它们在任何意义上都不是指针” –嗯,我希望有所不同。
SarekOfVulcan

指针是一个存储位置。句柄是任何唯一标识符。它可能是一个指针,但也可能是数组中的索引,或者其他任何与此相关的东西。您提供的链接只是一个特殊情况,其中句柄是指针,但不一定必须如此。另请参见parashift.com/c++-faq-lite/references.html#faq-8.8
伊恩·

该链接也不支持您断言它们在任何意义上都不是指针,例如-“例如,句柄可能是Fred **,其中指向Fred *的指针...”我不认为-1是公平的。
SarekOfVulcan

0

每个C / C ++初学者都有相同的问题,发生此问题的原因不是因为“指针很难学习”,而是因为“谁以及如何解释它”。一些学习者从视觉上以口头形式收集它,而最好的解释方式是使用“训练”示例(适合口头和视觉示例)。

其中“机车”不能容纳任何东西的指针,而“旅行车”是“机车”试图拉(或指向)的指针。之后,您可以对“旅行车”本身进行分类,可以容纳动物,植物或人(或它们的混合物)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.