为什么对于许多新的甚至老的C或C ++大学水平的学生来说,指针是导致混乱的主要因素?是否有任何工具或思维过程可以帮助您了解指针在变量,函数以及更高级别上的工作方式?
有什么好的做法可以使某人达到“啊哈,我明白了”的程度,而又不会使他们陷入整体观念的泥潭?基本上,像钻探一样的场景。
为什么对于许多新的甚至老的C或C ++大学水平的学生来说,指针是导致混乱的主要因素?是否有任何工具或思维过程可以帮助您了解指针在变量,函数以及更高级别上的工作方式?
有什么好的做法可以使某人达到“啊哈,我明白了”的程度,而又不会使他们陷入整体观念的泥潭?基本上,像钻探一样的场景。
Answers:
指针是一个概念,很多人一开始可能会感到困惑,尤其是在复制指针值并仍然引用同一内存块的时候。
我发现最好的类比是将指针视为一张纸,上面有一个房屋的地址,而它所引用的内存块就是实际的房屋。因此可以容易地解释各种操作。
我在下面添加了一些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(= 28) vv --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL] 1234首页0028 5678机舱0000 | ^ | + -------- + *(无链接)
通常将“无处指向”的地址存储为零地址。
从根本上讲,什么是指针?
指针只是一个保存内存地址的变量。通常,您可以要求编程语言为您提供编号,但是大多数编程语言和运行时都试图掩盖一个事实,即数字本身对您没有任何意义。最好将指针视为黑盒,即。只要它有效,您就不会真正知道或关心它的实际实现方式。
在我的第一届Comp Sci课程中,我们做了以下练习。当然,这是一个演讲厅,里面有大约200名学生...
教授在黑板上写道: int john;
约翰站起来
教授写道: int *sally = &john;
莎莉站起来,指着约翰
教授: int *bill = sally;
比尔站起来,指着约翰
教授: int sam;
山姆站起来
教授: bill = &sam;
比尔现在指向山姆。
我想你应该已经明白了。我认为我们花了大约一个小时来完成这项工作,直到我们了解了指针分配的基础知识为止。
我发现一个有助于解释指针的类比是超链接。大多数人都可以理解,网页上的链接“指向”互联网上的另一页,如果您可以复制并粘贴该超链接,则它们都将指向同一原始网页。如果您去编辑该原始页面,然后单击这些链接(指针)中的任何一个,您将获得该新的更新页面。
int *a = b
不制作的两个副本一样*b
)。
指针似乎让很多人感到困惑的原因是,它们在计算机体系结构中大多没有或几乎没有背景。由于许多人似乎对计算机(机器)的实际实现方式一无所知,因此在C / C ++中工作似乎有些陌生。
钻研是要求他们实现一个简单的基于字节码的虚拟机(以他们选择的任何语言,python都可以很好地实现这一点),其指令集专注于指针操作(加载,存储,直接/间接寻址)。然后要求他们为该指令集编写简单的程序。
除了简单的加法之外,任何需要更多的东西都将涉及指针,他们肯定会得到它。
为什么对于许多使用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
也许通过示例可以解释一些基本知识?
首先,我很难理解指针的原因是,许多解释都包含大量关于引用传递的废话。所有这些都使问题变得混乱。当使用指针参数时,您仍在按值传递。但是该值恰好是地址而不是int。
其他人已经链接到本教程,但是我可以重点介绍我开始理解指针的时刻:
int puts(const char *s);
暂时,忽略
const.
传递给的参数puts()
是一个指针,即一个指针的值(因为C中的所有参数都按值传递),而指针的值就是它指向的地址,或者, 一个地址。因此,puts(strA);
如我们所见,当我们写时,我们正在传递strA [0]的地址。
当我阅读这些单词的那一刻,乌云散开,一束阳光笼罩着我,使我理解了指针。
即使您是VB .NET或C#开发人员(就我而言),并且从不使用不安全的代码,仍然值得了解指针的工作方式,否则您将不了解对象引用的工作方式。然后,您将拥有一个常见但容易出错的概念,即将对象引用传递给方法会复制该对象。
我发现Ted Jensen的“ C语言中的指针和数组教程”是学习指针的绝佳资源。它分为10节课,首先说明什么是指针(以及它们的作用),最后以函数指针结束。http://home.netcom.com/~tjensen/ptr/cpoint.htm
从那里继续前进,Beej的《网络编程指南》讲授Unix套接字API,您可以从中开始做一些真正有趣的事情。http://beej.us/guide/bgnet/
指针的复杂性超出了我们可以轻松教导的范围。让学生互相指点并使用带有住所地址的纸质都是很好的学习工具。他们在介绍基本概念方面做得很好。确实,学习基本概念对于成功使用指针至关重要。但是,在生产代码中,进入比这些简单演示所封装的复杂得多的场景是很常见的。
我参与的系统中,我们的结构指向其他结构,而结构指向其他结构。这些结构中的一些还包含嵌入式结构(而不是指向其他结构的指针)。这是指针真正令人困惑的地方。如果您具有多个间接级别,那么您将以如下代码开始结尾:
widget->wazzle.fizzle = fazzle.foozle->wazzle;
它会很快变得令人迷惑(想象更多的行,并可能会有更多的关卡)。放入指针数组和节点到节点指针(树,链接列表)的数组,情况还会变得更糟。我已经看到一些真正优秀的开发人员一旦开始在这样的系统上工作,他们就会迷失方向,甚至是那些非常了解基础知识的开发人员。
指针的复杂结构也不一定表示编码不好(尽管它们可以)。组合是良好的面向对象编程的重要组成部分,在带有原始指针的语言中,组合不可避免地会导致多层间接。此外,系统经常需要使用第三方库,这些库的结构在样式或技术上都不匹配。在这种情况下,自然会出现复杂性(尽管可以肯定,我们应该尽可能地与之抗争)。
我认为大学可以帮助学生学习指针的最好的方法就是使用良好的示范,并结合需要指针使用的项目。一个困难的项目对指针的理解将比一千个示范做更多的事情。演示可以使您有一个浅浅的了解,但是要深刻掌握指针,您必须真正使用它们。
我以为我会在这个列表上添加一个类比,当我作为计算机科学导师解释指针(回溯过去)时,我发现这非常有帮助;首先,让我们:
设置阶段:
考虑一个有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
),但他们有不同的值(blue
,red
,green
,这可能是一种颜色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)
)看到那里有辆绿色的汽车(该位置的数据)
"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"
-你会很惊讶有多少人不理解,甚至指针到这个水平
我认为指针作为一个概念并不是特别棘手-大多数学生的心理模型都映射到这样的东西,并且一些快速的方框草图可能会有所帮助。
困难,至少是我过去所经历的以及其他人所遇到的困难,是,不必要地使C / C ++中的指针管理变得困难。
乔尔·斯波斯基(Joel Spolsky)在他的《游击面试指南》中提出了一些有关理解指针的观点:
由于某些原因,大多数人似乎出生时就没有大脑中能理解指针的部分。这是一个天才的事情,而不是技巧的事情–它需要一些人无法做到的复杂形式的双重间接思维。
指针的问题不是概念。它涉及执行和语言。当教师认为困难的是指针的概念,而不是行话,或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
。您可以剥离评估层以了解中间类型必须是什么。
(*(*fn)(42))('x')
什么?
x
),如果您进行评估*x
,您将获得双倍奖励。
fn
,而多些你能做的事情有fn
我认为理解指针的主要障碍是糟糕的老师。
几乎每个人都被教导关于指针的谎言:它们不过是内存地址,或者它们使您可以指向任意位置。
当然,它们很难理解,既危险又半神奇。
都不是真的。指针实际上是相当简单的概念,只要您坚持使用C ++语言必须对它们说的话,并且不要使它们具有“通常”在实践中起作用的属性,但是该语言不能保证这些属性,因此也不属于指针的实际概念。
几个月前,我曾在博客中写过对此的解释-希望它能对某人有所帮助。
(请注意,在有人对我me之以鼻之前,是的,C ++标准确实指出了指针 代表内存地址。但是它并没有说“指针是内存地址,除了内存地址外什么都没有,并且可以与内存互换使用或想到”地址”。区别很重要)
很难理解的原因不是因为它是一个困难的概念,而是因为语法不一致。
int *mypointer;
您首先了解到,变量创建的最左边部分定义了变量的类型。指针声明在C和C ++中不能像这样工作。相反,他们说该变量指向左侧的类型。在这种情况下:*
mypointer 指向一个int。
直到我尝试在C#中使用指针(不安全)时,我才完全掌握指针,它们以完全相同的方式工作,但具有逻辑和一致的语法。指针本身就是类型。这里mypointer 是一个指向int的指针。
int* mypointer;
甚至不让我开始使用函数指针...
int *p;
有一个简单的含义:*p
是一个整数。int *p, **pp
表示:*p
和**pp
是整数。
*p
和**pp
是不是整数,因为你永远不初始化p
或pp
或*pp
以点什么。我了解为什么有些人喜欢在此语法上坚持使用语法,特别是因为某些极端情况和复杂情况要求您这样做(尽管如此,在我所知道的所有情况下,您都可以琐碎地解决此问题)...但是我认为,这些案例并不比教导正确的对齐方式会误导新手这一事实更为重要。更何况丑陋!:)
我喜欢内部地址的比喻,但我一直认为地址是邮箱本身。这样,您可以形象化取消引用指针(打开邮箱)的概念。
例如,下面是一个链接列表:1)从带有地址的纸张开始2)转到纸张上的地址3)打开邮箱以查找带有下一个地址的新纸张
在线性链接列表中,最后一个邮箱(列表结尾)中没有任何内容。在循环链接列表中,最后一个邮箱中有第一个邮箱的地址。
请注意,第3步是发生取消引用的地方,并且当地址无效时您将崩溃或出错。假设您可以走到无效地址的邮箱,想象一下那里有一个黑洞或东西,这会使世界变得反过来:)
我认为人们对此感到困惑的主要原因是因为通常没有以一种有趣且引人入胜的方式来教授它。我希望看到一位讲师从人群中招募10名志愿者,并给他们每人一个1米的直尺,让他们以一定的配置站立并使用直尺互相指点。然后通过使人们四处移动(以及他们指向标尺的位置)来显示指针算法。这是一种简单但有效(最重要的是令人难忘的)方式来展示概念,而又不会使技术陷入困境。
一旦您接触到C和C ++,对于某些人来说似乎会越来越难。我不确定这是否是因为他们最终将他们没有正确掌握的理论付诸实践,或者是因为在这些语言中指针操作本来就更难。我不太记得自己的转换,但是我知道 Pascal中的指针,然后移到C并完全迷失了。
我认为这实际上可能是语法问题。指针的C / C ++语法似乎不一致,并且比需要的语法更复杂。
具有讽刺意味的是,实际上帮助我理解指针的一件事是在c ++ 标准模板库中遇到了迭代器的概念。。具有讽刺意味的是,我只能假设迭代器被认为是指针的一般化。
有时,直到学会忽略树木,您才看不到森林。
(*p)
可以使用指针的话,肯定会更容易(p->)
,因此,我们p->->x
*p->x
a->b
简单地意味着(*a).b
。
* p->x
手段* ((*a).b)
而*p -> x
手段(*(*p)) -> x
。混合使用前缀和后缀运算符会导致模棱两可的解析。
1+2 * 3
应该是9
混淆来自在“指针”概念中混合在一起的多个抽象层。程序员不会被Java / Python中的普通引用所迷惑,但是指针的不同之处在于它们公开了底层内存体系结构的特征。
干净地分离抽象层是一个好原则,而指针则不能这样做。
foo[i]
意味着要去某个地方,向前走一段距离,然后看看那里有什么。使事情复杂化的是标准为了纯粹编译器的利益而添加的更加复杂的额外抽象层,但是对事物进行建模的方式不适合程序员和编译器需求。
我喜欢用数组和索引来解释它的方式-人们可能不熟悉指针,但是他们通常知道索引是什么。
因此,我想像一下,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]]
。
我会在纸张列表上绘制一个数组,然后用它来显示诸如指向同一内存的许多指针,指针算术,指针指针之类的东西。
通过迭代器来掌握它不是一个坏方法。但是不断寻找,您会看到Alexandrescu开始抱怨它们。
许多前C ++开发人员(在转储该语言之前从未理解过迭代器是现代的指针)跳到C#并仍然相信他们拥有不错的迭代器。
嗯,问题在于所有迭代器都与运行时平台(Java / CLR)试图实现的目标完全相悖:新的,简单的,每个人都是开发人员的用法。可能不错,但是他们在紫色书中曾说过一次,甚至在C语言之前和之前都说过:
间接
这是一个非常强大的概念,但如果您始终这样做,则永远不会如此。.迭代器非常有用,因为它们有助于抽象算法,这是另一个示例。编译时是算法的地方,非常简单。您知道代码+数据,或者使用其他C#语言:
IEnumerable + LINQ + Massive Framework = 300MB糟糕的运行时间接代价,通过引用类型实例的堆拖动应用程序。
“勒珀特很便宜。”
上面的一些回答断言“指针并不难”,但并没有直接解决“指针难”的问题。来自。几年前,我曾辅导过一年级的CS学生(仅一年,因为我很清楚),所以我很清楚指针的想法并不难。很难理解为什么以及何时需要指针。
我认为您不能将这个问题(为什么以及何时使用指针)与解释更广泛的软件工程问题区分开来。为什么每个变量都不应该是全局变量,为什么每个变量都应该将相似的代码分解为函数(要做到这一点,请使用指针将其行为专门用于其调用位置)。
只是为了使事情更加混乱,有时您必须使用句柄而不是指针。句柄是指向指针的指针,以便后端可以移动内存中的内容以对堆进行碎片整理。如果指针在例行程序中更改,则结果是不可预测的,因此您首先必须锁定句柄以确保任何地方都不会移动。
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5比我讲得更连贯。:-)
每个C / C ++初学者都有相同的问题,发生此问题的原因不是因为“指针很难学习”,而是因为“谁以及如何解释它”。一些学习者从视觉上以口头形式收集它,而最好的解释方式是使用“训练”示例(适合口头和视觉示例)。
其中“机车”是不能容纳任何东西的指针,而“旅行车”是“机车”试图拉(或指向)的指针。之后,您可以对“旅行车”本身进行分类,可以容纳动物,植物或人(或它们的混合物)。