ポインター/再帰の難しさは何ですか?[閉まっている]


20

Javaスクール危険の中で、ジョエルはペンでの経験と「セグメンテーションフォールト」の難しさについて話します。彼は言い​​ます

[セグメンテーション違反はあなたまで難しい]「深呼吸をして、同時に2つの異なる抽象化レベルで心を働かせようとする。」

セグメンテーション違反の一般的な原因のリストを考えると、2つの抽象化レベルでどのように作業する必要があるのか​​理解できません。

なんらかの理由で、Joelはこれらの概念をプログラマーが抽象化する能力の中核と考えています。あまり思い込みたくありません。それでは、ポインタ/再帰について何がそんなに難しいのでしょうか?例はいいでしょう。


31
ジョエルがあなたについてどう思うか心配するのはやめましょう。再帰が簡単だと思うなら、それは良いことです。誰もがそうするわけではありません。
FrustratedWithFormsDesigner

6
定義上、再帰は簡単です(自己を呼び出す関数)が、再帰をいつ使用し、どのように機能させるかを知るのは難しい部分です。
-JeffO

9
フォグクリークの求人に応募し、その方法をお知らせください。私たちは皆、あなたの自己宣伝に非常に興味を持っています。
ジョエルイーサートン

4
@ P.Brian.Mackey:私たちは誤解していません。質問は実際には何も尋ねません。それは露骨な自己宣伝です。あなたはジョエルは、ポインタ/再帰について尋ねているかを知りたい場合は、彼に尋ねる:team@stackoverflow.com
ジョエルEtherton

19
この質問の重複?
-ozz

回答:


38

私は最初、大学ではポインターと再帰が難しいことに気付きました。私はいくつかの典型的な初年度コースを受講しました(1つはCとアセンブラーで、もう1つはSchemeでした)。どちらのコースも数百人の学生で始まり、その多くは長年にわたって高校レベルのプログラミングの経験がありました(当時は通常BASICとPascal)。しかし、Cコースでポインターが導入され、Schemeコースで再帰が導入されるとすぐに、膨大な数の生徒(おそらく大多数)が完全に混乱しました。これらは以前にたくさんのコードを書いたことがあり、まったく問題はありませんでしたが、ポインターや再帰をヒットすると、認知能力の面でも壁にぶつかりました。

私の仮説では、ポインターと再帰は同じであり、同時に2つのレベルの抽象化を頭の中に保持する必要があります。複数レベルの抽象化には、ある種の精神的適性を必要とするものがあります。これは、一部の人には決してありえないことです。

  • ポインターの場合、「2つの抽象化レベル」とは、「データ、データのアドレス、データのアドレスのアドレスなど」、または従来「値と参照」と呼ばれるものです。訓練を受けていない学生にとっては、xのアドレスx自体の違いを見ることは非常に困難です。
  • 再帰では、「2つの抽象化レベル」は、関数がそれ自体を呼び出す方法を理解しています。再帰的アルゴリズムは「希望的思考によるプログラミング」と呼ばれることもありますが、より自然な「問題を解決するための手順のリスト」ではなく、「ベースケース+帰納的ケース」の観点からアルゴリズムを考えるのは非常に不自然です」再帰的なアルゴリズムを見て訓練を受けていない学生にとって、アルゴリズムは質問請うように見えます。

また、ポインターや再帰を誰にでも教えることができることを完全に喜んで受け入れます...私は何らかの形で証拠を持っていません。経験的に、これらの2つの概念を本当に理解できることは、一般的なプログラミング能力の非常に優れた予測因子であり、学部のCSトレーニングの通常のコースでは、これら2つの概念が最大の障害の一部であることを知っています。


4
「「ベースケース+帰納的ケース」の観点からアルゴリズムを考えるのは非常に不自然です」-それは決して不自然なことではなく、子供たちがそれに応じて訓練されていないだけだと思います。
インゴ

14
それが自然なものであれば、訓練を受ける必要はないでしょう。:P
ジョエルスポルスキー

1
良い点:)、しかし、数学、論理、物理学などの訓練を必要としないでください。これらはすべて、より広い意味で最も自然なものです。興味深いことに、言語の構文に問題があるプログラマーはほとんどいませんが、再帰に満ちています。
インゴ

1
私の大学では、最初のコースは、突然変異などを導入するかなり前に、関数型プログラミングと再帰からほぼ即座に始まりました。私は学生のいくつかのことがわかっなし経験がより良いとのそれらよりも再帰を理解し、いくつかの経験。とはいえ、クラスの一番上は多くの経験を持つ人々で構成されていました。
ティコンジェルビス

2
ポインターと再帰を理解できないことは、a)全体的なIQレベルとb)悪い数学教育に関連していると思います。
quant_dev

23

