理解指针有什么障碍,克服它们可以做些什么?

Dav*_*raw 443 c c++ pointers

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

有什么好的做法可以让人达到"啊哈,我明白了"的水平,而不会让他们陷入整体概念的困境?基本上,钻取方案.

ang*_*son 744

指针是一个概念,许多人一开始可能会感到困惑,特别是在复制指针值并仍然引用相同的内存块时.

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

我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释.我选择了Delphi,因为我的其他主要编程语言C#没有以同样的方式展示内存泄漏等内容.

如果您只想学习指针的高级概念,那么您应该忽略下面解释中标记为"Memory layout"的部分.它们旨在举例说明操作后内存的样子,但它们本质上更低级.但是,为了准确地解释缓冲区溢出是如何工作的,重要的是我添加了这些图表.

免责声明:出于所有意图和目的,这种解释和示例存储器布局大大简化.如果您需要在低级别处理内存,则需要了解更多开销和更多详细信息.但是,对于解释内存和指针的意图,它足够准确.


让我们假设下面使用的THouse类看起来像这样:

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

初始化house对象时,将为构造函数指定的名称复制到私有字段FName中.有一个原因,它被定义为固定大小的数组.

在内存中,会有一些与房屋分配相关的开销,我将在下面说明如下:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

"tttt"区域是开销,对于各种类型的运行时和语言,通常会有更多这样的内容,例如8或12字节.必须确保在此区域中存储的任何值都不会被内存分配器或核心系统例程以外的任何内容更改,否则您可能会崩溃程序.


分配内存

让一个企业家建造你的房子,并给你房子的地址.与现实世界相比,内存分配无法分配到哪里,但会找到一个有足够空间的合适位置,并将地址报告给分配的内存.

换句话说,企业家会选择现场.

THouse.Create('My house');
Run Code Online (Sandbox Code Playgroud)

内存布局:

---[ttttNNNNNNNNNN]---
    1234My house

使用地址保留变量

在一张纸上写下你的新房子的地址.本文将作为您对您家的参考.没有这张纸,你就会迷路,找不到房子,除非你已经在里面.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
Run Code Online (Sandbox Code Playgroud)

内存布局:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

复制指针值

只需在新纸上写下地址即可.你现在有两张纸可以带你到同一栋房子,而不是两栋独立的房子.任何试图从一篇论文中删除地址并重新安排在那所房子里的家具的尝试都会使得其他房屋看起来像是以同样的方式被修改,除非你能明确地发现它实际上只是一个房子.

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

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
Run Code Online (Sandbox Code Playgroud)
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

释放记忆

拆毁房子.如果您愿意,您可以稍后重新使用纸张换新地址,或者清除它以忘记不再存在的房屋地址.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;
Run Code Online (Sandbox Code Playgroud)

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

内存布局:

    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
Run Code Online (Sandbox Code Playgroud)

h通话后使用.Free 可能会起作用,但那只是纯粹的运气.最有可能的是,它会在客户所在地,在关键操作过程中失败.

    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;
Run Code Online (Sandbox Code Playgroud)

在这里,我们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;
Run Code Online (Sandbox Code Playgroud)

执行此方法后,我们的变量中没有地方存在房子的地址,但房子仍在那里.

内存布局:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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

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


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

拆毁房子,擦掉其中一张纸,但你还有另一张纸上有旧地址,当你去地址时,你找不到房子,但你可能会发现类似废墟的东西一个.

也许你甚至会找到一所房子,但它不是你最初被给予地址的房子,因此任何试图使用它的方式就像它属于你一样可能会失败.

