为什么指针对于许多 C 或 C++ 的新的甚至是老的大学生来说是一个如此混乱的主要因素?是否有任何工具或思维过程可以帮助您了解指针如何在变量、函数和其他级别上工作?
有哪些好的实践可以让某人达到“啊哈,我明白了”的水平,而不会让他们陷入整体概念中?基本上,像场景一样钻取。
指针是一个对许多人来说一开始可能会感到困惑的概念,特别是在复制指针值并仍然引用同一个内存块时。
我发现最好的类比是将指针视为一张纸,上面有房子地址,它引用的内存块是实际房子。因此可以很容易地解释各种操作。
我在下面添加了一些 Delphi 代码,并在适当的地方添加了一些注释。我选择了 Delphi,因为我的其他主要编程语言 C# 不会以同样的方式表现出内存泄漏等问题。
如果您只想学习指针的高级概念,那么您应该忽略下面说明中标记为“内存布局”的部分。它们旨在举例说明操作后内存的外观,但它们本质上更底层。但是,为了准确解释缓冲区溢出的实际工作原理,我添加了这些图表非常重要。
免责声明:出于所有意图和目的,此解释和示例内存布局已大大简化。如果需要在低级别处理内存,则需要了解更多开销和更多详细信息。但是,对于解释内存和指针的意图,它已经足够准确了。
假设下面使用的 THouse 类如下所示:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
当你初始化房子对象时,给构造函数的名字被复制到私有字段 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
...
h1 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
在调用 to 之后使用.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我家<--+
如您所见,旧数据原封不动地留在内存中,不会被内存分配器重用。分配器跟踪已使用的内存区域,除非您释放它,否则不会重用它们。
释放内存但保留(现在无效)引用
拆掉房子,擦掉一张纸,但你还有另一张纸,上面写着旧地址,当你去地址时,你不会找到房子,但你可能会找到类似废墟的东西之一。
也许你甚至会找到一所房子,但它不是你最初获得地址的房子,因此任何试图将它当作属于你的使用都可能会失败。
有时你甚至会发现相邻的地址有一个相当大的房子,占据了三个地址(Main Street 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
但仍然有旧的、过时的地址。进入不再站立的房子可能会也可能不会。
这是上面悬空指针的变体。查看它的内存布局。
缓冲区溢出
你搬进房子里的东西超出了你的承受能力,溢出到邻居的房子或院子里。等隔壁房子的主人以后回家时,他会发现各种他认为属于自己的东西。
这就是我选择固定大小数组的原因。为了做好准备,假设我们分配的第二个房子由于某种原因将放在内存中的第一个之前。换句话说,第二个房子的地址将低于第一个房子。此外,它们被分配在彼此旁边。
因此,这段代码:
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] 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;
比尔现在指向山姆。
我想你应该已经明白了。我想我们花了大约一个小时来做这件事,直到我们了解了指针分配的基础知识。
我发现有助于解释指针的一个类比是超链接。大多数人都可以理解,网页上的链接“指向”互联网上的另一个页面,如果您可以复制并粘贴该超链接,那么它们都将指向同一个原始网页。如果您去编辑该原始页面,然后按照这些链接(指针)中的任何一个,您将获得该新的更新页面。
指针似乎让很多人感到困惑的原因是,它们大多很少或根本没有计算机体系结构的背景。由于许多人似乎不知道计算机(机器)是如何实际实现的——在 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://web.archive.org/web/20181011221220/http://home.netcom.com:80/~tjensen/ptr/cpoint.htm
从那里继续,Beej 的网络编程指南讲授了 Unix 套接字 API,您可以从中开始做真正有趣的事情。http://beej.us/guide/bgnet/
指针的复杂性超出了我们可以轻松教授的范围。让学生互相指点并使用带有家庭地址的纸片都是很好的学习工具。他们在介绍基本概念方面做得很好。事实上,学习基本概念对于成功使用指针至关重要。但是,在生产代码中,通常会遇到比这些简单演示所能封装的复杂得多的场景。
我参与过系统,我们有指向其他结构的结构指向其他结构。其中一些结构还包含嵌入式结构(而不是指向其他结构的指针)。这是指针真正令人困惑的地方。如果您有多个间接级别,并且您开始使用如下代码结束:
widget->wazzle.fizzle = fazzle.foozle->wazzle;
它很快就会变得混乱(想象更多的行,可能还有更多的关卡)。放入指针数组和节点到节点的指针(树、链表),情况会变得更糟。我见过一些真正优秀的开发人员一旦开始在这样的系统上工作就会迷失方向,即使是非常了解基础知识的开发人员。
指针的复杂结构也不一定表示编码不佳(尽管可以)。组合是良好的面向对象编程的重要组成部分,在具有原始指针的语言中,它不可避免地会导致多层间接。此外,系统通常需要使用在样式或技术上彼此不匹配的结构的第三方库。在这种情况下,复杂性自然会出现(当然,我们应该尽可能地与之抗争)。
我认为大学可以帮助学生学习指针的最好的事情就是使用好的演示,结合需要使用指针的项目。一个困难的项目比一千次演示更能帮助理解指针。演示可以让你理解浅薄,但要深入掌握指针,你必须真正使用它们。
我不认为指针作为一个概念特别棘手——大多数学生的心智模型都映射到这样的东西,一些快速的方框草图可以提供帮助。
至少我过去经历过并看到其他人处理的困难在于,C/C++ 中的指针管理可能会变得不必要地复杂化。
我想我应该在这个列表中添加一个类比,当我作为计算机科学导师解释指针时(过去),我发现它非常有用;首先,让我们:
设置舞台:
考虑一个有 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 |
这些汽车都是同一种类型(汽车),所以一种思考方式是,我们的汽车是某种数据(比如 an 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)
) 来查看那里有一辆绿色汽车(那个位置的数据)
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”是从什么开始的,以及 doIt 打算用它做什么。该语言不支持在实践中遇到的嵌套级别。就像当我必须将“指针”交给创建新缓冲区的函数时,它会修改指针以指向缓冲区的新位置。它真的要指针,还是指向指针的指针,所以它知道去哪里修改指针的内容。大多数时候我只需要猜测“”是什么意思
“指针”太重了。指针是指向值的地址吗?或者它是一个将地址保存到值的变量。当一个函数想要一个指针时,它是想要指针变量保存的地址,还是想要指针变量的地址?我很困惑。
我认为理解指针的主要障碍是糟糕的老师。
几乎每个人都被教导关于指针的谎言:它们只不过是内存地址,或者它们允许您指向任意位置。
当然,它们是难以理解的、危险的和半魔法的。
这些都不是真的。指针实际上是相当简单的概念,只要你坚持 C++ 语言对它们的说法,并且不要给它们灌输“通常”在实践中可以工作的属性,但语言不能保证,因此不是指针实际概念的一部分。
几个月前,我试图在这篇博文中对此进行解释——希望它会对某人有所帮助。
(注意,在任何人对我迂腐之前,是的,C++ 标准确实说指针代表内存地址。但它并没有说“指针是内存地址,只不过是内存地址,可以与内存互换使用或考虑地址”。区别很重要)
我认为使指针难以学习的原因在于,在指针之前,您对“在此内存位置是一组表示 int、double、字符等的位”的想法感到满意。
当您第一次看到指针时,您并没有真正了解该内存位置的内容。“你的意思是,它有一个地址?”
我不同意“要么得到它们要么不得到它们”的概念。
当您开始为它们找到真正的用途时(例如不将大型结构传递给函数),它们会变得更容易理解。
之所以这么难理解,不是因为它是一个难懂的概念,而是因为语法不一致。
int *mypointer;
您首先了解到变量创建的最左边部分定义了变量的类型。指针声明在 C 和 C++ 中不能这样工作。相反,他们说变量指向左侧的类型。在这种情况下:*
mypointer指向一个 int。
我没有完全掌握指针,直到我尝试在 C# 中使用它们(不安全),它们以完全相同的方式工作,但具有逻辑和一致的语法。指针本身就是一个类型。这里mypointer是一个指向 int 的指针。
int* mypointer;
甚至不要让我开始使用函数指针......
当我只知道 C++ 时,我可以使用指针。我有点知道在某些情况下该做什么,以及从试错中不该做什么。但让我完全理解的是汇编语言。如果你用你编写的汇编语言程序做一些严肃的指令级调试,你应该能够理解很多东西。
我喜欢房屋地址的类比,但我一直认为地址是邮箱本身。通过这种方式,您可以可视化取消引用指针(打开邮箱)的概念。
例如遵循一个链接列表:1)从你的论文开始,地址为 2)转到论文上的地址 3)打开邮箱找到一张新的论文,上面有下一个地址
在线性链表中,最后一个邮箱中没有任何内容(链表的末尾)。在循环链表中,最后一个邮箱包含第一个邮箱的地址。
请注意,第 3 步是取消引用发生的地方,当地址无效时,您将在此崩溃或出错。假设你可以走到一个无效地址的邮箱,想象那里有一个黑洞或什么东西可以把世界翻过来:)
我认为人们对它有困难的主要原因是因为它通常没有以有趣和引人入胜的方式教授。我希望看到一个讲师从人群中抽出 10 名志愿者,给他们每人一把 1 米的尺子,让他们以一定的姿势站着,用尺子互相指点。然后通过移动人们(以及他们指向标尺的位置)来显示指针算术。这将是一种简单但有效(最重要的是令人难忘)的展示概念的方式,而不会陷入机制中。
一旦你接触到 C 和 C++,对某些人来说似乎变得更难了。我不确定这是因为他们最终将他们没有正确掌握的理论付诸实践,还是因为在这些语言中指针操作本质上更难。我不太记得自己的转换,但我知道Pascal 中的指针,然后转到 C 并完全迷失了方向。
我不认为指针本身令人困惑。大多数人都能理解这个概念。现在您可以考虑多少个指针,或者您对多少个间接级别感到满意。将人们置于边缘并不需要太多。当你的代码出现问题时,它们可能会被程序中的错误意外更改,这一事实也会使它们很难调试。
我认为这实际上可能是一个语法问题。指针的 C/C++ 语法似乎不一致,而且比它需要的更复杂。
具有讽刺意味的是,真正帮助我理解指针的是在 c++标准模板库中遇到了迭代器的概念。具有讽刺意味的是,我只能假设迭代器被认为是指针的泛化。
有时,在你学会忽略树木之前,你只是看不到森林。
混淆来自于“指针”概念中混合在一起的多个抽象层。程序员不会对 Java/Python 中的普通引用感到困惑,但指针的不同之处在于它们暴露了底层内存架构的特征。
清晰地分离抽象层是一个很好的原则,而指针不会这样做。
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.
邮政信箱号码。
这是一条信息,可让您访问其他内容。
(如果你对邮政信箱号码进行算术运算,你可能会遇到问题,因为这封信放在了错误的盒子里。如果有人搬到另一个州——没有转发地址——那么你就有一个悬空指针。开另一方面——如果邮局转发邮件,那么你就有一个指向指针的指针。)
通过迭代器来掌握它并不是一个坏方法。但是继续看你会看到 Alexandrescu 开始抱怨它们。
许多前 C++ 开发人员(在放弃该语言之前从不理解迭代器是现代指针)跳到 C# 并仍然相信他们有不错的迭代器。
嗯,问题在于,所有迭代器都与运行时平台(Java/CLR)试图实现的目标完全不同:新的、简单的、每个人都是开发者的用法。这可能很好,但他们在紫皮书中说过一次,甚至在 C 之前和之前都说过:
间接的。
一个非常强大的概念,但如果你一直这样做就不会如此。迭代器很有用,因为它们有助于算法的抽象,另一个例子。编译时是算法的地方,非常简单。你知道代码 + 数据,或者其他语言 C#:
IEnumerable + LINQ + Massive Framework = 300MB 运行时惩罚,通过引用类型实例堆的糟糕、拖拽应用程序..
“Le Pointer 很便宜。”
上面的一些答案断言“指针并不难”,但没有继续直接解决“指针很难!” 来自。几年前,我辅导过一年级的 CS 学生(只辅导了一年,因为我显然不擅长),我很清楚指针的想法并不难。很难理解为什么以及何时需要指针。
我认为您不能将这个问题(为什么以及何时使用指针)与解释更广泛的软件工程问题分开。为什么每个变量都不应该是全局变量,以及为什么应该将类似的代码分解为函数(即,得到这个,使用指针将它们的行为专门化到它们的调用站点)。
我看不出指针有什么令人困惑的地方。它们指向内存中的一个位置,即它存储内存地址。在 C/C++ 中,您可以指定指针指向的类型。例如:
int* my_int_pointer;
表示 my_int_pointer 包含指向包含 int 的位置的地址。
指针的问题在于它们指向内存中的一个位置,因此很容易进入您不应该在的某个位置。作为证据,看看 C/C++ 应用程序中的缓冲区溢出(增加指针)中的众多安全漏洞超过分配的边界)。
只是为了让事情更加混乱,有时您必须使用句柄而不是指针。句柄是指向指针的指针,因此后端可以移动内存中的内容以对堆进行碎片整理。如果指针在程序中间发生变化,结果是不可预测的,因此您首先必须锁定手柄以确保没有任何东西可以移动到任何地方。
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5比我更连贯地谈论它。:-)
每个 C/C++ 初学者都有同样的问题,而且这个问题的出现不是因为“指针很难学习”,而是“谁以及如何解释它”。一些学习者通过视觉来口头收集它,解释它的最好方法是使用“训练”示例(适合口头和视觉示例)。
其中“机车”是不能容纳任何东西的指针, “货车”是“机车”试图拉(或指向)的东西。之后,您可以对“马车”本身进行分类,它是否可以容纳动物、植物或人(或它们的混合体)。