再帰は、「自分自身を呼び出す関数」ではありません。再帰降下パーサーで何が問題になったかを把握するためにスタックフレームを描画するまで、再帰が難しい理由を本当に理解するつもりはありません。多くの場合、相互に再帰的な関数があります(関数Aは関数Bを呼び出し、関数Bは関数Cを呼び出し、関数Cは関数Aを呼び出します)。相互再帰的な一連の関数の中でNスタックフレームの深さがあるときに何がうまくいかなかったのかを把握するのは非常に困難です。

ポインタに関しても、ポインタの概念は非常に単純です。メモリアドレスを格納する変数です。繰り返しvoid**ますが、異なるノードを指すポインターの複雑なデータ構造に問題が発生した場合、ポインターの1つがガベージアドレスを指している理由を突き止めるのに苦労するので、なぜそれがトリッキーになるのかがわかります。


1
再帰的で適切なパーサーを実装したのは、再帰をある程度理解していると本当に感じたときでした。あなたが言ったように、ポインタは簡単に理解できます。ポインタを処理する実装の要点を理解するまで、それらが複雑な理由はわかりません。
クリス

多くの関数間の相互再帰は、本質的にと同じgotoです。
スターブルー

2
@starblueではありません-各スタックフレームはローカル変数の新しいインスタンスを作成するためです。
チャールズサルビア

そのとおりです。末尾再帰のみがと同じgotoです。
スターブルー

3
@wnoise int a() { return b(); }は再帰的ですが、の定義に依存しbます。そのため、見かけほど単純ではありません
代替案

14

Javaはポインターをサポートし(参照と呼ばれます)、再帰をサポートします。したがって、表面的には、彼の議論は無意味に見えます。

彼が本当に話しているのは、デバッグする能力です。Javaポインター(err、参照)は、有効なオブジェクトを指すことが保証されています。ACポインターはそうではありません。また、valgrindのようなツールを使用しないと仮定した場合のCプログラミングの秘theは、ポインターをどこにねじ込んだかを正確に見つけることです(スタックトレースで見つかった時点ではめったにありません)。


5
ポインター自体が詳細です。Javaでの参照の使用は、Cでのローカル変数の使用ほど複雑ではありません。Lisp実装のようにそれらを混在させることもできます(アトムはサイズが制限された整数、文字、またはポインター)。言語が同じ種類のデータを異なる構文でローカルまたは参照できるようにする場合は難しくなり、言語がポインター演算を許可する場合は非常に難しくなります。
デビッドソーン

@David-ええと、これは私の応答と何の関係がありますか?
アノン

1
ポインターをサポートするJavaについてのコメント。
デビッドソーンリー

「ポインタを台無しにした場所(スタックトレースで見つかった時点ではめったにありません)。」スタックトレースを取得できるほど幸運な場合。
オメガケンタウリ

5
David Thornleyに同意します。Javaは、intへのポインターへのポインターへのポインターを作成できない限り、ポインターをサポートしません。それぞれ4〜5個のクラスを作成して、それぞれが別の何かを参照することができると思いますが、それは本当にポインターなのでしょうか、それともい回避策ですか?
代替案

12

ポインターと再帰の問題は、必ずしも理解するのが難しいということではなく、特に CやC ++などの言語に関して不適切に教えられていることです(主に言語自体がひどく教えられているため)。誰かが「配列は単なるポインターである」と言う(または読む)たびに、私は少し死んでしまいます。

同様に、誰かがフィボナッチ関数を使用して再帰を説明するたびに、悲鳴を上げたいと思います。反復バージョンが書き込みに何ら困難ですので、それは悪い例ませんし、それは同様またはより優れた再帰1よりも少なくとも実行し、それはあなたの再帰的な解決策が有用または望ましいであろう理由に本当の洞察力を与えません。クイックソート、ツリートラバーサルなどは、再帰の理由と方法のはるかに優れた例です。

ポインターをいじる必要があるのは、それらを公開するプログラミング言語で作業することの成果です。Fortranプログラマーの世代は、専用のポインタータイプ(または動的メモリ割り当て)を必要とせずにリストとツリー、スタック、およびキューを構築していました。Fortranがおもちゃの言語であると非難する人はいません。


私は同意します、実際のポインタを見る前にFortranの数年/数十年を持っていたので、lanquage /コンパイラにそれをさせる機会を与えられる前に、私はすでに同じことをする独自の方法を使用していました。また、アドレスに格納されている値の概念は非常に単純ですが、ポインター/アドレスに関するC構文は非常に紛らわしいと思います。
オメガケンタウリ

Fortran IVに実装されたQuicksortへのリンクがあれば、ぜひご覧ください。それができないと言っていない-実際には、私は約30年前にBASICでそれを実装しました-しかし、私はそれを見て興味があります。
アノン

