指针/递归有什么困难?[关闭]


20

Java学校危险中, Joel讨论了他在Penn的经历以及“分段错误”的难度。他说

[在您之前,段错误是很难的]“深呼吸,真正尝试迫使您的思想同时在两个不同的抽象级别上工作。”

给出了导致段错误的常见原因的清单,我不明白我们必须如何在2个抽象级别上工作。

由于某种原因,Joel认为这些概念是程序员抽象能力的核心。我不想承担太多。那么,指针/递归到底有什么困难呢?例子会很好。


31
不要再担心乔尔会对您有何想法。如果您发现递归很容易,那很好。并非所有人都这样做。
FrustratedWithFormsDesigner

6
从定义上来说,递归很容易(调用self的函数),但是知道何时使用它以及如何使它起作用是困难的部分。
JeffO 2011年

9
在Fog Creek申请一份工作,让我们知道进展如何。我们都对您的自我晋升非常感兴趣。
乔尔·埃瑟顿

4
@ P.Brian.Mackey:我们没有误会。这个问题并没有真正问过什么。这是公然的自我促进。如果您想知道Joel在问有关指针/递归的问题,请问他:team@stackoverflow.com
Joel Etherton

19
这个问题重复吗?
ozz 2011年

Answers:


38

我首先注意到指针和递归在大学里很困难。我参加了一些典型的第一年课程(一个是C和Assembler,另一个是Scheme)。这两门课程都是从数百名学生开始的,其中许多人具有多年的高中级编程经验(当时通常是BASIC和Pascal)。但是,一旦在C课程中引入了指针,并且在Scheme课程中引入了递归,则大量学生-甚至可能是大多数学生都被完全弄糊涂了。这些孩子以前曾经写过很多代码,一点也没有问题,但是当他们碰到指针和递归时,他们的认知能力也会碰壁。

我的假设是,指针和递归是相同的,因为它们要求您同时保留两个抽象级别。关于多层次的抽象需要某种精神才能,这是某些人永远不可能拥有的。

  • 对于指针,“两个抽象级别”是“数据,数据地址,数据地址地址等”,或者我们通常称为“值与引用”。对于未经培训的学生,很难看到xx本身的地址之间区别。
  • 通过递归,“两个抽象层次”正在理解函数如何调用自身。递归算法有时被人们称为“一厢情愿的编程”,从“基本案例+归纳案例”的角度来思考算法,而不是从更自然的“解决问题的步骤清单”的角度来思考算法,这是非常非常不自然的。” 对于正在研究递归算法的未经训练的学生,该算法似乎是个难题

我也非常愿意接受有可能向任何人教授指针和/或递归...我没有任何证据可以证明。我确实知道,凭经验,能够真正理解这两个概念是一般编程能力的非常非常好的预测指标,并且在本科CS训练的正常过程中,这两个概念是最大的障碍。


4
“从“基础案例+归纳案例”的角度来考虑算法非常非常不自然”-我认为这绝不是不自然的,只是没有对孩子进行相应的训练。
Ingo

14
如果是自然的,则无需培训。:P
Joel Spolsky

1
好点:),但是我们不需要数学,逻辑,物理等方面的培训。从广义上讲,所有这些都是最自然的。有趣的是,很少有程序员对语言的语法有任何疑问,但是它充满了递归。
Ingo

1
在我的大学里,第一门课程几乎是在引入变异等之前就开始的,几乎都是从函数编程和递归开始的。我发现,一些学生没有经验,了解递归比有更好的一些经验。就是说,班上最顶尖的人有丰富经验的人组成的。
Tikhon Jelvis 2011年

2
我认为无法理解指针和递归与a)总体智商水平和b)不良的数学教育有关。
quant_dev

23

递归不仅是“调用自身的函数”。您不会真正理解为什么递归比较困难,直到您发现自己画出堆栈帧来找出递归下降解析器出了什么问题。通常,您将拥有相互递归的函数(函数A调用函数B,后者调用函数C,后者可能调用函数A)。当您在一个相互递归的函数系列中有N个堆栈帧很深时,很难弄清楚出了什么问题。

