首页 文章

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

提问于
浏览
443

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

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

28 回答

  • 4

    通过迭代器 grab 它并不是一个糟糕的方法..但继续看你会看到Alexandrescu开始抱怨他们 .

    许多ex-C开发人员(在转储语言之前从未理解迭代器是一个现代指针)跳转到C#并且仍然认为他们有合适的迭代器 .

    嗯,问题是所有迭代器都在运行时平台(Java / CLR)试图实现的完全赔率:新的,简单的,每个人都是开发者的用法 . 这可能是好的,但他们曾在紫皮书中说过,他们甚至在C之前和之前都说过:

    间接 .

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

    IEnumerable LINQ Massive Framework = 300MB运行时惩罚间接糟糕,通过大量的引用类型实例拖动应用程序 .

    “Le Pointer很便宜 . ”

  • 123

    我认为它可能实际上是一个语法问题 . 指针的C / C语法似乎不一致,而且比它需要的更复杂 .

    具有讽刺意味的是,实际帮助我理解指针的事情是在c Standard Template Library中遇到迭代器的概念 . 这很具有讽刺意味,因为我只能假设迭代器被认为是指针的泛化 .

    有时在你学会忽视树木之前,你根本看不到森林 .

  • 0

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

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

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

    这些都不是真的 . 指针实际上是相当简单的概念,只要你坚持使用C语言对它们所说的内容并且不受语言保证,因此它们不是指针的实际概念的一部分 .

    几个月前我试图在this blog post写下这个解释 - 希望它能帮助别人 .

    (注意,在任何人对我嗤之以鼻之前,是的,C标准确实说指针代表内存地址 . 但它并没有说"pointers are memory addresses, and nothing but memory addresses and may be used or thought of interchangeably with memory addresses" . 区别很重要)

  • 10

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

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

  • 1

    指针似乎让很多人感到困惑的原因是,他们在计算机体系结构方面几乎没有任何背景 . 由于许多人似乎并不知道计算机(机器)是如何实际实现的 - 在C / C中工作似乎很陌生 .

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

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

  • 2

    指针的问题不是概念 . 这是涉及的执行和语言 . 当教师认为这是困难的指针的概念,而不是行话,或者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”的开头是什么,以及它打算用它做什么 . 该语言不支持实践中遇到的嵌套级别 . 就像当我必须将“指针”交给一个创建一个新缓冲区的函数时,它会修改指针以指向缓冲区的新位置 . 它真的需要指针或指针的指针,因此它知道去哪里修改指针的内容 . 大多数时候我只需要猜测“指针”是什么意思,而且大部分时间我都错了,不管我猜测有多少经验 .

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

  • 23

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

    当您第一次看到指针时,您不会在该内存位置处看到.430142_ . “你是什么意思,它有一个地址?”

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

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

  • 8

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

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

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

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

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

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

  • 0

    我找到了Ted Jensen 's 430094 an excellent resource for learning about pointers. It is divided into 10 lessons, beginning with an explanation of what pointers are (and what they' for)并完成了函数指针 . http://home.netcom.com/~tjensen/ptr/cpoint.htm

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

  • 5

    混淆来自于“指针”概念中混合在一起的多个抽象层 . 程序员不会对Java / Python中的普通引用感到困惑,但指针的不同之处在于它们暴露了底层内存架构的特性 .

    清晰地分离抽象层是一个很好的原则,而指针则不这样做 .

  • 1

    上面的一些答案断言"pointers aren't really hard",但没有继续直接解决"pointer are hard!"来自哪里 . 几年前,我辅导了一年级的CS学生(仅仅一年,因为我很清楚它),我很清楚,指针并不难 . 什么是难以理解 why and when you would want a pointer .

    我认为你不能离婚问题 - 为何以及何时使用指针 - 解释更广泛的软件工程问题 . 为什么每个变量都不应该是一个全局变量,为什么要将类似的代码分解为函数(为此,使用指针将其行为专门化为其调用站点) .

  • 28

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

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

    A Tutorial on Pointers and Arrays in C: Chapter 3 - Pointers and Strings

    int puts(const char *s);
    

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

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

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

  • 10

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

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

    约翰站起来

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

    约翰站起来,指着约翰

    教授: int *bill = sally;

    比尔站起来,指着约翰

    教授: int sam;

    萨姆站起来

    教授: bill = &sam;

    比尔现在指向萨姆 .

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

  • 1

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

    一旦你到达C和C,对某些人来说似乎变得更难了 . 我正确地掌握了实践,或者因为指针操作在这些语言中本质上更难 . 我不记得我自己的过渡了,但是我在Pascal中指点然后转移到C并完全迷失了 .

  • 49

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

  • 4

    我喜欢房子地址类比,但我一直都想到地址是邮箱本身 . 这样,您可以可视化取消引用指针(打开邮箱)的概念 .

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

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

    请注意,步骤3是取消引用的地方,以及当地址无效时您将崩溃或出错的地方 . 假设你可以走到一个无效地址的邮箱,想象那里有一个黑洞或其他什么东西可以把世界彻底改变掉:)

  • 734

    之所以_430143是一个难以理解的概念,而是因为 the syntax is inconsistent .

    int *mypointer;
    

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

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

    int* mypointer;
    

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

  • 10

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

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

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

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

    免责声明:出于所有意图和目的,这种解释和示例存储器布局大大简化 . 如果你需要在低级别的基础上处理内存,那么你需要知道更多的开销和更多细节 . 但是,对于解释内存和指针的意图,它足够准确 .


    让我们假设下面使用的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字节 . 必须确保存储在此区域中的任何值都不会被内存分配器或核心系统例程以外的任何内容更改,否则您可能会崩溃程序 .


    Allocate memory

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

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

    THouse.Create('My house');
    

    内存布局:

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

    Keep a variable with the address

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

    var
        h: THouse;
    begin
        h := THouse.Create('My house');
        ...
    

    内存布局:

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

    Copy pointer value

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

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

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

    Freeing the memory

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

    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)
    

    Dangling pointers

    你告诉你的企业家破坏房子,但你忘了从你的纸上删除地址 . 当你稍后看到这张纸时,你已经忘记了房子已经不存在了,然后去参观它,结果失败了(另见下面关于无效参考的部分) .

    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仍然指向内存中数据的残余,但由于它可能不完整,因此使用它可能会失败 .


    Memory leak

    你丢失了那张纸,找不到房子 . 房子仍然站在某个地方,当你以后想要建造一所新房子时,你不能重复使用那个地方 .

    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             <--+
    

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


    Freeing the memory but keeping a (now invalid) reference

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

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

    有时你甚至可能会发现邻近的地址上有一个相当大的房子,占据了三个地址(主街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 仍然有旧的,过时的地址 . 进入不再站立的房屋可能会或可能不会 .

    这是上面悬空指针的变体 . 查看其内存布局 .


    Buffer overrun

    你搬进房子里的东西比你可能适合的东西多,溢出到邻居的房子或院子里 . 当邻近房子的主人回家后,他会找到他认为属于自己的各种东西 .

    这就是我选择固定大小数组的原因 . 要设置阶段,假设我们分配的第二个房子由于某种原因将被放置在内存中的第一个房屋之前 . 换句话说,第二个房子的地址低于第一个房子的地址 . 而且,它们彼此相邻分配 .

    因此,这段代码:

    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名称的某些部分可能不是问题,但是当你试图使用破坏的对象时,覆盖对象的开销很可能会崩溃,覆盖存储到对象中其他对象的链接 .


    Linked lists

    当你按照一张纸上的地址,你就到了一所房子,在那个房子里还有另一张纸,上面有一个新的地址,链中的下一个房子,依此类推 .

    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)
    

    In basic terms, what is a memory address?

    内存地址基本上只是一个数字 . 如果您将内存视为一个大字节数组,则第一个字节的地址为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)
    

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


    In basic terms, what is a pointer?

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

  • 148

    我没有看到关于指针的混乱 . 它们指向内存中的某个位置,即存储内存地址 . 在C / C中,您可以指定指针指向的类型 . 例如:

    int* my_int_pointer;
    

    说my_int_pointer包含一个包含int的位置的地址 .

    指针的问题在于它们指向内存中的某个位置,因此很容易跟踪到您不应该进入的某个位置 . 作为证据,从缓冲区溢出中查看C / C应用程序中的众多安全漏洞(递增指针)超过分配的边界) .

  • 12

    An example of a tutorial with a good set of diagrams helps greatly with the understanding of pointers .

    Joel Spolsky在他的Guerrilla Guide to Interviewing文章中提出了一些关于理解指针的好点:

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

  • 2

    为什么指针如此C / C语言中许多新的,甚至是老的大学生的混乱的主要因素是什么?

    Value 占位符的概念 - 变量 - 映射到我们在学校教授的东西 - 代数 . 没有现成的并行,你可以在不了解内存在计算机中如何物理布局的情况下绘制,并且在处理低级事物之前没有人会考虑这种事情 - 在C / C /字节通信级别 .

    是否有任何工具或思维过程可以帮助您理解指针在变量,函数和更高级别的工作方式?

    地址框 . 我记得当我学习将BASIC编程到微型计算机中的时候,有些这些漂亮的书中都有游戏,有时候你不得不把 Value 观点到特定的地址 . 他们有一堆盒子的图片,逐渐标记为0,1,2 ......并且解释说这些盒子中只有一个小东西(一个字节)可以放入,而且它们中有很多 - 一些计算机有多达65535!他们彼此相邻,他们都有一个地址 .

    有什么好的做法可以让人达到“Ah-hah,我得到它”的水平,而不会让他们陷入整体概念的困境?基本上,钻取方案 .

    对于演习?制作一个结构:

    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
    

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

  • 19

    我认为这些指针本身并不令人困惑 . 大多数人都能理解这个概念 . 现在您可以考虑多少指针或您熟悉多少层次的间接 . 把人放在边缘并不需要太多 . 它们可能会被程序中的错误意外更改,这也会使代码中出现问题时很难调试 .

  • 7

    邮政信箱号码 .

    这是一条允许您访问其他内容的信息 .

    (如果你对邮局票号进行算术运算,你可能会遇到问题,因为这封信出错了 . 如果有人进入另一个状态 - 没有转发地址 - 那么你就有一个悬空指针 . 另一方面 - 如果邮局转发邮件,那么你有一个指针指针 . )

  • 2

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

  • 1

    每个C / C初学者都有同样的问题,并且问题的发生不是因为"pointers are hard to learn"而是"who and how it is explained" . 有些学习者在视觉上口头收集它,解释它的最佳方式是使用 "train" example (适用于口头和视觉示例) .

    "locomotive" 是一个指针, can not 持有任何东西, "wagon" 是"locomotive"尝试拉(或指向) . 之后,您可以对"wagon"本身进行分类,它可以保存动物,植物或人(或它们的混合物) .

  • 5

    我喜欢解释它的方式是数组和索引 - 人们可能不熟悉指针,但他们通常知道索引是什么 .

    所以我想说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]] .

    我会在纸张列表上绘制一个数组,并使用它来显示指向同一内存的许多指针,指针算法,指针指针等等 .

  • 5

    只是为了混淆一些东西,有时你必须使用句柄而不是指针 . 句柄是指针的指针,因此后端可以在内存中移动内容以对堆进行碎片整理 . 如果指针在中间例程中发生变化,则结果是不可预测的,因此您首先必须锁定句柄以确保没有任何内容在任何地方 .

    http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5谈论它比我更连贯一点 . :-)

  • 2

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


    Set the stage

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

    -------------------
    |     |     |     |
    |  1  |  2  |  3  |
    |     |     |     |
    

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


    Add the data

    一个停车场永远不会空着......如果它这样做将毫无意义,没有人会建造任何 . 所以,让我们说,随着时间的推移,这个地段充满了3辆汽车,一辆蓝色轿车,一辆红色汽车和一辆绿色汽车 . 汽车:

    1     2     3
    -------------------
    | o=o | o=o | o=o |
    | |B| | |R| | |G| |
    | o-o | o-o | o-o |
    

    这些车型都是同一类型(汽车),所以考虑到这一点的一种方式是我们的汽车是某种数据(比如 int ),但它们有不同的值( blueredgreen ;这可能是一种颜色 enum


    Enter the pointer

    现在,如果我带你进入这个停车场,并要求你找到一辆蓝色的汽车,你伸出一根手指并用它指向现场1的蓝色汽车 . 这就像拿一个指针并将它指定给一个记忆地址( int *finger = parking_lot

    你的手指(指针)不是我的问题的答案 . 看着 at 你的手指什么也没告诉我,但是如果我看到你手指指向的地方(取消引用指针),我就能找到我正在寻找的汽车(数据) .


    Reassigning the pointer

    现在我可以要求你找一辆红色轿车而你可以将你的手指重定向到一辆新车 . 现在你的指针(和以前一样)向我展示了相同类型(汽车)的新数据(可以找到红色汽车的停车位) .

    指针仍然是你的手指,只是它显示我改变的数据 . ("parking spot"地址)


    Double pointers (or a pointer to a pointer)

    这也适用于多个指针 . 我可以问指针在哪里,指向红色汽车,你可以用另一只手,用手指指向第一根手指 . (这就像 int **finger_two = &finger

    现在,如果我想知道蓝色汽车的位置,我可以按照第一根手指的方向指向第二根手指,然后是汽车(数据) .


    The dangling pointer

    现在让我们说你感觉非常像雕像,你想要无限期地用手指着红色的汽车 . 如果那辆红色车开走了怎么办?

    1     2     3
    -------------------
    | o=o |     | o=o |
    | |B| |     | |G| |
    | o-o |     | o-o |
    

    你的指针仍然指向红色车的位置,但不再指向 . 让's say a new car pulls in there... a Orange car. Now if I ask you again, 430121 , you'仍然指向那里,但现在你're wrong. That'不是一辆红色的车,那是橙色的 .


    Pointer arithmetic

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

    1     2     3
    -------------------
    | o=o | o=o | o=o |
    | |B| | |O| | |G| |
    | o-o | o-o | o-o |
    

    那么我现在有一个新问题......我想知道下一个停车位的汽车颜色 . 你可以看到你're pointing at spot 2, so you just add 1 and you'指着下一个地方 . ( finger+1 ),现在因为我想知道那里有什么数据,你必须检查那个点(不仅仅是手指)所以你可以顺从指针( *(finger+1) )看看那里有一辆绿色汽车(数据在那个位置)

相关问题