私はFortran IVで働いたことはありませんでしたが、Fortran 77のVAX / VMS実装にいくつかの再帰アルゴリズムを実装しました(gotoのターゲットを特別な種類の変数として保存できるフックがあったので、書くことができましたGOTO target) 。ただし、独自のランタイムスタックを構築する必要があったと思います。これはかなり前だったので、もう詳細を思い出せません。
ジョンボード

8

ポインターにはいくつかの問題があります。

  1. エイリアシング異なる名前/変数を使用してオブジェクトの値を変更する可能性。
  2. 非局所性宣言されているコンテキストとは異なるコンテキストでオブジェクト値を変更する可能性(これは、参照渡しの引数でも発生します)。
  3. ライフタイムの不一致ポインターのライフタイムは、ポインターが指すオブジェクトのライフタイムと異なる場合があり、無効な参照(SEGFAULTS)またはガベージにつながる可能性があります。
  4. ポインタ演算。一部のプログラミング言語では、ポインターを整数として操作できます。つまり、ポインターはどこでも指すことができます(バグが存在する場合の最も予期しない場所を含む)。ポインター演算を正しく使用するには、プログラマーが指すオブジェクトのメモリーサイズを認識している必要があります。これについては、さらに考慮する必要があります。
  5. 型キャストある型から別の型にポインターをキャストする機能により、意図したものとは異なるオブジェクトのメモリを上書きできます。

そのため、プログラマーはポインターを使用するとき、より徹底的に考える必要があります(抽象化の2つのレベルについては知りません)。これは、初心者が犯す典型的な間違いの例です。

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

上記のようなコードは、ポインターの概念を持たない言語では完全に合理的であることに注意してください。関数型プログラミング言語やガベージコレクション(Java、Python)のある言語のように、名前(参照)、オブジェクト、値のいずれかです。

再帰関数の難しさは、十分な数学的背景のない人々(再帰性が一般的で必要な知識)が、関数が以前に呼び出された回数に応じて異なる動作をすると考えてアプローチしようとするときに発生します。再帰関数があるため、その問題を悪化さ確かにできている方法で作成されますが、その方法を考える必要がありますそれらを理解します。

データ構造がその場で変更されるRed-Black Treeの手続き型実装のように、ポインタが渡される再帰関数を考えてください。それは機能的な対応者より考えるのがより難しいものです。

質問では言及されていませんが、初心者が困難に感じる他の重要な問題は並行性です。

他の人が述べたように、いくつかのプログラミング言語の構成要素には、概念的ではない追加の問題があります。理解できたとしても、それらの構成要素の単純で正直な間違いはデバッグが非常に困難です。


その関数を使用すると有効なポインターが返されますが、変数は関数を呼び出したスコープよりも高いスコープにあるため、mallocが使用されている場合はポインターが無効になる可能性があります。
右折

4
@Radek S:いいえ、ありません。無効なポインターを返します。これは、環境によっては、他の何かが上書きするまでしばらく動作するものです。(実際には、これはスタックであり、ヒープでmalloc()はありません。 そうする可能性は他のどの関数よりも高くありません。)
wnoise

1
@Radeckサンプル関数では、ポインターは、関数が返されるとプログラミング言語(この場合はC)が解放されることを保証するメモリを指します。したがって、返されるポインターはgarbageを指します。ガベージコレクションのある言語は、任意のコンテキストで参照されている限り、オブジェクトを存続させます。
アパララ

ところで、Rustにはポインターがありますが、これらの問題はありません。(安全でない状況ではない場合)
セージボルシュ

2

ポインターと再帰は2つの別個の獣であり、それぞれが「困難」であるとみなされるさまざまな理由があります。

一般に、ポインターには、純粋な変数の割り当てとは異なるメンタルモデルが必要です。ポインタ変数を持っているとき、それはただです:別のオブジェクトへのポインタ、それに含まれる唯一のデータはそれが指すメモリアドレスです。たとえば、int32ポインターがあり、値を直接割り当てる場合、intの値は変更せず、新しいメモリアドレスを指定します(これでできる巧妙なトリックがたくさんあります) )。さらに興味深いのは、ポインターへのポインターを持つことです(C#で関数パラメーターとしてRef変数を渡すと、関数はまったく異なるオブジェクトをパラメーターに割り当てることができ、その値は、関数が終了します。

再帰は、関数をそれ自体の観点から定義しているため、最初の学習時にわずかな精神的飛躍を行います。最初に出くわすと、それはワイルドなコンセプトですが、一度アイデアを理解すると、それは第二の性質になります。

しかし、手元の主題に戻ります。ジョエルの主張は、それ自体でのポインターや再帰に関するものではなく、コンピューターが実際に機能する方法から学生がさらに削除されているという事実です。これはコンピューターサイエンスの科学です。プログラムの学習とプログラムの仕組みの学習には明確な違いがあります。多くのCSプログラムが栄誉ある貿易学校になりつつあると彼が主張しているので、「私はこの方法で学んだので誰もがこの方法で学ばなければならない」ということはそれほど問題ではないと思います。


1

私はP.ブライアンに+1を与えます。なぜなら、彼はそうしているように感じるからです。

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
それは尾の反抗であり、間違いなくループしている。
マイケルK

6
申し訳ありませんが、ループは間違いなくテール再帰です:)
Ingo

