ポインターが、CまたはC ++の多くの新しい、さらには古い、大学生レベルの学生にとって混乱の主な要因であるのはなぜですか?ポインターが変数、関数、およびレベルを超えてどのように機能するかを理解するのに役立つツールまたは思考プロセスはありますか?
誰かが「ああ、わかった」のレベルに到達するために、全体的なコンセプトに夢中にならないようにするために実行できるいくつかの優れた実践方法は何ですか?基本的に、シナリオのようにドリルします。
ポインターが、CまたはC ++の多くの新しい、さらには古い、大学生レベルの学生にとって混乱の主な要因であるのはなぜですか?ポインターが変数、関数、およびレベルを超えてどのように機能するかを理解するのに役立つツールまたは思考プロセスはありますか?
誰かが「ああ、わかった」のレベルに到達するために、全体的なコンセプトに夢中にならないようにするために実行できるいくつかの優れた実践方法は何ですか?基本的に、シナリオのようにドリルします。
回答:
ポインターは、多くの人にとって最初は混乱する可能性がある概念です。特に、ポインター値をコピーして同じメモリーブロックを参照する場合は特にそうです。
一番の類似点は、ポインターを家の住所が記載された紙のようなものと見なし、メモリブロックが実際の家として参照することです。したがって、あらゆる種類の操作を簡単に説明できます。
以下にDelphiコードをいくつか追加し、必要に応じてコメントもいくつか追加しました。Delphiを選択した理由は、他の主要なプログラミング言語であるC#が、メモリリークなどを同様に示さないためです。
ポインタの高レベルの概念のみを学習したい場合は、以下の説明で「メモリレイアウト」というラベルの付いた部分を無視してください。これらは、操作後のメモリの例を示すことを目的としていますが、実際には低レベルです。ただし、バッファオーバーランが実際にどのように機能するかを正確に説明するために、これらの図を追加することが重要でした。
免責事項:すべての意図と目的のために、この説明とメモリレイアウトの例は大幅に簡略化されています。低レベルでメモリを処理する必要がある場合、知っておく必要のあるオーバーヘッドと詳細が多くなります。ただし、メモリとポインタを説明する目的では、十分に正確です。
以下で使用されるTHouseクラスが次のようになっていると仮定します。
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
houseオブジェクトを初期化すると、コンストラクターに指定された名前がプライベートフィールドFNameにコピーされます。固定サイズの配列として定義されている理由があります。
メモリでは、家の割り当てに関連するオーバーヘッドがいくつかあります。これを以下のように説明します。
--- [ttttNNNNNNNNNN] --- ^ ^ | | | +-FName配列 | +-オーバーヘッド
「tttt」領域はオーバーヘッドです。通常、8バイトや12バイトなど、さまざまなタイプのランタイムや言語では、これよりも多くなります。この領域に格納されている値がメモリアロケータまたはコアシステムルーチン以外で変更されないようにする必要があります。変更しないと、プログラムがクラッシュするおそれがあります。
メモリを割り当てる
起業家に家を建ててもらい、家の住所を教えてください。現実の世界とは対照的に、メモリ割り当てはどこに割り当てるかを伝えることはできませんが、十分なスペースがある適切なスポットを見つけて、割り当てられたメモリにアドレスを報告します。
言い換えれば、起業家はスポットを選択します。
THouse.Create('My house');
メモリレイアウト:
--- [ttttNNNNNNNNNN] --- 1234:私の家
変数をアドレスで保持する
新しい家の住所を紙に書き留めます。この紙はあなたの家への参照として役立ちます。この紙切れがなければ、あなたは迷子になっていて、家に入っていなければ家を見つけることができません。
var
h: THouse;
begin
h := THouse.Create('My house');
...
メモリレイアウト:
h v --- [ttttNNNNNNNNNN] --- 1234:私の家
ポインタ値をコピー
新しい紙に住所を書いてください。これで、2つの別々の家ではなく、同じ家に行くための2枚の紙があります。1つの紙の住所をたどり、その家の家具を並べ替えようとすると、実際には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
変数の内容を新しい家の住所で上書きしましたが、古い家はまだ残っています...どこかにあります。このコードの後、その家に到達する方法はなく、そのまま残されます。つまり、割り当てられたメモリは、アプリケーションが閉じるまで割り当てられたままであり、その時点でオペレーティングシステムはアプリケーションを破棄します。
最初の割り当て後のメモリレイアウト:
h v --- [ttttNNNNNNNNNN] --- 1234:私の家
2番目の割り当て後のメモリレイアウト:
h 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ページを指します。その元のページに移動して編集した場合、これらのリンク(ポインタ)のいずれかをたどると、新しい更新されたページが表示されます。
int *a = b
が2つのコピーを作成しないように*b
)。
ポインタが非常に多くの人々を混乱させるように見える理由は、彼らがほとんどコンピュータアーキテクチャの背景をほとんどまたはまったく持っていないためです。多くの人はコンピュータ(マシン)が実際にどのように実装されているかを理解していないようです-C / C ++での作業は異質なようです。
ドリルは、単純なバイトコードベースの仮想マシン(選択した任意の言語で、Pythonはこれに最適です)を、ポインター操作(ロード、ストア、直接/間接アドレス指定)に焦点を合わせて実装するように依頼することです。次に、その命令セット用の簡単なプログラムを作成するように依頼します。
単純な追加より少しだけ多くのことを必要とするものはすべてポインタを含み、彼らはそれを確実に得るでしょう。
ポインターが、C / C ++言語の多くの新しい、さらには古い大学レベルの学生にとって、なぜ混乱の主な要因なのでしょうか。
値のプレースホルダーの概念-変数-学校で教えられているもの-代数にマップします。コンピュータ内でメモリが物理的にどのように配置されているかを理解せずに描くことができる既存のパラレルはありません。低レベルのもの(C / C ++ /バイト通信レベル)を扱うまで、この種のことについて誰も考えません。
ポインターが変数、関数、およびレベルを超えてどのように機能するかを理解するのに役立つツールまたは思考プロセスはありますか?
ボックスに対処します。BASICをマイクロコンピュータにプログラムすることを学んでいたとき、ゲームが書かれたこれらのかわいい本があり、特定のアドレスに値を突き刺さなければならないこともあったことを覚えています。彼らは、0、1、2 ...とラベルが付けられた一連のボックスの写真を持っていて、これらのボックスには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#の開発者(私と同じ)で安全でないコードを使用していない場合でも、ポインターのしくみを理解することは価値があります。そうでない場合、オブジェクト参照のしくみを理解できません。次に、オブジェクト参照をメソッドに渡すとオブジェクトがコピーされるという一般的だが誤った概念があります。
Ted Jensenの「Cのポインターと配列のチュートリアル」は、ポインターについて学ぶための優れたリソースでした。これは10レッスンに分かれており、最初はポインターの説明(およびポインターの目的)から、関数ポインターで終わります。http://home.netcom.com/~tjensen/ptr/cpoint.htm
次に、Beejのネットワークプログラミングガイドでは、UnixソケットAPIについて説明します。ここから、本当に楽しいことができるようになります。http://beej.us/guide/bgnet/
ポインターの複雑さは、私たちが簡単に教えることができる範囲を超えています。生徒にお互いを指差させ、家の住所が書かれた紙を使用することは、どちらも優れた学習ツールです。彼らは基本的な概念を紹介するのに素晴らしい仕事をします。実際、基本的な概念を学ぶことは、ポインタをうまく使用するために不可欠です。ただし、量産コードでは、これらの単純なデモンストレーションでカプセル化できるよりもはるかに複雑なシナリオに入るのが一般的です。
私は、他の構造を指す他の構造を指す構造を持つシステムに関与してきました。それらの構造の一部には、(追加の構造へのポインターではなく)埋め込み構造も含まれていました。これは、ポインタが本当に混乱する場所です。複数レベルの間接参照があり、次のようなコードで終わる場合:
widget->wazzle.fizzle = fazzle.foozle->wazzle;
それは本当にすぐに混乱する可能性があります(より多くの行、そして潜在的にはより多くのレベルを想像してください)。ポインターの配列、およびノード間ポインター(ツリー、リンクリスト)をスローしても、さらに悪化します。このようなシステムに取り組み始めると、基本をよく理解している開発者でさえ、本当に優秀な開発者が迷子になるのを見てきました。
ポインターの複雑な構造は必ずしもコーディングが不十分であることを示しているわけではありません(可能ですが)。合成は優れたオブジェクト指向プログラミングの不可欠な要素であり、生のポインタを使用する言語では、必然的に多層の間接化につながります。さらに、システムは多くの場合、スタイルや手法が互いに一致しない構造を持つサードパーティのライブラリを使用する必要があります。そのような状況では、複雑さが自然に発生します(確かに、できる限りそれと戦う必要があります)。
大学がポインタを学ぶのを助けるために大学ができる最善のことは、ポインタの使用を必要とするプロジェクトと組み合わせて、良いデモンストレーションを使用することだと思います。困難なプロジェクトの1つは、1,000のデモよりもポインタの理解に多くのことを行います。デモンストレーションは浅い理解を得ることができますが、ポインタを深く理解するには、実際に使用する必要があります。
このリストに類推を加えて、コンピュータサイエンスの講師として(当時の)ポインタを説明するときに非常に役立つと思いました。まず、みましょう:
ステージを設定します。
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 |
これを考えるための一つの方法は、我々のクルマは、データのいくつかの並べ替えです(と言うことであるように、これらの車は、すべて同じタイプ(車)ですint
)が、それらは異なる値(持っているblue
、red
、green
、色かもしれませんenum
)
ポインタを入力してください
さて、この駐車場にあなたを連れて行き、青い車を見つけるように頼んだら、1本の指を伸ばして、スポット1の青い車を指し示します。これは、ポインタを取得してメモリアドレスに割り当てるようなものです(int *finger = parking_lot
)
あなたの指(ポインタ)は私の質問に対する答えではありません。探している相手にあなたの指は、私には何も伝えていないが、私はあなたがしている指がどこにあるかに見える場合を指す(ポインタを逆参照)、私は私が探していた車(データを)見つけることができます。
ポインターの再割り当て
代わりに赤い車を探すように頼むことができ、指を新しい車にリダイレクトすることができます。これで、ポインター(以前と同じ)に、同じタイプ(車)の新しいデータ(赤い車が見つかる駐車スポット)が表示されます。
ポインタが物理的に変更されていない、それはまだだ、あなたの指、それは私を見せていただけのデータが変更されました。(「駐車場」住所)
二重ポインター(またはポインターへのポインター)
これは複数のポインタでも機能します。赤い車を指しているポインターがどこにあるか尋ねることができます。もう一方の手を使用して、指で人差し指を指すことができます。(これはのようですint **finger_two = &finger
)
青い車がどこにあるかを知りたい場合は、1本目の指の方向を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)
)を確認して、ポインター()を参照して、そこに緑色の車が存在することを確認する必要があります(その場所のデータ) )
"without getting them bogged down in the overall concept"
高レベルの理解として。そしてあなたの要点:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
-あなたはこのレベルにさえポインタを理解していない多くの人々を驚かせることでしょう
ポインターの問題は概念ではありません。それは実行と関与する言語です。教師が難しいのはポインタの概念であり、専門用語ではない、または複雑な混乱したCおよびC ++が概念を作成していると教師が想定すると、さらに混乱が生じます。だから、この質問の受け入れられた答えのように、コンセプトの説明に多大な労力が費やされ、私はすでにすべてを理解しているので、それは私のような誰かにかなり無駄にされています。問題の間違った部分を説明しているだけです。
私がどこから来たのかを説明するために、私はポインターを完全によく理解している人であり、アセンブラー言語で十分にそれらを使用できます。アセンブラー言語では、これらはポインターと呼ばれていません。それらはアドレスと呼ばれます。Cでのプログラミングとポインターの使用については、多くの間違いを犯し、本当に混乱しています。私はまだこれを整理していません。例を挙げましょう。
APIが言うとき:
int doIt(char *buffer )
//*buffer is a pointer to the buffer
何が欲しいの?
それはしたいかもしれません:
バッファへのアドレスを表す数値
(それを与えるために、私は言うかdoIt(mybuffer)
、それともdoIt(*myBuffer)
?)
アドレスからバッファへのアドレスを表す数値
(それはdoIt(&mybuffer)
or doIt(mybuffer)
またはdoIt(*mybuffer)
?)
バッファへのアドレスへのアドレスへのアドレスを表す数
(多分それdoIt(&mybuffer)
は。またはそれdoIt(&&mybuffer)
ですか?またはdoIt(&&&mybuffer)
)
など、そして「xはyへのアドレスを保持します」および「この関数にはyへのアドレスが必要です。さらに、答えは、一体何が "mybuffer"から始まるのか、何をするつもりなのかに依存します。この言語は、実際に発生する入れ子のレベルをサポートしていません。新しいバッファを作成する関数に「ポインタ」を渡さなければならないときのように、バッファの新しい場所を指すようにポインタを変更します。本当にポインタが必要ですか、それともポインタへのポインタが必要ですか?それで、ポインタの内容を変更するためにどこへ行くかがわかります。ほとんどの場合、私は「」の意味を推測する必要があります
「ポインタ」が多すぎます。ポインタは値へのアドレスですか?または、値へのアドレスを保持する変数です。関数がポインターを必要とする場合、ポインター変数が保持するアドレスが必要ですか、それともポインター変数へのアドレスが必要ですか?よくわかりません。
double *(*(*fn)(int))(char)
、評価の結果はに*(*(*fn)(42))('x')
なりますdouble
。評価の層を取り除いて、中間タイプがどうあるべきかを理解することができます。
(*(*fn)(42))('x')
ですか?
x
評価*x
した場合、double が得られるもの(それをと呼ぶことにします)を取得します。
fn
であるあなたができるかという点で、より行うとfn
ポインタを理解する上での主な障壁は悪い教師だと思います。
ほとんどすべての人がポインタについてうそをつきます。それらはメモリアドレスにすぎないこと、または任意の場所をポイントできるようにすることです。
そしてもちろん、それらは理解するのが難しく、危険で半魔法です。
どれも真実ではありません。ポインタは、C ++言語がそれらについて述べなければならないことに固執し、「通常は」実際に機能することが判明しているが、言語によって保証されていない属性を付けない限り、実際にはかなり単純な概念です。 、およびそのため、ポインタの実際の概念の一部ではありません。
数か月前にこのブログ投稿でこれについての説明を書き込もうとしました-うまくいけば誰かの助けになるでしょう。
(注意:だれかが私に夢中になる前に、はい、C ++標準はポインタがメモリアドレスを表すと言っています。しかし、「ポインタはメモリアドレスであり、メモリアドレスにすぎず、メモリと交換可能に使用または考えられるかもしれません。アドレス」。区別は重要です)
理解するのが難しいのは、難しい概念ではなく、構文に一貫性がないためです。
int *mypointer;
最初に、変数作成の左端の部分が変数のタイプを定義することを学びます。ポインター宣言は、CおよびC ++ではこのように機能しません。代わりに、変数は型が左を指していると言います。この場合、*
mypointer は intを指しています。
ポインターをC#で(安全でない状態で)使用するまでは、ポインターを完全には把握していませんでした。ポインターはまったく同じように機能しますが、論理的で一貫性のある構文です。ポインタはそれ自体が型です。ここで、mypointer は intへのポインターです。
int* mypointer;
関数ポインタから始めないでください...
int *p;
簡単な意味があります:*p
は整数です。int *p, **pp
意味:*p
および**pp
整数です。
*p
そして、**pp
しているではない、あなたが初期化されませんので、整数p
またはpp
または*pp
何にもポイントに。特に一部のエッジケースや複雑なケースではそうする必要があるため、一部の人々がこの文法にこだわるのを好む理由を理解しています(ただし、私が知っているすべてのケースでそれを簡単に回避できます)...しかし、私はそれらのケースが右揃えを教えることが初心者に誤解を招くという事実よりも重要だとは思いません。醜いことは言うまでもありません!:)
私は家の住所の類推が好きですが、私はいつも住所が郵便受け自体にあることを考えていました。このようにして、ポインターの逆参照(メールボックスを開く)の概念を視覚化できます。
たとえば、リンクされたリストをたどる:1)紙のアドレスから始めます2)紙のアドレスに移動します3)メールボックスを開いて、次のアドレスの新しい紙を見つけます
線形リンクリストでは、最後のメールボックスには何もありません(リストの最後)。循環リンクリストでは、最後のメールボックスには最初のメールボックスのアドレスが含まれます。
ステップ3は逆参照が発生する場所であり、アドレスが無効な場合にクラッシュまたは失敗することに注意してください。無効なアドレスのメールボックスに近づく可能性があると仮定して、そこにブラックホールまたは世界を裏返す何かがあると想像してください。
人々がそれに悩む主な理由は、それが一般的に興味深く魅力的な方法で教えられていないからだと思います。講師が群集から10人のボランティアを獲得し、それぞれに1メートルの定規を与え、特定の構成で立ってもらい、定規を使用してお互いを指すようにしてほしいです。次に、人々を動かして(そして彼らが定規を指す場所に)ポインター算術を示します。これは、メカニズムに夢中になることなく、コンセプトを示すシンプルで効果的な(そして何よりも覚えやすい)方法です。
CとC ++に到達すると、一部の人にとっては難しくなるようです。彼らが最終的に適切に理解していない理論を実行に移しているためか、ポインターの操作がこれらの言語では本質的に難しいためか、私にはわかりません。私自身のトランジションはよく覚えていませんが、Pascalでポインターを知っていて、Cに移動して完全に失われました。
実際には構文の問題かもしれません。ポインターのC / C ++構文は一貫性がなく、必要以上に複雑に見えます。
皮肉なことに、実際にポインタを理解するのに役立つのは、c ++ 標準テンプレートライブラリのイテレータの概念に出会ったことです。皮肉なのは、反復子がポインタの一般化として考えられたとしか想定できないからです。
時々、あなたは木を無視することを学ぶまで森を見ることはできません。
(*p)
されているだろう(p->)
ので、私たちは持っていると思いますp->->x
代わりに曖昧で*p->x
a->b
単にを意味し(*a).b
ます。
* p->x
手段* ((*a).b)
に対し、*p -> x
手段(*(*p)) -> x
。前置演算子と後置演算子を混在させると、あいまいな解析が発生します。
1+2 * 3
が9であるべきだと言っているようなものです
混乱は、「ポインター」の概念で混合された複数の抽象化レイヤーに起因します。プログラマーは、Java / Pythonの通常の参照に混乱することはありませんが、ポインターは、基礎となるメモリーアーキテクチャーの特性を公開するという点で異なります。
抽象化の層を明確に分離することは良い原則であり、ポインタはそれを行いません。
foo[i]
は、特定の場所に行き、特定の距離だけ前進し、そこにあるものを見るということです。物事を複雑にするのは、コンパイラーの利益のために標準によって追加された、はるかに複雑な追加の抽象化レイヤーですが、プログラマーのニーズとコンパイラーのニーズに似合わない方法で物事をモデル化します。
私がそれを説明するのが好きだった方法は、配列とインデックスに関してでした-人々はポインタに慣れていないかもしれませんが、彼らは一般にインデックスが何であるかを知っています。
したがって、RAMが配列であると想像してください(RAMは10バイトしかありません)。
unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };
次に、変数へのポインタは、実際にはRAM内のその変数(の最初のバイト)の単なるインデックスです。
したがって、ポインタ/インデックスunsigned char index = 2
がある場合、値は明らかに3番目の要素、つまり数値4になります。ポインタへのポインタは、その番号を取得し、それ自体をインデックスとして使用する場所ですRAM[RAM[index]]
。
私は紙のリストに配列を描き、それを使用して、同じメモリを指す多くのポインタ、ポインタ演算、ポインタからポインタなどを表示します。
私書箱番号。
それはあなたが何か他のものにアクセスすることを可能にする情報の一部です。
(私書箱の数値を計算すると、文字が間違った箱に入ってしまうため、問題が発生する可能性があります。また、誰かが転送アドレスなしで別の州に移動した場合は、ぶら下がりポインタがあります。一方、郵便局がメールを転送する場合、ポインターへのポインターがあります。)
イテレータを介してそれを把握するための悪い方法ではありません。
多くの元C ++開発者(イテレータが言語をダンプする前に最新のポインタであることを決して理解していなかった人)はC#にジャンプし、それでも適切なイテレータがあると信じています。
うーん、問題は、反復子のすべてが、実行時プラットフォーム(Java / CLR)が達成しようとしていること、つまり、新しくてシンプルな、誰でもが開発者であるという使用法に完全に対立していることです。これは良いことですが、紫色の本で一度言っただけでなく、Cの前でも前でも言っていました。
間接。
非常に強力なコンセプトですが、すべての方法を実行する場合はそうではありません。イテレータは、別の例として、アルゴリズムの抽象化に役立つため便利です。そして、コンパイル時は、非常に単純なアルゴリズムの場所です。あなたはコードとデータを知っていますか、または他の言語でC#:
IEnumerable + LINQ + Massive Framework =参照タイプのインスタンスのヒープを介してアプリをドラッグすると、お粗末なため、300MBのランタイムペナルティインダイレクション。
「ルポインターは安い。」
上記のいくつかの回答は、「ポインタは実際には難しい」ではないと主張しましたが、「ポインタが難しい」場所に直接対処することはしていません。から来た。数年前、私は最初の1年のCS学生を指導しました(私はそれをはっきりと吸ったので、1年間だけ)、理解。ポインター考え方は難しくないでした。難しいのは、いつ、いつポインタが必要になるのかを理解することです。
幅広いソフトウェアエンジニアリングの問題を説明することから、その質問-いつ、いつポインターを使用するのか-を離脱することはできないと思います。なぜすべての変数がグローバル変数であってはならないのか、そして同様のコードを関数に分解しなければならないのはなぜですか(つまり、これを取得し、ポインターを使用して呼び出しサイトへの動作を特殊化します)。
ポインターについて何が混乱しているのかわかりません。それらはメモリ内の場所を指します。つまり、メモリアドレスを格納します。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は、私よりも首尾一貫してそれについて話します。:-)
すべてのC / C ++初心者は同じ問題を抱えており、その問題は「ポインターを学ぶのが難しい」からではなく、「だれがどのように説明されているのか」という理由で発生します。一部の学習者は、口頭で視覚的に収集し、それを説明する最良の方法は、「トレーニング」の例を使用することです(口頭および視覚的な例のスーツ)。
ここで、「機関車」は何も保持できないポインタであり、「ワゴン」は「機関車」がプル(またはポイント)しようとするものです。その後、「ワゴン」自体を分類し、動物、植物、または人(またはそれらの混合)を保持できます。