同样,对于指针,指针的概念非常简单:存储内存地址的变量。但是同样,当void**指向不同节点的指针的复杂数据结构出了问题时,您会发现为什么当您难以弄清为什么其中一个指针指向垃圾地址时,它变得棘手。


1
当我真正感到自己对递归有所了解时,便实现了一个递归的体面解析器。指针很容易理解,就像您说的那样。直到您深入了解实现指针的实现,才知道它们为什么很复杂。
克里斯(Chris)

许多函数之间的相互递归与基本上相同goto
starblue 2011年

2
@starblue,不是真的-因为每个堆栈框架都会创建局部变量的新实例。
Charles Salvia

没错,只有递归与相同goto
starblue 2011年

3
@wnoise int a() { return b(); }可以递归,但这取决于的定义b。所以它并不像看起来那么简单...
替代

14

Java支持指针(它们称为引用),并且支持递归。因此从表面上看,他的论点显得毫无意义。

他真正在说的是调试能力。确保Java指针(err,引用)指向有效对象。AC指针不是。假设您不使用valgrind之类的工具,那么C编程中的诀窍是找出指针的确切位置(很少在stacktrace中找到该点)。


5
指针本身就是一个细节。在Java中使用引用并不比在C中使用局部变量复杂。即使以Lisp实现的方式混合它们(原子可能是有限大小的整数,字符或指针)也并不困难。当语言允许使用不同的语法在本地或引用相同类型的数据时,将变得更加困难,而在语言允许指针算术时,这将变得非常困难。
David Thornley,

@David-嗯,这与我的回复有什么关系?
Anon

1
您对Java支持指针的评论。
David Thornley

“在哪里弄糟了一个指针(很少在堆栈跟踪中找到该点)。” 如果您有幸获得堆栈跟踪。
欧米茄半人马座

5
我同意戴维·桑利的观点;Java不支持指针,除非我可以使一个指针指向一个指向int指针的指针。也许我想我可以通过制作4-5个类,每个类都引用其他东西,但是这真的是指针还是丑陋的解决方法?
替代

12

指针和递归的问题并不在于它们不一定很难理解,而是对它们的教学不好,尤其是对于诸如C或C ++之类的语言(主要是因为对语言本身的理解很差)。每当我听到(或读到)某人说“数组只是一个指针”时,我就会在里面死掉一点。

同样,每次有人使用斐波那契函数来说明我想尖叫的递归时。这是一个不好的例子,因为迭代版本不难编写,并且其性能至少与递归版本相同或更好,并且它没有使您真正了解为何递归解决方案有用或令人期望。快速排序,树的遍历,等等,都远远的原因和递归如何更好的例子。

必须处理指针是使用暴露于指针的编程语言的人工产物。几代Fortran程序员在不需要专用指针类型(或动态内存分配)的情况下构建列表,树,堆栈和队列,而且我从未听说过有人指责Fortran是玩具语言。


我会同意的,在了解实际的指针之前,我已经使用了数十年的Fortran,因此我一直在使用自己的方式来做同样的事情,然后才有机会让lanquage /编译器为我做这件事。我也认为关于指针/地址的C语法非常混乱,即使存储在地址中的值的概念非常简单。
欧米茄半人马座

如果您有在Fortran IV中实现的Quicksort的链接,我希望看到它。并不是说不可能做到这一点-实际上,大约30年前我在BASIC中实现了它-但我很想看到它。
Anon

我从未在Fortran IV中工作过,但是我确实在Fortran 77的VAX / VMS实现中实现了一些递归算法(有一个钩子可以让您将goto的目标另存为一种特殊的变量,因此您可以编写GOTO target) 。我认为我们必须构建自己的运行时堆栈。这已经很久了,我再也记不清细节了。
约翰·波德

8