3
@Ingo::)機能的な狂信者!
マイケルK

1
@Michael-確かに!、しかし、再帰がより基本的な概念であると主張することができると思います。
インゴ

@Ingo:確かに可能です(あなたの例はそれをよく示しています)。しかし、なんらかの理由で、人間はプログラミングに苦労しています- goto top何らかの理由でIME を追加する必要があるようです。
マイケルK

1

私は問題が抽象化自体の複数のレベルで考えることの1つであるというジョエルに反対します、私はポインターと再帰が人々がプログラムの働き方について持っている精神モデルの変化を必要とする問題の2つの良い例であるともっと思います。

ポインターは、説明するのにもっと簡単なケースだと思います。ポインターを扱うには、プログラムがメモリアドレスとデータを実際に処理する方法を説明するプログラム実行のメンタルモデルが必要です。私の経験では、多くの場合、プログラマーはポインターについて学ぶ前にこれについて考えさえしていませんでした。たとえ抽象的な意味でそれを知っていたとしても、プログラムの動作の認知モデルにそれを採用していません。ポインターが導入されると、コードの動作についての考え方を根本的に変える必要があります。

理解するには2つの概念ブロックがあるため、再帰には問題があります。1つ目はマシンレベルであり、ポインターのように、プログラムが実際に格納および実行される方法を十分に理解することで克服できます。再帰に関する他の問題は、再帰的な問題を非再帰的な問題に分解しようとする自然な傾向があることだと思います。これは、数学的背景が不十分な人々の問題か、数学的理論をプログラムの開発に結び付けない精神モデルのいずれかです。

問題は、ポインターと再帰が、不十分なメンタルモデルにとらわれている人々にとって問題となる2つの唯一の領域だとは思わないことです。並列処理は、一部の人々が単に行き詰まって精神的なモデルを適応させるのに苦労している別の領域のようです。


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

自己参照データと自己参照の概念は、それぞれポインターと再帰の定義の根底にあります。残念ながら、命令型プログラミング言語への広範囲な露出により、コンピューターサイエンスの学生は、言語の機能的な側面にこの謎を信頼する必要がある場合、ランタイムの操作上の動作によって実装を理解する必要があると信じるようになりました。100までのすべての数値を合計することは、1から始めてシーケンスの次の数値に追加し、循環自己参照関数を使用して逆方向に実行するという単純な問題のように思えます。純粋な機能。

自己修正データとコードの概念は、それぞれオブジェクト(スマートデータ)とマクロの定義の根底にあります。特に、4つの概念すべての組み合わせからランタイムの操作上の理解が期待される場合、これらは理解するのがさらに難しいため、これらに言及します。たとえば、ポインターツリーの助けを借りて再帰まともなパーサーを実装するオブジェクトのセットを生成するマクロ。命令型プログラマーは、抽象化のすべてのレイヤーを段階的にプログラムの状態の操作全体を一度にトレースするのではなく、変数が純粋な関数内で一度だけ割り当てられ、同じ純粋な関数の繰り返し呼び出しを信頼することを学ぶ必要がありますJavaのような不純な機能もサポートする言語であっても、同じ引数は常に同じ結果(つまり、参照透過性)をもたらします。実行後に円を描くことは実りのない努力です。抽象化は簡素化されるはずです。


-1

アノンの答えに非常に似ています。
初心者にとっての認知の問題は別として、ポインターと再帰の両方は非常に強力であり、不可解な方法で使用できます。

大きな力のマイナス面は、微妙な方法でプログラムを台無しにする大きな力を与えることです。
偽の値を通常の変数に格納するだけでは十分ではありませんが、ポインターに偽の値を格納すると、あらゆる種類の遅延した壊滅的なことが発生する可能性があります。
さらに悪いことに、奇妙なプログラムの動作の原因を診断/デバッグしようとすると、これらの効果が変わる可能性があります。しかし、何かが微妙に間違って行われた場合、何が起こっているのかを把握するのは困難です。

同様に再帰。トリッキーなものを隠れたデータ構造(スタック)に詰め込むことで、トリッキーなものを整理する非常に強力な方法になります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.