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

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

===============>>#1 票数:743 已采纳

指针是一个概念,对于许多人而言,一开始可能会造成混淆,尤其是在围绕周围复制指针值并仍然引用同一内存块的时候。

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

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

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

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


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

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个字节,通常会有更多的开销。 至关重要的是,除了内存分配器或核心系统例程外,任何存储在该区域中的值都不得更改,否则您有崩溃程序的风险。


分配内存

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

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

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

获取此方法的更常见方法是忘记释放某些内容,而不是像上面那样覆盖它。 用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                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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


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

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

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

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

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

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

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

h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

最经常导致崩溃的部分是当您覆盖存储的数据的重要部分时,实际上不应随意更改这些部分。 例如,就程序崩溃而言,更改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
    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)

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


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

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

===============>>#2 票数:151

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

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

约翰站起来

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

莎莉站起来,指着约翰

教授: int *bill = sally;

比尔站起来,指着约翰

教授: int sam;

山姆站起来

教授: bill = &sam;

比尔现在指向山姆。

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

===============>>#3 票数:124

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

===============>>#4 票数:48

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

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

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

===============>>#5 票数: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

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

===============>>#6 票数:24

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

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

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

int puts(const char *s);

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

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

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

===============>>#7 票数:19

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

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

===============>>#8 票数:12

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

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

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

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

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

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

===============>>#9 票数: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) )以查看是否有绿色。那里的汽车(该位置的数据)

===============>>#10 票数:9

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

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

===============>>#11 票数:9

一个带有一组图表的教程示例可以极大地帮助您理解指针

乔尔·斯波斯基(Joel Spolsky)在其《 游击面试指南》中提出了一些有关理解指针的观点:

由于某些原因,大多数人似乎出生时就没有大脑中能理解指针的部分。 这是一个天才的事情,而不是技巧的事情–它需要一些人无法做到的复杂形式的双重间接思维。

===============>>#12 票数:8

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

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

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

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

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

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

===============>>#13 票数: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”,以及它打算做什么。 该语言不支持在实践中遇到的嵌套级别。 就像我必须向创建新缓冲区的函数中传递“指针”一样,它会修改指针以使其指向缓冲区的新位置。 它确实需要指针还是指向该指针的指针,因此它知道去哪里可以修改指针的内容。 大多数时候,我只需要猜测“指针”的含义,大多数时候我就错了,不管我在猜测中有多少经验。

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

===============>>#14 票数:5

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

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

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

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

===============>>#15 票数:5

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

   int *mypointer;

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

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

  int* mypointer;

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

===============>>#16 票数:5

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

===============>>#17 票数:4

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

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

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

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

===============>>#18 票数:3

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

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

===============>>#19 票数: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]]

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

===============>>#20 票数:2

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

===============>>#21 票数:2

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

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

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

===============>>#22 票数:2

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

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

===============>>#23 票数:1

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

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

===============>>#24 票数:1

邮政信箱号。

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

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

===============>>#25 票数:1

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

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

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

间接

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

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

“勒珀特很便宜。”

===============>>#26 票数:0

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

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

===============>>#27 票数:0

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

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

===============>>#28 票数:0

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

int* my_int_pointer;

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

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

  ask by community wiki translate from so

未解决问题?本站智能推荐: