理解指针有什么障碍,克服它们有什么可以做的?
为什么在C或C ++中,许多新的,甚至是老年的大学生的混乱的主要原因是什么? 是否有任何工具或思维过程帮助您了解指针在variables,函数和超越级别上的工作方式?
有什么好的做法可以把人带到“啊,我明白了”的水平,而不会让他们陷入整体概念? 基本上,钻出像情景。
指针是一个概念,对于许多人来说,最初可能会引起混淆,特别是当涉及到复制指针值并仍然引用相同的内存块时。
我发现最好的比喻是将指针视为一张纸,上面有一个房子地址,而它所指的内存块就是实际的房子。 各种各样的操作可以很容易地解释。
我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释。 我select了Delphi,因为我的另一个主要的编程语言C#并没有以同样的方式显示内存泄漏等内容。
如果您只想学习指针的高级概念,那么您应该在下面的解释中忽略标记为“内存布局”的部分。 它们的目的是举例说明操作之后内存的样子,但是它们本质上是更低级的。 但是,为了准确地解释缓冲区溢出如何真正起作用,添加这些图很重要。
免责声明:对于所有的意图和目的,这个解释和示例内存布局大大简化。 有更多的开销和更多的细节,你需要知道,如果你需要处理低层次的内存。 但是,为了解释内存和指针,它是足够准确的。
假设下面使用的THouse类如下所示:
type THouse = class private FName : array[0..9] of Char; public constructor Create(name: PChar); end;
初始化房屋对象时,赋予构造函数的名称被复制到专用字段FName中。 有一个原因被定义为一个固定大小的数组。
在内存中,会有一些与房屋分配有关的开销,我将在下面进行说明:
--- [ttttNNNNNNNNNN] --- ^ ^ | | | + - FName数组 | + - 开销
“tttt”区域是开销,对于各种types的运行时和语言,通常会有更多的这种情况,如8或12字节。 无论存储分配器还是核心系统例程,任何值都不会被存储在这个区域中,或者有可能导致程序崩溃。
分配内存
找个企业家盖房子,把房子的地址告诉你。 与现实世界相反,内存分配不能告诉分配的位置,但会find足够空间的适当位置,并将地址报告给分配的内存。
换句话说,企业家会select现货。
THouse.Create('My house');
内存布局:
--- [ttttNNNNNNNNNN] --- 1234我的房子
用地址保留一个variables
把这个地址写在你的新房子里,放在一张纸上。 本文将作为你参考你的房子。 没有这张纸,你就迷路了,除非你已经在这里,否则找不到房子。
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 ...
H1 v --- [ttttNNNNNNNNNN] --- 1234我的房子 ^ H2
释放内存
拆毁房子。 如果你愿意的话,你可以稍后重新使用这个文件作为一个新的地址,或者清除它来忘记那个不再存在的地址。
var h: THouse; begin h := THouse.Create('My house'); ... h.Free; h := nil;
在这里,我首先build造房子,并掌握它的地址。 然后,我做了一些事情(使用它,代码,留给读者做一个练习),然后我释放它。 最后,我清除我的variables的地址。
内存布局:
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
在打电话给.Free
之后使用h
可能会有效,但这只是纯粹的运气。 在一个关键的操作过程中,很可能在客户的地方失败。
h < - + v + - 免费之前 --- [ttttNNNNNNNNNN] --- | 1234我的房子< - + h < - + v + - 免费后 ---------------------- | xx34我的房子< - +
正如你所看到的,h仍然指向内存中的数据的残余,但是因为它可能不完整,所以像以前一样使用它可能会失败。
内存泄漏
你失去了这张纸,找不到房子。 房子仍然站在某个地方,而当你以后想要build造一所新房子时,你不能重用那个地方。
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
variables的内容,但是旧房子仍然站在某处。 在这个守则之后,没有办法到达那个房子,它将被放置。 换句话说,分配的内存将保持分配状态,直到应用程序closures,此时操作系统将会closures它。
第一次分配后的内存布局:
H v --- [ttttNNNNNNNNNN] --- 1234我的房子
第二次分配后的内存布局:
H v --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN] 1234我的房子5678我的房子
获得这种方法的一个更常见的方法就是忘记释放某些东西,而不是像上面那样覆盖它。 用德尔斐术语来说,这将通过以下方法进行:
procedure OpenTheFrontDoorOfANewHouse; var h: THouse; begin h := THouse.Create('My house'); h.OpenFrontDoor; // uh-oh, no .Free here, where does the address go? end;
这种方法执行后,我们的variables中没有地址存在,但房子仍然在那里。
内存布局:
h < - + v + - 丢失指针之前 --- [ttttNNNNNNNNNN] --- | 1234我的房子< - + h(现在无处)< - + + - 丢失指针后 --- [ttttNNNNNNNNNN] --- | 1234我的房子< - +
正如你所看到的,旧数据在内存中保持不变,不会被内存分配器重用。 分配器会跟踪哪些内存区域已被使用,除非您释放它,否则不会重用它们。
释放内存,但保留(现在是无效的)引用
拆掉房子,擦掉其中一张纸,但是还有另外一张纸,上面写着旧地址,当你去地址时,你不会find房子,但是你可能会发现类似于废墟的东西的一个。
也许你甚至会find一所房子,但这不是你最初给予的地址,因此任何使用它的尝试都可能会失败。
有时你甚至可能会发现邻近的地址有一个相当大的房子,占据三个地址(主要街道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
仍然有旧的,过时的地址。 进入不再站立的房屋可能会或可能不会工作。
这是上面悬挂指针的变体。 看到它的内存布局。
缓冲区溢出
你把更多的东西放进房子里,而不是放在邻居家或者院子里。 当那个邻居家的主人回家后,他会发现他会认为他自己的各种各样的东西。
这是我select一个固定大小的数组的原因。 为了设定舞台,假设我们分配的第二间房子出于某种原因将被放置在第一个房屋之前。 换句话说,第二宫的地址比第一宫低。 而且,它们分配在一起。
因此,这个代码:
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] 5678我的房子
第二次分配后的内存布局:
h2 h1 VV --- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN] 我的其他房子在哪里 ^ - + - ^ | + - 覆盖
最经常导致崩溃的部分是当您覆盖您存储的数据的重要部分时,确实不应该随机更改。 例如,可能不是一个问题,部分名称的房子被改变,就崩溃的程序而言,但覆盖对象的开销很可能会崩溃,当你尝试使用破碎的对象,将覆盖存储到对象中的其他对象的链接。
链接列表
当你在一张纸上写下一个地址时,你到了一个房子,在那个房子里有另外一张纸,上面有一个新的地址,下一个房子在这个链上,等等。
var h1, h2: THouse; begin h1 := THouse.Create('Home'); h2 := THouse.Create('Cabin'); h1.NextHouse := h2;
在这里,我们创build一个从我们的家到我们的小屋的链接。 我们可以跟随这个链,直到一个房子没有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] 1234Home + 5678Cabin + | ^ | + -------- + *(无链接)
基本而言,什么是内存地址?
内存地址基本上只是一个数字。 如果将内存看作是一大串字节,则第一个字节的地址为0,下一个字节的地址为1,依此类推。 这是简化的,但足够好。
所以这个内存布局:
h1 h2 VV --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN] 1234我的房子5678我的房子
可能有这两个地址(最左边的是地址0):
- h1 = 4
- h2 = 23
这意味着我们上面的链接列表可能看起来像这样:
h1(= 4)h2(= 28) VV --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL] 1234Home 0028 5678Cabin 0000 | ^ | + -------- + *(无链接)
通常将“无处指向”地址存储为零地址。
基本而言,什么是指针?
指针只是一个保存内存地址的variables。 通常可以让程序devise语言给出它的编号,但是大多数编程语言和运行时都试图隐藏下面有一个数字的事实,只是因为数字本身对你没有任何意义。 最好把一个指针看作一个黑盒子,也就是说。 你不会真正知道或关心它是如何实现的,只要它能够工作。
在我的第一个Comp Sci类中,我们做了以下练习。 当然,这是一个大约有200名学生的演讲厅。
教授在董事会上写道: int john;
约翰站起来
教授写道: int *sally = &john;
莎莉站起来,指向约翰
教授: int *bill = sally;
比尔站起来,指着约翰
教授: int sam;
山姆站起来
教授: bill = &sam;
比尔现在指向萨姆。
我想你明白了。 我想我们花了大约一个小时这样做,直到我们完成了指针分配的基础知识。
一个比喻我发现有用的解释指针是超链接。 大多数人可以理解,网页上的链接“指向”互联网上的另一个页面,如果您可以复制并粘贴该超链接,则它们都将指向相同的原始网页。 如果你去编辑原始页面,然后按照任何一个链接(指针),你会得到新的更新页面。
指针似乎混淆了这么多人的原因是,他们大多是在计算机体系结构很less或根本没有背景。 因为很多人似乎并不知道如何实现计算机(机器) – 在C / C ++中工作似乎是陌生的。
一个演练就是让他们用一个专注于指针操作(加载,存储,直接/间接寻址)的指令集来实现一个简单的基于字节码的虚拟机(使用任何他们select的语言,python对此非常好)。 然后让他们为这个指令集编写简单的程序。
任何需要略多于简单的加法的东西都会涉及到指针,他们一定会得到它。
为什么C / C ++语言中的许多新的,甚至是老年的大学学生都混淆了这种混乱的主要原因?
价值占位符的概念 – variables – 映射到我们在学校所教的东西 – 代数。 如果不理解内存是如何在计算机中进行物理布局的,现在还没有一种平行的方法,在C / C ++ / byte通信级别处理低级别的事情之前,没有人会考虑这种事情。
是否有任何工具或思维过程帮助您了解指针在variables,函数和超越级别上的工作方式?
地址框。 我记得当我正在学习将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
也许这通过例子解释了一些基础知识?
我之所以难以理解指针,是因为很多解释中都包含了大量有关通过引用的废话。 这一切都是混淆的问题。 当你使用一个指针参数时,你仍然在传递值; 但价值恰好是一个地址,而不是一个整数。
其他人已经链接到本教程,但我可以强调我开始理解指针的那一刻:
C中的指针和数组教程:第3章 – 指针和string
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的“networking编程指南”教导了Unix套接字API,从中你可以开始做有趣的事情。 http://beej.us/guide/bgnet/
指针的复杂性超出了我们可以轻易教导的范围。 让学生相互指点,并使用带有住址的纸片都是很好的学习工具。 他们在介绍基本概念方面做得很好。 确实,学习基本概念对于成功使用指针是至关重要的 。 但是,在生产代码中,进入比这些简单的演示封装更复杂的场景是很常见的。
我一直参与系统,我们有指向其他结构指向其他结构的结构。 其中一些结构还包含embedded式结构(而不是指向其他结构的指针)。 这是指针变得非常混乱的地方。 如果你有多个层次的间接性,你会开始像这样的代码:
widget->wazzle.fizzle = fazzle.foozle->wazzle;
它可能会很快混淆(想象更多的线条,并可能更多的水平)。 抛出指针数组,节点指针(树,链表),它变得更糟。 我看到一些真正优秀的开发人员在开始研究这样的系统时就迷失了方向,甚至是那些了解基本知识的开发人员。
指针的复杂结构不一定表示编码不好,尽pipe它们可以。 组合是一个很好的面向对象编程的重要部分,在使用原始指针的语言中,它将不可避免地导致多层间接。 此外,系统通常需要使用第三方库,其结构在风格或技术上彼此不匹配。 在这样的情况下,复杂性自然就会出现(当然,我们应该尽可能地去对抗)。
我认为学院可以做的最好的事情是帮助学生学习指针,就是使用好的演示,结合需要使用指针的项目。 一个困难的项目比起一千个示范,对于指针理解来说更多。 示范可以让你有一个浅显的理解,但要深入掌握指针,你必须真正使用它们。
我不认为指针作为一个概念是特别棘手的 – 大多math生的心智模式映射到这样的东西,一些快速框图可以帮助。
至less我过去经历过的并且看到其他人处理的困难是,C / C ++中指针的pipe理可能会在一开始就变得复杂起来。
我想我会在这个列表中添加一个类比,当我作为计算机科学导师解释指针(当天)时,我发现它非常有帮助; 首先,让我们:
设置阶段 :
考虑一个3个停车位,这些空间被编号为:
------------------- | | | | | 1 | 2 | 3 | | | | |
在某种程度上,这就像内存位置,它们是连续的,连续的。有点像数组。 现在没有汽车,所以它就像一个空arrays( parking_lot[3] = {0}
)。
添加数据
一个停车场永远不会空置很长时间…如果这样做是毫无意义的,没有人会build立任何。 所以,让我们说,当这一天的行程充满了3辆车,一辆蓝色的车,一辆红色的汽车和一辆绿色的汽车:
1 2 3 ------------------- | o=o | o=o | o=o | | |B| | |R| | |G| | | oo | oo | oo |
这些汽车都是相同的types(汽车),所以想到这一点的一种方式是,我们的汽车是某种数据(如int
),但它们有不同的值( blue
, red
, green
,这可能是一个颜色enum
)
input指针
现在,如果我带你进入这个停车场,并要求你find我一辆蓝色的汽车,那么你伸出一根手指,用它指向一号车的一辆蓝色汽车。这就像拿一个指针并把它分配给一个内存地址( int *finger = parking_lot
)
你的手指(指针)不是我的问题的答案。 看着你的手指什么都不告诉我,但是如果我看看你的手指指向哪里(取消指针),我可以find我正在寻找的汽车(数据)。
重新分配指针
现在我可以要求你find一辆红色的车,而你可以将你的手指换到一辆新车上。 现在你的指针(和以前一样)显示了同样types(汽车)的新数据(红色汽车可以find的停车位)。
指针没有物理变化,它仍然是你的手指,只是它显示我改变的数据。 (“停车位”地址)
双指针(或指向指针的指针)
这也适用于多个指针。 我可以问指针在哪里,指向红色的车,你可以用另一只手,用手指指向第一根手指。 (这就像int **finger_two = &finger
)
现在,如果我想知道蓝色的车是哪里,我可以按照第一根手指的方向,第二个手指,汽车(数据)。
摇晃的指针
现在让我们说,你感觉非常像一个雕像,你想无限期地握着你的手指着红色的车。 如果那辆红色轿车开走了怎么办?
1 2 3 ------------------- | o=o | | o=o | | |B| | | |G| | | oo | | oo |
你的指针仍然指向红色车的位置,但不再是。 比方说,一辆新车在那里拉…橙色的车。 现在,如果我再次问你,“红色车在哪里”,你仍然指着那里,但现在你错了。 那不是红色的车,那是橙色的。
指针算术
好的,所以你仍然指着第二个停车位(现在被橙色汽车占用)
1 2 3 ------------------- | o=o | o=o | o=o | | |B| | |O| | |G| | | oo | oo | oo |
Well I have a new question now… I want to know the color of the car in the next parking spot. You can see you're pointing at spot 2, so you just add 1 and you're pointing at the next spot. ( finger+1
), now since I wanted to know what the data was there, you have to check that spot (not just the finger) so you can deference the pointer ( *(finger+1)
) to see there is a green car present there (the data at that location)
An example of a tutorial with a good set of diagrams helps greatly with the understanding of pointers .
Joel Spolsky makes some good points about understanding pointers in his Guerrilla Guide to Interviewing article:
For some reason most people seem to be born without the part of the brain that understands pointers. This is an aptitude thing, not a skill thing – it requires a complex form of doubly-indirected thinking that some people just can't do.
I think the main barrier to understanding pointers is bad teachers.
Almost everyone are taught lies about pointers: That they are nothing more than memory addresses , or that they allow you to point to arbitrary locations .
And of course that they are difficult to understand, dangerous and semi-magical.
None of which is true. Pointers are actually fairly simple concepts, as long as you stick to what the C++ language has to say about them and don't imbue them with attributes that "usually" turn out to work in practice, but nevertheless aren't guaranteed by the language, and so aren't part of the actual concept of a pointer.
I tried to write up an explanation of this a few months ago in this blog post — hopefully it'll help someone.
(Note, before anyone gets pedantic on me, yes, the C++ standard does say that pointers represent memory addresses. But it does not say that "pointers are memory addresses, and nothing but memory addresses and may be used or thought of interchangeably with memory addresses". The distinction is important)
The problem with pointers is not the concept. It's the execution and language involved. Additional confusion results when teachers assume that it's the CONCEPT of pointers that's difficult, and not the jargon, or the convoluted mess C and C++ makes of the concept. So vast amounts of effort are poored into explaining the concept (like in the accepted answer for this question) and it's pretty much just wasted on someone like me, because I already understand all of that. It's just explaining the wrong part of the problem.
To give you an idea of where I'm coming from, I'm someone who understands pointers perfectly well, and I can use them competently in assembler language. Because in assembler language they are not referred to as pointers. They are referred to as addresses. When it comes to programming and using pointers in C, I make a lot of mistakes and get really confused. I still have not sorted this out. Let me give you an example.
When an api says:
int doIt(char *buffer ) //*buffer is a pointer to the buffer
what does it want?
it could want:
a number representing an address to a buffer
(To give it that, do I say doIt(mybuffer)
, or doIt(*myBuffer)
?)
a number representing the address to an address to a buffer
(is that doIt(&mybuffer)
or doIt(mybuffer)
or doIt(*mybuffer)
?)
a number representing the address to the address to the address to the buffer
(maybe that's doIt(&mybuffer)
. or is it doIt(&&mybuffer)
? or even doIt(&&&mybuffer)
)
and so on, and the language involved doesn't make it as clear because it involves the words "pointer" and "reference" that don't hold as much meaning and clarity to me as "x holds the address to y" and "this function requires an address to y". The answer additionally depends on just what the heck "mybuffer" is to begin with, and what doIt intends to do with it. The language doesn't support the levels of nesting that are encountered in practice. Like when I have to hand a "pointer" in to a function that creates a new buffer, and it modifies the pointer to point at the new location of the buffer. Does it really want the pointer, or a pointer to the pointer, so it knows where to go to modify the contents of the pointer. Most of the time I just have to guess what is meant by "pointer" and most of the time I'm wrong, regardless of how much experience I get at guessing.
"Pointer" is just too overloaded. Is a pointer an address to a value? or is it a variable that holds an address to a value. When a function wants a pointer, does it want the address that the pointer variable holds, or does it want the address to the pointer variable? 我很困惑。
I think that what makes pointers tricky to learn is that until pointers you're comfortable with the idea that "at this memory location is a set of bits that represent an int, a double, a character, whatever".
When you first see a pointer, you don't really get what's at that memory location. "What do you mean, it holds an address ?"
I don't agree with the notion that "you either get them or you don't".
They become easier to understand when you start finding real uses for them (like not passing large structures into functions).
The reason it's so hard to understand is not because it's a difficult concept but because the syntax is inconsistent .
int *mypointer;
You are first learned that the leftmost part of a variable creation defines the type of the variable. Pointer declaration does not work like this in C and C++. Instead they say that the variable is pointing on the type to the left. In this case: *
mypointer is pointing on an int.
I didn't fully grasp pointers until i tried using them in C# (with unsafe), they work in exact same way but with logical and consistent syntax. The pointer is a type itself. Here mypointer is a pointer to an int.
int* mypointer;
Don't even get me started on function pointers…
I could work with pointers when I only knew C++. I kind of knew what to do in some cases and what not to do from trial/error. But the thing that gave me complete understanding is assembly language. If you do some serious instruction level debugging with an assembly language program you've written, you should be able to understand a lot of things.
I think that the main reason that people have trouble with it is because it's generally not taught in an interesting and engaging manner. I'd like to see a lecturer get 10 volunteers from the crowd and give them a 1 meter ruler each, get them to stand around in a certain configuration and use the rulers to point at each other. Then show pointer arithmetic by moving people around (and where they point their rulers). It'd be a simple but effective (and above all memorable) way of showing the concepts without getting too bogged down in the mechanics.
Once you get to C and C++ it seems to get harder for some people. I'm not sure if this is because they are finally putting theory that they don't properly grasp into practice or because pointer manipulation is inherently harder in those languages. I can't remember my own transition that well, but I knew pointers in Pascal and then moved to C and got totally lost.
I like the house address analogy, but I've always thought of the address being to the mailbox itself. This way you can visualize the concept of dereferencing the pointer (opening the mailbox).
For instance following a linked list: 1) start with your paper with the address 2) Go to the address on the paper 3) Open the mailbox to find a new piece of paper with the next address on it
In a linear linked list, the last mailbox has nothing in it (end of the list). In a circular linked list, the last mailbox has the address of the first mailbox in it.
Note that step 3 is where the dereference occurs and where you'll crash or go wrong when the address is invalid. Assuming you could walk up to the mailbox of an invalid address, imagine that there's a black hole or something in there that turns the world inside out 🙂
I don't think that pointers themselves are confusing. Most people can understand the concept. Now how many pointers can you think about or how many levels of indirection are you comfortable with. It doesn't take too many to put people over the edge. The fact that they can be changed accidently by bugs in your program can also make them very difficult to debug when things go wrong in your code.
I think it might actually be a syntax issue. The C/C++ syntax for pointers seems inconsistent and more complex than it needs to be.
Ironically, the thing that actually helped me to understand pointers was encountering the concept of an iterator in the c++ Standard Template Library . It's ironic because I can only assume that iterators were conceived as a generalization of the pointer.
Sometimes you just can't see the forest until you learn to ignore the trees.
The confusion comes from the multiple abstraction layers mixed together in the "pointer" concept. Programmers don't get confused by ordinary references in Java/Python, but pointers are different in that they expose characteristics of the underlying memory-architecture.
It is a good principle to cleanly separate layers of abstraction, and pointers do not do that.
The way I liked to explain it was in terms of arrays and indexes – people might not be familiar with pointers, but they generally know what an index is.
So I say imagine that the RAM is an array (and you have only 10-bytes of RAM):
unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };
Then a pointer to a variable is really just the index of (the first byte of) that variable in the RAM.
So if you have a pointer/index unsigned char index = 2
, then the value is obviously the third element, or the number 4. A pointer to a pointer is where you take that number and use it as an index itself, like RAM[RAM[index]]
.
I would draw an array on a list of paper, and just use it to show things like many pointers pointing to the same memory, pointer arithmetic, pointer to pointer, and so on.
I don't see what is so confusing about pointers. They point to a location in memory, that is it stores the memory address. In C/C++ you can specify the type the pointer points to. 例如:
int* my_int_pointer;
Says that my_int_pointer contains the address to a location that contains an int.
The problem with pointers is that they point to a location in memory, so it is easy to trail off into some location you should not be in. As proof look at the numerous security holes in C/C++ applications from buffer overflow (incrementing the pointer past the allocated boundary).
Not a bad way to grasp it, via iterators.. but keep looking you'll see Alexandrescu start complaining about them.
Many ex-C++ devs (that never understood that iterators are a modern pointer before dumping the language) jump to C# and still believe they have decent iterators.
Hmm, the problem is that all that iterators are is in complete odds at what the runtime platforms (Java/CLR) are trying to achieve: new, simple, everyone-is-a-dev usage. Which can be good, but they said it once in the purple book and they said it even before and before C:
Indirection.
A very powerful concept but never so if you do it all the way.. Iterators are useful as they help with abstraction of algorithms, another example. And compile-time is the place for an algorithm, very simple. You know code + data, or in that other language C#:
IEnumerable + LINQ + Massive Framework = 300MB runtime penalty indirection of lousy, dragging apps via heaps of instances of reference types..
"Le Pointer is cheap."
Some answers above have asserted that "pointers aren't really hard", but haven't gone on to address directly where "pointer are hard!" comes from. Some years back I tutored first year CS students (for only one year, since I clearly sucked at it) and it was clear to me that the idea of pointer is not hard. What's hard is understanding why and when you would want a pointer .
I don't think you can divorce that question – why and when to use a pointer – from explaining broader software engineering issues. Why every variable should not be a global variable, and why one should factor out similar code into functions (that, get this, use pointers to specialize their behaviour to their call site).
Just to confuse things a bit more, sometimes you have to work with handles instead of pointers. Handles are pointers to pointers, so that the back end can move things in memory to defragment the heap. If the pointer changes in mid-routine, the results are unpredictable, so you first have to lock the handle to make sure nothing goes anywhere.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 talks about it a bit more coherently than me. 🙂
Post office box number.
It's a piece of information that allows you to access something else.
(And if you do arithmetic on post office box numbers, you may have a problem, because the letter goes in the wrong box. And if somebody moves to another state — with no forwarding address — then you have a dangling pointer. On the other hand — if the post office forwards the mail, then you have a pointer to a pointer.)
Every C/C++ beginner has the same problem and that problem occurs not because "pointers are hard to learn" but "who and how it is explained". Some learners gather it verbally some visually and the best way of explaining it is to use "train" example (suits for verbal and visual example).
Where "locomotive" is a pointer which can not hold anything and "wagon" is what "locomotive" tries pull (or point to). After, you can classify the "wagon" itself, can it hold animals,plants or people (or a mix of them).