指针有几个困难:

  1. 别名可以使用不同的名称/变量来更改对象的值。
  2. 非局部性在与声明上下文不同的上下文中更改对象值的可能性(通过引用传递的参数也会发生这种情况)。
  3. 生命周期不匹配指针的生命周期可能与其指向的对象的生命周期不同,并可能导致无效引用(SEGFAULTS)或垃圾。
  4. 指针算术。一些编程语言允许将指针作为整数进行操作,这意味着指针可以指向任何地方(包括存在错误时最意外的位置)。为了正确使用指针算法,程序员必须知道所指向对象的内存大小,这是需要考虑的更多内容。
  5. 类型转换将指针从一种类型转换为另一种类型的能力可以覆盖与预期对象不同的对象的内存。

这就是为什么程序员在使用指针时必须更深入地思考(我不知道抽象两个层次)。这是一个新手犯下的典型错误的示例:

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

请注意,上述代码在没有指针概念而是名称(引用),对象和值之一的语言中是完全合理的,就像函数编程语言和带有垃圾回收的语言(Java,Python)一样。

递归函数的困难发生在没有足够数学背景(递归性是常识和必需知识)的人试图接近他们时,认为该函数的行为将取决于之前被调用的次数而有所不同。由于可以确实某种方式创建递归函数,而您必须以这种方式来理解它们因此该问题更加严重。

考虑一下传递指针的递归函数,例如在Red-Black Tree的过程实现中,就地修改了数据结构;与功能对应者相比,它很难考虑。

问题中没有提到它,但是新手遇到的另一个重要问题是并发性

正如其他人提到的那样,某些编程语言构造还有一个附加的,非概念上的问题:即使我们了解这些构造的简单而诚实的错误,也很难调试。


使用该函数将返回一个有效的指针,但该变量的作用域大于调用该函数的作用域,因此使用malloc时,指针(可能会)无效。
19:39正确

4
@Radek S:不,不会。它将返回一个无效的指针,该指针在某些环境下碰巧会工作一段时间,直到其他人覆盖它为止。(实际上,这将是堆栈,而不是堆。 malloc()这样做的可能性不比任何其他函数大。)
wnoise 2011年

1
@Radeck在示例函数中,指针指向该函数返回后将释放编程语言(在这种情况下为C)保证的内存。因此,返回的指针指向垃圾。只要在任何上下文中都引用了对象,带有垃圾回收的语言都会使该对象保持活动状态。
2011年

顺便说一句,Rust有指针但没有这些问题。(如果不是在不安全的情况下)
Sarge Borsch

2

指针和递归是两个独立的野兽,并且有不同的原因使它们各自“难于”。

通常,指针需要的心理模型与纯变量分配不同。当我有一个指针变量时,就是这样:一个指向另一个对象的指针,它包含的唯一数据是它指向的内存地址。因此,例如,如果我有一个int32指针并直接为其分配一个值,则我没有更改int的值,而是指向一个新的内存地址(您可以使用许多巧妙的技巧来完成此操作)。更有趣的是有一个指向指针的指针(当您在C#中将Ref变量作为函数Parameter传递时,会发生这种情况,该函数可以为Parameter分配一个完全不同的对象,并且当函数使用该值时,该值仍在范围内退出。

初次学习时,递归在思想上会略有飞跃,因为您是根据自身定义函数的。当您第一次遇到它时,这是一个疯狂的概念,但是一旦您掌握了这个想法,它就会成为第二天性。

但是回到眼前的话题。Joel的论点与其本身无关的是指针或递归,而是这样的事实,即学生将被进一步从计算机的实际工作方式中移除。这是计算机科学中的科学。学习编程和学习程序的工作方式有明显的区别。我认为“我以这种方式学习它,所以每个人都必须以这种方式学习”并不是什么大问题,因为他认为许多CS计划正成为光荣的贸易学校。


1

我给P. Brian一个+1,因为我感觉像他一样:递归是一个基本概念,他对此有丝毫困难,应该更好地考虑在Mac Donalds找工作,但是,即使有递归:

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

当然,缺乏理解也与我们的学校有关。这里应该介绍Peano,Dedekind和Frege这样的自然数,这样以后我们就不会有太多困难了。


6
那是尾巴的回缩,可以说是循环的。
Michael K

6
对不起,对我来说,循环可以说是尾部递归:)
Ingo

