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');
...
メモリ レイアウト:
時間 v ---[ttttNNNNNNNNNN]--- 1234私の家
ポインタ値をコピー
新しい紙に住所を書くだけです。これで、2 つの別々の家ではなく、同じ家に行くことができる 2 枚の紙ができました。ある紙の住所をたどってその家の家具を再配置しようとすると、実際には 1 つの家であることを明示的に検出できない限り、他の家も同じように変更されているように見えます。
注これは通常、私が人々に説明するのに最も問題がある概念です。2 つのポインターは、2 つのオブジェクトまたはメモリ ブロックを意味するわけではありません。
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
の呼び出しの後に使用するとうまくいく.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
変数の内容を新しい家の住所で上書きしましたが、古い家はまだ立っています... どこかに。このコードの後、その家に到達する方法はなく、立ったままになります。つまり、割り当てられたメモリは、アプリケーションが閉じられるまで割り当てられたままになり、その時点でオペレーティング システムによって破棄されます。
最初の割り当て後のメモリ レイアウト:
時間 v ---[ttttNNNNNNNNNN]--- 1234私の家
2 番目の割り当て後のメモリ レイアウト:
時間 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私の家 <--+
ご覧のとおり、古いデータはメモリ内にそのまま残り、メモリ アロケータによって再利用されません。アロケータは、メモリのどの領域が使用されたかを追跡し、解放しない限り再利用しません。
メモリを解放するが、(現在は無効な) 参照を保持する
家を取り壊し、紙片の 1 つを消去しますが、古い住所が記載された別の紙もあります。その住所に行くと、家は見つかりませんが、廃墟に似たものが見つかる場合があります。ひとつの。
家を見つけることさえあるかもしれませんが、それは最初に住所を与えられた家ではありません。
場合によっては、隣の住所に 3 つの住所 (大通り 1 ~ 3) を占有するかなり大きな家が建てられており、あなたの住所が家の真ん中にあることに気付くことさえあります。大きな 3 住所の家のその部分を 1 つの小さな家として扱う試みも、恐ろしく失敗する可能性があります。
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
が、古い古い住所が残っています。立っていない家へのアクセスは、機能する場合と機能しない場合があります。
これは、上記のダングリング ポインターのバリエーションです。そのメモリ レイアウトを参照してください。
バッファ オーバーラン
収まりきらないほど多くのものを家に移動し、隣の家や庭にこぼれます。その隣の家の所有者が後で家に帰ったとき、彼は自分のものと考えるあらゆる種類のものを見つけるでしょう.
これが、固定サイズの配列を選択した理由です。ステージを設定するために、割り当てた 2 番目の家が何らかの理由でメモリ内の最初の家の前に配置されると仮定します。つまり、2 番目の家は最初の家よりも低い住所になります。また、それらは互いに隣接して割り当てられます。
したがって、このコード:
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私の家
2 番目の割り当て後のメモリ レイアウト:
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 が追加されました。下の図では 4 つの LLLL で示されています):
h1 h2 vv ---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL] 1234ホーム + 5678キャビン + | | ^ | +--------+ * (リンクなし)
基本的に、メモリアドレスとは何ですか?
メモリアドレスは、基本的には単なる数字です。メモリをバイトの大きな配列と考えると、最初のバイトのアドレスは 0、次のバイトのアドレスは 1 というように上へと続きます。これは単純化されていますが、十分です。
したがって、このメモリ レイアウトは次のようになります。
h1 h2 vv ---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN] 1234私の家 5678私の家
次の 2 つのアドレスがある可能性があります (左端 - アドレス 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;
ビルはサムを指さします。
私はあなたがアイデアを得ると思います。ポインター割り当ての基本を説明するまで、これに約 1 時間費やしたと思います。
ポインタを説明するのに役立つと私が見つけたアナロジーはハイパーリンクです。ほとんどの人は、Webページ上のリンクがインターネット上の別のページを「指している」ことを理解できます。そのハイパーリンクをコピーして貼り付けることができれば、両方とも同じ元のWebページを指します。その元のページに移動して編集し、それらのリンク(ポインター)のいずれかをたどると、その新しい更新されたページが表示されます。
ポインターが非常に多くの人を混乱させているように見える理由は、ほとんどの場合、ポインターがコンピューター アーキテクチャの背景知識をほとんど、またはまったく持っていないためです。多くの人は、コンピューター (マシン) が実際にどのように実装されているかを理解していないように見えるため、C/C++ での作業は異質に思えます。
ドリルは、ポインター操作 (ロード、ストア、直接/間接アドレッシング) に焦点を当てた命令セットを使用して、単純なバイトコード ベースの仮想マシン (選択した任意の言語で、Python がこれに最適です) を実装するように依頼することです。次に、その命令セット用の簡単なプログラムを作成するように依頼します。
単純な足し算以上のものを必要とするものはすべてポインターを伴い、確実にそれを取得します。
C/C++ 言語を使用する多くの新入生、さらには古い大学レベルの学生にとって、ポインタが混乱の主な要因である理由は何ですか?
値 (変数) のプレースホルダーの概念は、学校で教えられている代数に対応します。コンピューター内でメモリが物理的にどのように配置されているかを理解せずに描くことができる既存の類似点はありません。また、C/C++/バイト通信レベルで低レベルのものを扱うまで、誰もこの種のことについて考えません。 .
ポインターが変数、関数、およびその先のレベルでどのように機能するかを理解するのに役立つツールや思考プロセスはありますか?
アドレス ボックス。BASIC をマイクロコンピューターにプログラミングする方法を学んでいたときのことを覚えています。ゲームが載っているきれいな本があり、特定のアドレスに値を突っ込まなければならないこともありました。彼らは箱の束の写真を持っていて、0、1、2... と段階的にラベル付けされていて、これらの箱には 1 つの小さなもの (1 バイト) しか収まらないと説明されていました。 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 ではなくアドレスになっています。
他の誰かがすでにこのチュートリアルにリンクしていますが、ポインターを理解し始めた瞬間を強調できます。
C のポインターと配列に関するチュートリアル: 第 3 章 - ポインターと文字列
int puts(const char *s);
しばらくの間、
const.
渡されたパラメーターputs()
はポインター、つまりポインターの値 (C ではすべてのパラメーターが値で渡されるため) であり、ポインターの値はそれが指すアドレスである、または単純に無視します。 、 アドレス。したがって、puts(strA);
これまで見てきたように記述すると、strA[0] のアドレスが渡されます。
これらの言葉を読んだ瞬間、雲が切れ、一筋の太陽光線が私をポインターの理解で包み込みました。
あなたが VB .NET または C# の開発者 (私のように) であり、安全でないコードを使用したことがない場合でも、ポインターのしくみを理解する価値はあります。そうしないと、オブジェクト参照のしくみを理解できません。次に、オブジェクト参照をメソッドに渡すとオブジェクトがコピーされるという、よくあるが間違った考えを持つことになります。
TedJensenの「Cのポインタと配列に関するチュートリアル」はポインタについて学ぶための優れたリソースであることがわかりました。ポインターとは何か(およびその目的)の説明から始まり、関数ポインターで終わる10のレッスンに分かれています。http://web.archive.org/web/20181011221220/http://home.netcom.com:80/~tjensen/ptr/cpoint.htm
そこから先に進むと、Beejのネットワークプログラミングガイドでは、UnixソケットAPIについて説明しています。このAPIから、本当に楽しいことを始めることができます。http://beej.us/guide/bgnet/
ポインターの複雑さは、私たちが簡単に教えられることを超えています。生徒同士で指さし合ったり、家の住所が書かれた紙を使ったりすることは、どちらも優れた学習ツールです。彼らは基本的な概念を紹介する素晴らしい仕事をしています。実際、基本的な概念を学ぶことは、ポインターをうまく使用するために不可欠です。ただし、運用コードでは、これらの単純なデモでカプセル化できるよりもはるかに複雑なシナリオに入るのが一般的です。
私は、他の構造を指す他の構造を指す構造を持つシステムに携わってきました。これらの構造の一部には、(追加の構造へのポインターではなく) 埋め込み構造も含まれていました。これは、ポインターが本当に混乱する場所です。複数レベルの間接化があり、次のようなコードになってしまう場合:
widget->wazzle.fizzle = fazzle.foozle->wazzle;
非常にすぐに混乱する可能性があります (より多くの行と潜在的により多くのレベルを想像してみてください)。ポインターの配列、およびノードからノードへのポインター (ツリー、リンクされたリスト) を投入すると、さらに悪化します。基本をよく理解している開発者でさえ、そのようなシステムに取り組み始めると、非常に優れた開発者が道に迷ってしまうのを見てきました。
ポインターの複雑な構造も、必ずしもコーディングが不十分であることを示しているわけではありません (可能性はありますが)。コンポジションは、優れたオブジェクト指向プログラミングの重要な部分であり、生のポインターを使用する言語では、必然的に多層の間接化につながります。さらに、システムは、スタイルや技術が互いに一致しない構造を持つサードパーティのライブラリを使用する必要があることがよくあります。そのような状況では、当然、複雑さが生じます (確かに、可能な限りそれと戦う必要があります)。
学生がポインターを学ぶのを助けるために大学ができる最善のことは、ポインターの使用を必要とするプロジェクトと組み合わせて、優れたデモンストレーションを使用することだと思います. 1 つの難しいプロジェクトは、1000 のデモンストレーションよりもポインターの理解に役立ちます。デモンストレーションは浅い理解を得ることができますが、ポインターを深く理解するには、それらを実際に使用する必要があります。
概念としてのポインターは特にトリッキーではないと思います。ほとんどの学生のメンタル モデルはこのようなものにマッピングされており、いくつかの簡単なボックス スケッチが役立ちます。
少なくとも私が過去に経験し、他の人が対処しているのを見た難しさは、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 |
これらの車はすべて同じタイプ (車) であるため、これを考える 1 つの方法は、私たちの車はある種のデータ (たとえばint
) ですが、異なる値 ( blue
、red
、green
; 色である可能性がありますenum
)を持っているということです。
ポインタを入力してください
この駐車場に連れて行って、青い車を探すように頼んだら、指を 1 本伸ばして、スポット 1 の青い車を指します。これは、ポインターを取得してメモリ アドレスに割り当てるようなものです。 ( int *finger = parking_lot
)
あなたの指 (ポインター) は、私の質問に対する答えではありません。指を見ても何もわかりませんが、指が指している場所(ポインターを逆参照) を見ると、探していた車 (データ) を見つけることができます。
ポインターの再割り当て
代わりに赤い車を探すように頼むことができます。指を新しい車に向け直してください。今、あなたのポインター (前と同じもの) は、同じタイプ (車) の新しいデータ (赤い車が見つかる駐車場) を示しています。
ポインターは物理的に変化していません。それはまだあなたの指であり、私に表示されていたデータが変化しただけです。(「駐車場」住所)
ダブルポインター (またはポインターへのポインター)
これは、複数のポインターでも機能します。赤い車を指しているポインタがどこにあるか尋ねることができます。もう一方の手で人差し指を指してください。(これは のようなものですint **finger_two = &finger
)
青い車がどこにあるかを知りたい場合は、人差し指の方向を 2 番目の指の車 (データ) にたどることができます。
ぶら下がっているポインター
ここで、あなたが彫像のように感じていて、赤い車をいつまでも指さしたいと思っているとしましょう。あの赤い車が走り去ったら?
1 2 3
-------------------
| o=o | | o=o |
| |B| | | |G| |
| o-o | | o-o |
ポインターは赤い車があった場所を指し続けていますが、もうありません。新しい車がそこに引っ張られたとしましょう... オレンジ色の車。「赤い車はどこですか」ともう一度聞いたら、あなたはまだそこを指していますが、今は間違っています。あれは赤い車じゃない、オレンジだ。
ポインター演算
わかりました。まだ 2 番目の駐車場を指しています (現在はオレンジ色の車が占有しています)。
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 および 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」が何から始まるのか、そしてそれをどうするつもりなのかにもよります。この言語は、実際に発生するネストのレベルをサポートしていません。新しいバッファを作成する関数に「ポインタ」を渡す必要があり、ポインタがバッファの新しい場所を指すように変更する場合のように。本当にポインターが必要なのか、ポインターへのポインターが必要なのか、ポインターの内容を変更するためにどこに行くべきかを知っています。ほとんどの場合、「」の意味を推測するだけです。
「ポインター」はオーバーロードされすぎています。ポインタは値へのアドレスですか? または、値へのアドレスを保持する変数です。関数がポインターを必要とする場合、ポインター変数が保持するアドレスが必要ですか、それともポインター変数へのアドレスが必要ですか? よくわかりません。
ポインターを習得するのが難しいのは、ポインターまでは、「このメモリ位置には、int、double、文字などを表すビットのセットがある」という考えに慣れているからだと思います。
ポインタを初めて見たとき、そのメモリ位置に何があるかはわかりません。「どういう意味ですか、それはアドレスを保持していますか?」
「理解できるかできないかのどちらか」という考えには同意しません。
それらの実際の用途を見つけ始めると、理解しやすくなります (大きな構造体を関数に渡さないなど)。
私は家の住所の例えが好きですが、住所はメールボックス自体のものだといつも思っていました。このようにして、ポインターの逆参照(メールボックスを開く)の概念を視覚化できます。
たとえば、リンクリストをたどる:1)住所のある論文から始める2)論文の住所に移動する3)メールボックスを開いて、次の住所のある新しい論文を探す
線形リンクリストでは、最後のメールボックスには何も含まれていません(リストの最後)。循環リンクリストでは、最後のメールボックスに最初のメールボックスのアドレスが含まれています。
ステップ3は、間接参照が発生する場所であり、アドレスが無効な場合にクラッシュまたは失敗する場所であることに注意してください。無効なアドレスのメールボックスまで歩いて行くことができると仮定して、世界を裏返しにするブラックホールまたは何かがそこにあると想像してください:)
人々が問題を抱えている主な理由は、一般的に興味深く魅力的な方法で教えられていないためだと思います. 講演者が群衆の中から 10 人のボランティアを集めて、それぞれに 1 メートルの定規を渡し、特定の構成で立ち回り、定規を使ってお互いを指さすようにしてもらいたいです。次に、人を動かしてポインター演算を表示します (および定規を指している場所)。これは、メカニズムに行き詰まりすぎずにコンセプトを示すための、シンプルだが効果的な (そして何よりも記憶に残る) 方法です。
C や C++ にたどり着くと、一部の人にとっては難しくなるようです。これが、彼らが適切に把握していない理論を最終的に実践に移したためなのか、それともそれらの言語ではポインター操作が本質的に難しいためなのかはわかりません。自分の変遷はよく覚えていませんが、パスカルでポインタを知っていて、それから C に移行して、完全に道に迷ってしまいました。
ポインタ自体が紛らわしいとは思いません。ほとんどの人は概念を理解できます。さて、いくつのポインタについて考えることができますか、または何レベルの間接化に満足していますか. 人々を窮地に陥れるのに、それほど多くは必要ありません。プログラムのバグによって誤って変更される可能性があるという事実は、コードで問題が発生した場合のデバッグを非常に困難にする可能性もあります。
実際には構文の問題である可能性があると思います。ポインターの C/C++ 構文は一貫性がなく、必要以上に複雑に見えます。
皮肉なことに、ポインターを理解するのに実際に役立ったのは、 c++標準テンプレート ライブラリの反復子の概念に遭遇したことでした。皮肉なことに、反復子はポインターの一般化として考えられているとしか思えません。
木を無視することを学ぶまで、森が見えないことがあります。
私書箱番号。
これは、他の何かにアクセスできるようにする情報です。
(また、私書箱の番号を計算すると、問題が発生する可能性があります。なぜなら、手紙が間違った箱に入ってしまうからです。また、転送先住所がない別の州に誰かが移動すると、ポインタがぶら下がります。一方、郵便局が郵便物を転送する場合は、ポインターへのポインターがあります。)
イテレータを介してそれを把握するのは悪い方法ではありません..しかし、見続けると、Alexandrescuがそれらについて不平を言い始めていることがわかります。
多くの元 C++ 開発者 (言語をダンプする前に、イテレーターが最新のポインターであることを理解していなかった) は、C# にジャンプし、まだ適切なイテレーターがあると信じています。
うーん、問題は、すべてのイテレータが、ランタイム プラットフォーム (Java/CLR) が達成しようとしているものと完全に一致していないことです: 新しい、シンプルな、全員が開発者の使用法です。それは良いことかもしれませんが、彼らは紫色の本で一度それを言いました、そして彼らはCの前と前でもそれを言いました:
間接。
非常に強力な概念ですが、完全に実行すると決してそうではありません. イテレータは、アルゴリズムの抽象化に役立つため、別の例として役立ちます。そして、コンパイル時は非常に単純なアルゴリズムの場所です。コード + データ、または他の言語の C# を知っている:
IEnumerable + LINQ + Massive Framework = 300MB の実行時ペナルティ 参照型のインスタンスのヒープを介してアプリケーションを引きずるお粗末な間接参照..
「ル・ポインターは安いです。」
すべての C/C++ 初心者は同じ問題を抱えていますが、その問題は「ポインターが習得しにくい」ためではなく、「誰がどのように説明されているか」が原因で発生します。一部の学習者は口頭で視覚的に収集しますが、それを説明する最良の方法は、「トレーニング」の例を使用することです(口頭および視覚的な例に適しています)。
ここで、 「機関車」は何も保持できないポインターであり、 「ワゴン」は「機関車」が引っ張ろうとする (または指し示す) ものです。その後、「ワゴン」自体を分類し、動物、植物、または人 (またはそれらの混合物) を乗せることができます。
ポインターについて何がそれほど混乱しているのかわかりません。それらはメモリ内の場所を指します。つまり、メモリアドレスを格納します。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は、それについて私よりも少し首尾一貫して話しています。:-)