有时你甚至可能会发现邻近的地址上有一个相当大的房子,占据了三个地址(主街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?
Run Code Online (Sandbox Code Playgroud)

在这里,房子被拆除,通过参考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
Run Code Online (Sandbox Code Playgroud)

首次分配后的内存布局:

                        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;
Run Code Online (Sandbox Code Playgroud)

在这里,我们创建了从我们家到我们的小屋的链接.我们可以跟随链条直到房子没有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;
Run Code Online (Sandbox Code Playgroud)

内存布局(将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)

通常将"无处点"的地址存储为零地址.


从基本的角度来说,什么是指针?

指针只是一个包含内存地址的变量.你通常可以要求编程语言给你它的编号,但是大多数编程语言和运行时都试图隐藏下面有一个数字的事实,因为数字本身对你没有任何意义.最好将指针视为黑盒子,即.只要它有效,你就不知道或关心它是如何实际实现的.

  • 缓冲区溢出是搞笑的."邻居回到家里,他的头骨裂开,在你的垃圾上滑落,并让你被遗忘." (57认同)
  • 自从你写完答案以来,我已多次重访这篇文章.您对代码的澄清非常好,我感谢您重新审视它以添加/改进更多想法.Bravo Lasse! (11认同)
  • 当然,这是对这个概念的一个很好的解释.这个概念并不是我对指针感到困惑的事情,所以整篇文章有点浪费. (10认同)
  • 但只是问过,你有什么*发现令人困惑的指针? (10认同)
  • 单页文本(无论多长时间)都无法总结内存管理,参考文献,指针等的每一个细微差别.由于有465人投票,我认为它足够好了关于信息的起始页面.还有更多要学习的东西吗?当然,什么时候不是? (3认同)
  • 早在80年代早期,对我而言,AHA时刻就是认识到指针和动态内存分配不是同义词.Pascal的指针语法专门用于动态内存分配/释放.但是,C的指针语法独立于动态内存(例如,您可以获取自动变量的地址并将其作为指针作为参数传递给函数,并使用指针而无需执行`malloc`或`free`).一旦我在C中看到它,就会发生AHA,并且Pascal指针语义也变得更加清晰. (2认同)

Try*_*yke 151

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

教授在董事会上写道: int john;

约翰站起来

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

约翰站起来,指着约翰

教授: int *bill = sally;

比尔站起来,指着约翰

教授: int sam;

萨姆站起来

教授: bill = &sam;

比尔现在指向萨姆.

我想你应该已经明白了.我想我们花了大约一个小时做这个,直到我们讨论了指针赋值的基础知识.

  • 更像是Sam占据约翰的座位,约翰漂浮在房间里,直到他碰到一些关键的东西,并导致一个段错误. (59认同)
  • 但令人困惑的原因是,这并不像约翰从座位上站起来然后山姆坐下来,正如我们想象的那样.这更像是山姆过来并将他的手伸进约翰并将山姆编程克隆到约翰的身体中,就像重新装载的矩阵中的hugo编织一样. (24认同)
  • 我不认为我弄错了.我的目的是将指向变量的值从John更改为Sam.与人交往有点困难,因为看起来你正在改变两个指针的价值. (5认同)
  • 我个人认为这个例子不必要地复杂化.我的教授告诉我指着一盏灯说"你的手是指向光明物体的指针". (2认同)

Wil*_*lka 124

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

  • 我真的很喜欢这个.不难发现两次写出超链接并不会使两个网站出现(就像`int*a = b`不会生成两个'*b`副本). (15认同)
  • 你是在思考它.超链接指向服务器上的文件,这就是类比的范围. (5认同)
  • 这实际上非常直观,每个人都应该能够与之相关.虽然有很多场景可以解释这种类比.非常适合快速介绍.+1 (4认同)

JSN*_*JSN 48

指针似乎让很多人感到困惑的原因是,他们在计算机体系结构方面很少或根本没有背景.由于许多人似乎并不了解计算机(机器)的实际实现方式 - 在C/C++中工作似乎很陌生.

演习是要求他们实现一个简单的基于字节码的虚拟机(使用他们选择的任何语言,python非常适合这一点),其指令集主要集中在指针操作(加载,存储,直接/间接寻址).然后让他们为该指令集编写简单的程序.

任何需要稍微多于简单添加的东西都会涉及指针,他们肯定会得到它.

  • @Luke我不认为那些容易理解那些根本无法掌握指针的人(或者,更确切地说,一般来说是间接的).你基本上假设那些不理解C语言指针的人能够开始学习汇编,理解计算机的底层架构,并在理解指针的情况下返回C语言.这对许多人来说可能都是如此,但根据一些研究,似乎有些人固有地无法掌握间接,即使在原则上(我仍然觉得很难相信,但也许我对我的"学生们很幸运") "). (4认同)
  • 有趣.不知道怎么开始这个.有资源分享吗? (2认同)

Jos*_*osh 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;
Run Code Online (Sandbox Code Playgroud)

与上面相同的例子,除了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);
Run Code Online (Sandbox Code Playgroud)

输出:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u
Run Code Online (Sandbox Code Playgroud)

也许这通过例子解释了一些基础知识?

  • 我知道这是一个旧帖子,但如果提供的代码输出添加到帖子中会很棒. (4认同)

Rya*_*ndy 24

我起初很难理解指针的原因是,许多解释包括很多关于通过引用传递的废话.所有这一切都是混淆了这个问题.当您使用指针参数时,您仍然按值传递; 但是值恰好是一个地址而不是一个int.

其他人已经链接到本教程,但我可以突出我开始理解指针的那一刻:

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

int puts(const char *s);
Run Code Online (Sandbox Code Playgroud)

暂时,忽略const.传递给的参数puts()是一个指针,指针的值(因为C中的所有参数都是按值传递的),而指针的值是它指向的地址,或者,简单地说, 一个地址.因此,当我们puts(strA);按照我们所看到的那样写时,我们传递的是strA [0]的地址.

在我读完这些文字的那一刻,云层分开,一束阳光笼罩着我,指针理解.

即使你是VB .NET或C#开发人员(就像我一样)并且从不使用不安全的代码,仍然值得理解指针是如何工作的,或者你不会理解对象引用是如何工作的.然后你会有一个常见但错误的概念,即将对象引用传递给方法会复制该对象.


Ted*_*val 19

我发现Ted Jensen的"C指针和数组教程"是学习指针的绝佳资源.它分为10节课,首先解释指针是什么(以及它们的用途)和完成函数指针.http://home.netcom.com/~tjensen/ptr/cpoint.htm

从那里开始,Beej的网络编程指南教授Unix套接字API,您可以从中开始做有趣的事情.http://beej.us/guide/bgnet/


Der*_*ark 12

指针的复杂性超出了我们可以轻易教授的范围.让学生互相指出并使用带有房屋地址的纸张都是很好的学习工具.他们在介绍基本概念方面做得很好.实际上,学习基本概念对于成功使用指针至关重要.但是,在生产代码中,除了这些简单的演示可以封装之外,进入更复杂的场景是很常见的.

我参与过系统,我们的结构指向指向其他结构的其他结构.其中一些结构也包含嵌入式结构(而不​​是指向其他结构的指针).这是指针变得非常混乱的地方.如果你有多个级别的间接,那么你开始使用这样的代码:

widget->wazzle.fizzle = fazzle.foozle->wazzle;
Run Code Online (Sandbox Code Playgroud)

它可以很快变得混乱(想象更多的线,可能更多的水平).抛出指针数组,节点到节点指针(树,链表),它会变得更糟.我看到一些非常优秀的开发人员在开始研究这样的系统时会迷路,甚至是那些理解基础知识的开发人员.

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

我认为大学可以帮助学生学习指针的最好方法是使用良好的演示,结合需要使用指针的项目.一个困难的项目将比一千次演示更多地用于指针理解.演示可以让你有一个浅薄的理解,但要深入掌握指针,你必须真正使用它们.


Mik*_*ike 10

我想我会在这个列表中添加一个类比,在我作为计算机科学导师解释指针(当天)时,我发现它非常有用; 首先,让我们:


设置阶段:

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

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |
Run Code Online (Sandbox Code Playgroud)

在某种程度上,这就像内存位置,它们是连续的和连续的......有点像数组.现在它们里面没有汽车所以它就像一个空数组(parking_lot[3] = {0}).


添加数据

一个停车场永远不会长时间空着......如果这样做会毫无意义,没有人会建造任何停车场.所以,让我们说,当这一天的行动充满了3辆汽车,一辆蓝色汽车,一辆红色汽车和一辆绿色汽车:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |
Run Code Online (Sandbox Code Playgroud)

这些车都是同一类型(轿车),所以想到这一点的一种方式是,我们的车有一些类型的数据(比如一个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 |
Run Code Online (Sandbox Code Playgroud)

你的指针仍然指向红色汽车的位置,但不再指向.让我们说一辆新车在那里拉动......一辆橙色汽车.如果我再次问你,"红色汽车在哪里",你仍指着那里,但现在你错了.那不是一辆红色的车,那是橙色的.


指针算术

好的,所以你仍然指着第二个停车位(现在被Orange车占用)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |
Run Code Online (Sandbox Code Playgroud)

我现在有一个新问题......我想知道下一个停车位的汽车颜色.你可以看到你指向第2点,所以你只需加1就可以指向下一个位置.(finger+1),现在因为我想知道那里的数据是什么,你必须检查那个点(不仅仅是手指)所以你可以用指针(*(finger+1))来看看那里有一辆绿色汽车(那个位置的数据) )


Mat*_*ell 9

我不认为指针作为一个概念特别棘手 - 大多数学生的心理模型都映射到这样的东西,一些快速的盒子草图可以帮助.

困难,至少是我过去经历过和其他人看到过的难点,就是C/C++中指针的管理可能会不经意地复杂化.


Dav*_*vid 9

具有一组良好图表的教程示例有助于理解指针.

乔尔·斯波尔斯基(Joel Spolsky)在他的" 游击队采访指南"中采用了一些关于理解指针的好点:

出于某种原因,大多数人似乎是在没有理解指针的大脑部分出生的.这是一种天赋,而不是技能 - 它需要一种复杂形式的双向间接思维,有些人根本无法做到.


Bre*_*ton 8

指针的问题不是概念.这是涉及的执行和语言.当教师认为这是困难的指针的概念,而不是行话,或者C和C++的复杂混乱时,会产生额外的混淆.因此,大量的努力都被用于解释这个概念(就像在这个问题的公认答案中一样)而且它几乎只是浪费在像我这样的人身上,因为我已经理解了所有这些.它只是在解释问题的错误部分.

为了让你知道我来自哪里,我是一个非常了解指针的人,我可以在汇编语言中胜任它们.因为在汇编语言中它们不被称为指针.它们被称为地址.当涉及到在C中编程和使用指针时,我犯了很多错误并且变得非常困惑.我还没有把它整理出来.让我给你举个例子.

当api说:

int doIt(char *buffer )
//*buffer is a pointer to the buffer
Run Code Online (Sandbox Code Playgroud)

它想要什么?

它可能想要:

表示缓冲区地址的数字

(为了给它,我说doIt(mybuffer),还是doIt(*myBuffer)?)

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

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

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

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

等等,所涉及的语言并没有明确表达,因为它涉及的词语"指针"和"引用"对我来说没有那么多含义和清晰度,因为"x将地址保持为y"和"这个函数需要一个地址y".答案另外还取决于"mybuffer"的开头是什么,以及它打算用它做什么.该语言不支持实践中遇到的嵌套级别.就像当我必须将"指针"交给一个创建一个新缓冲区的函数时,它会修改指针以指向缓冲区的新位置.它确实需要指针或指针指针,因此它知道在哪里修改指针的内容.大多数时候我只需要猜测"指针"是什么意思,而且大部分时间我都错了,不管我猜测有多少经验.

"指针"太过分了.指针是一个值的地址吗?或者它是一个保存值的地址的变量.当一个函数想要一个指针时,它是否需要指针变量所持有的地址,或者它是否希望地址指向变量?我糊涂了.


jal*_*alf 8

我认为理解指针的主要障碍是坏老师.

几乎每个人都被教导谎言:它们只不过是内存地址,或者它们允许你指向任意位置.

当然,他们很难理解,危险和半魔法.

这些都不是真的.指针实际上是相当简单的概念,只要你坚持使用C++语言对它们所说的内容并且不要给它们灌输"通常"在实践中发挥作用的属性,但是语言并不能保证这些属性. ,所以不是指针实际概念的一部分.

几个月前我在这篇博客文章中试图写出一个解释- 希望它能帮到某个人.

(注意,在任何人对我嗤之以鼻之前,是的,C++标准确实说指针代表内存地址.但它并没有说"指针是内存地址,只是内存地址,可以使用或者可以与内存交换使用地址".区别很重要)


Bal*_*ark 5

我认为使得指针难以学习的是,直到指针你对"在这个内存位置是一组表示int,double,a character,what"的想法感到满意.

当你第一次看到指针时,你并没有真正得到那个内存位置的内容."你是什么意思,它有一个地址?"

我不同意"你要么得到它们要么你没得到"这一概念.

当您开始为它们寻找实际用途时(例如不将大型结构传递到函数中),它们变得更容易理解.


小智 5

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

   int *mypointer;
Run Code Online (Sandbox Code Playgroud)

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

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

  int* mypointer;
Run Code Online (Sandbox Code Playgroud)

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

  • 实际上,你的两个片段都是有效的C.这是C风格很多年的问题,第一个更常见.例如,第二种在C++中更常见. (2认同)
  • `int*p;`有一个简单的含义:`*p`是一个整数.`int*p,**pp`表示:`*p`和`**pp`是整数. (2认同)

tot*_*oto 5

当我只知道C++时,我可以使用指针.我有点知道在某些情况下该做什么以及从试验/错误中无法做什么.但让我完全理解的是汇编语言.如果你使用你编写的汇编语言程序进行一些严格的指令级调试,你应该能够理解很多东西.