3
@Ingo::)功能狂热!
迈克尔·K

1
@Michael-的确如此!但是我认为可以证明递归是更基本的概念。
Ingo

@Ingo:的确可以(您的示例很好地说明了这一点)。但是,由于某些原因,人类在编程时会遇到困难- goto top由于某种原因,我们似乎想要IME付出更多。
Michael K

1

我不同意乔尔(Joel)的观点,问题是在多个抽象层次上进行思考,我认为,更多的是指针和递归是问题的两个很好的例子,这些问题需要人们改变关于程序工作方式的思维模型。

我认为,指针是更简单的例子。处理指针需要程序执行的心理模型,该模型说明程序实际使用内存地址和数据的方式。我的经验是,很多时候程序员在学习指针之前甚至都没有想到这一点。即使他们从抽象的角度了解它,也没有将其纳入程序运作方式的认知模型中。引入指针时,它要求他们对代码的工作方式进行根本性的转变。

递归是有问题的,因为有两个概念上的块需要理解。首先是在机器级别,并且与指针很像,可以通过对程序实际存储和执行方式的深入了解来克服它。我认为,递归的另一个问题是人们很自然地试图将递归问题解构为非递归问题,这使人们对递归函数作为格式塔的理解变得混乱。这可能是由于数学背景不足的人遇到的问题,或是不是将数学理论与程序开发联系在一起的思维模型。

问题是,我不认为指针和递归对于陷入思维模式不足的人们来说是唯一有问题的两个领域。并行性似乎是另一个被人们卡住并且难以适应其心理模型的领域,这只是指针和递归经常在面试中容易测试的时候。


1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

自引用数据和代码的概念分别构成了指针和递归的定义。不幸的是,对命令式编程语言的广泛了解使计算机科学专业的学生认为,当他们应该将此谜团相信于语言的功能方面时,他们必须通过运行时的操作行为来理解实现。将所有数字相加到一百似乎是一个简单的问题,即从一个数字开始并将其添加到序列中的下一个数字,然后借助循环自参考函数向后进行计算,这对于许多不习惯于安全性的人来说似乎是错误的甚至是危险的纯函数。

自我修改数据和代码的概念分别是对象(即智能数据)和宏的定义的基础。我提到这些是因为它们甚至更难以理解,尤其是当希望从所有四个概念的组合中获得对运行时的操作理解时-例如,宏生成一组对象,该对象集借助指针树实现递归体面的解析器。命令式程序员不仅要一次一步地遍历每个抽象层来跟踪程序状态的整个操作,还需要学会相信自己的变量在纯函数内仅被分配一次,并且使用相同的纯函数重复调用该函数。即使在同样支持不纯函数的语言(例如Java)中,相同的参数也始终会产生相同的结果(即引用透明性)。在运行时之后绕圈跑是徒劳的。抽象应该简化。


-1

与Anon的答案非常相似。
除了新手的认知困难之外,指针和递归都非常强大,并且可以以隐秘的方式使用。

强大功能的缺点在于,它们给您强大的功能,可以以微妙的方式修改程序。
将伪造的值存储到普通变量中已经足够糟糕,但是将伪造的值存储在指针中可能会导致各种延迟的灾难性事件发生。
更糟糕的是,当您尝试诊断/调试异常程序行为的原因时,这些影响可能会改变。

与递归类似。通过将棘手问题填充到隐藏的数据结构(堆栈)中,这可能是组织棘手事务的一种非常有效的方法。
但是,如果某件事做得很微妙,那可能很难弄清楚发生了什么。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.