ここに投稿された質問の数から、ポインタとポインタ演算について頭を悩ますとき、人々がいくつかのかなりの根本的な問題を抱えていることは明らかです。
その理由を知りたいです。彼らは私に大きな問題を実際に引き起こしたことはありません(私は最初に新石器時代にそれらについて知りましたが)。これらの質問に対するより良い答えを書くために、人々が難しいと思うことを知りたいのです。
それで、ポインタに苦労している場合、または最近「突然」気がついた場合、問題の原因となったポインタの側面は何でしたか?
ここに投稿された質問の数から、ポインタとポインタ演算について頭を悩ますとき、人々がいくつかのかなりの根本的な問題を抱えていることは明らかです。
その理由を知りたいです。彼らは私に大きな問題を実際に引き起こしたことはありません(私は最初に新石器時代にそれらについて知りましたが)。これらの質問に対するより良い答えを書くために、人々が難しいと思うことを知りたいのです。
それで、ポインタに苦労している場合、または最近「突然」気がついた場合、問題の原因となったポインタの側面は何でしたか?
回答:
人々は答えを深めすぎているのではないかと思います。スケジューリング、実際のCPU操作、またはアセンブリレベルのメモリ管理について理解する必要はありません。
私が教えていたとき、学生の理解の次の穴が最も一般的な問題の原因であることがわかりました。
私の学生のほとんどは、メモリのチャンク、一般的には現在のスコープのスタックのローカル変数セクションの簡略図を理解することができました。一般的には、さまざまな場所に架空の住所を明示的に提供することが役立ちました。
要約すると、ポインタを理解したい場合、変数を理解する必要があり、それらが現代のアーキテクチャで実際に何であるかを理解する必要があると私は言っています。
私が最初にそれらを使用し始めたとき、私が抱えていた最大の問題は構文でした。
int* ip;
int * ip;
int *ip;
すべて同じです。
だが:
int* ip1, ip2; //second one isn't a pointer!
int *ip1, *ip2;
どうして?宣言の「ポインタ」の部分は、型ではなく変数に属しているためです。
そして、事物の逆参照は非常に類似した表記を使用します:
*ip = 4; //sets the value of the thing pointed to by ip to '4'
x = ip; //hey, that's not '4'!
x = *ip; //ahh... there's that '4'
実際にポインターを取得する必要がある場合を除いて、アンパサンドを使用します!
int *ip = &x;
一貫性のための万歳!
次に、明らかにジャークであり、それらがいかに賢いかを証明するために、多くのライブラリ開発者はポインターからポインターへのポインターを使用しています。
void foo(****ipppArr);
これを呼び出すには、intのポインターへのポインターへのポインターの配列のアドレスが必要です。
foo(&(***ipppArr));
6か月後、このコードを保守する必要がある場合は、ゼロから書き直すよりも、これらすべての意味を理解することに多くの時間を費やします。(ええ、おそらくその構文が間違っているでしょう-Cで何かをしたので久しぶりです。ちょっと寂しいですが、それから私は少しマゾヒストです)
ポインタを正しく理解するには、基盤となるマシンのアーキテクチャに関する知識が必要です。
車の運転方法を知っているほとんどの人がエンジンについて何も知らないのと同じように、今日の多くのプログラマーは自分の機械がどのように機能するかを知りません。
ポインターを扱うとき、混乱する人々は2つのキャンプの1つに広くいます。私は両方に行ったことがありますか?
array[]
群衆これは、まっすぐにポインタ表記から配列表記に変換する方法がわからない(またはそれらが関連していることさえ知らない)群集です。配列の要素にアクセスするには、次の4つの方法があります。
int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;
array element pointer
notation number vals notation
vals[0] 0 10 *(ptr + 0)
ptr[0] *(vals + 0)
vals[1] 1 20 *(ptr + 1)
ptr[1] *(vals + 1)
vals[2] 2 30 *(ptr + 2)
ptr[2] *(vals + 2)
vals[3] 3 40 *(ptr + 3)
ptr[3] *(vals + 3)
vals[4] 4 50 *(ptr + 4)
ptr[4] *(vals + 4)
ここでの考え方は、ポインタを介した配列へのアクセスは非常に単純で簡単なが、非常に複雑で巧妙なことはこの方法で行うことができます。経験の浅い初心者は言うまでもなく、経験豊富なC / C ++プログラマーを困惑させる人もいます。
reference to a pointer
とpointer to a pointer
群衆このは違いを説明する素晴らしい記事であり、私が引用し、そこからいくつかのコードを盗みます:)
小さな例として、次のようなことに遭遇した場合に、作者が何をしたいのかを正確に確認することは非常に困難です。
//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??
int main()
{
int nvar=2;
int* pvar=&nvar;
func(pvar);
....
return 0;
}
または、それほどではありませんが、次のようなものです。
//function prototype
void func(int** ppInt);
int main()
{
int nvar=2;
int* pvar=&nvar;
func(&pvar);
....
return 0;
}
結局のところ、この意味不明なことすべてで本当に何を解決できるでしょうか。何もない。
これで、ptr-to-ptrおよびref-to-ptrの構文を見てきました。どちらか一方の利点はありますか?私は恐れています、いいえ。どちらか一方の使用法は、一部のプログラマーにとっては単なる個人的な好みです。ref-to-ptrを使用する人は構文が「よりクリーン」であると言いますが、ptr-to-ptrを使う人、ptr-to-ptr構文を使用する人はあなたが読んでいることを読む人にそれをより明確にします。
この複雑さと、参照との見かけ上の(太字のように見える)互換性は、ポインタのもう1つの警告であり、新規参入者のエラーであり、ポインタの理解を困難にします。これは、参照へのポインタがにあなたを取る理由は混乱のためのCおよびC ++で違法であることを、完成のために、理解することも重要ですlvalue
-rvalue
セマンティクスを。
以前の答えが述べたように、多くの場合、これらのホットショットプログラマーは、彼らが使用することで賢いと考えている******awesome_var->lol_im_so_clever()
だけで、おそらくそのような残虐行為を書くことで罪を犯しますが、それは単に良いコードではなく、確実に保守可能ではありません。
さて、この答えは私が期待していたよりも長くなりました...
私は参考資料の質と教えをしている人々を個人的に責めます。Cのほとんどの概念(ただし、特にポインター)は、ひどく教えられているだけです。私は自分のCブック(「世界が必要とする最後のもの」はCプログラミング言語に関する別のブックです)を書くと脅し続けていますが、そのための時間も忍耐力もありません。だから私はここでぶらぶらして、人々から標準からのランダムな引用を投げます。
Cが最初に設計されたときに、日常の作業でそれを回避する方法がなかったという理由だけでマシンアーキテクチャをかなり詳細なレベルまで理解していると想定されていたという事実もあります(メモリが非常にタイトでプロセッサが非常に低速でした)作成した内容がパフォーマンスにどのように影響するかを理解する必要がありました)。
Joel Spolskyのサイト(JavaSchoolsの危険)にポインターが難しいという考えを支持する素晴らしい記事があります。ます。
[免責事項- それ自体はJava嫌いではありません。]
「下」にある知識に基づいていない場合、ほとんどのことは理解するのが難しくなります。私がCSを教えたとき、学生が非常に単純な「マシン」、つまり10進レジスタと10進アドレスで構成されるメモリを持つ10進オペコードを備えたシミュレートされた10進コンピュータのプログラミングを始めたとき、はるかに簡単になりました。彼らは非常に短いプログラムを入れて、例えば、合計を得るために一連の数値を追加します。それから彼らはそれを一歩踏み込んで何が起こっているのかを見守っていました。彼らは、「Enter」キーを押したまま、「速く」実行されるのを見ることができます。
SOのほとんどの人は、なぜ基本的なものにすると便利なのか疑問に思います。プログラミングの仕方がわからなかったのを忘れます。そのようなおもちゃのコンピューターで遊ぶと、計算がステップバイステップのプロセスであるという考え、少数の基本的なプリミティブを使用してプログラムを構築するという考え、メモリの概念など、プログラミングなしではできない概念が導入されます変数が格納される場所としての変数。変数のアドレスまたは名前は、変数に含まれる番号とは異なります。プログラムに入る時間とプログラムが「実行される」時間には違いがあります。私は、非常に単純なプログラム、ループとサブルーチン、配列、シーケンシャルI / O、ポインタ、データ構造など、一連の「スピードバンプ」を横断するようにプログラムすることを学ぶのが好きです。
最後に、Cに到達したとき、K&Rは非常に良い説明をしてくれましたが、ポインターは混乱しています。私がCでそれらを学習した方法は、右から左に読む方法を知ることでした。int *p
頭の中で見たときのように、「をp
指す」と言いint
ます。Cは、アセンブリ言語からの1つのステップとして発明されたもので、それが私が気に入っているところです。Cはその「基礎」に近いものです。他のものと同様に、その基礎がない場合、ポインターは理解するのが難しくなります。
K&Rで説明を読むまで、ポインタはありませんでした。その時点まで、ポインタは意味がありませんでした。「ポインタを覚えないでください、混乱し、頭が痛くなり、動脈瘤ができます」と人々が言ったたくさんのことを読んだので、私は長い間それを避け、この不必要な難しい概念の空気を作りました。
そうでなければ、主に私が思っていたのは、なぜ地球上で値を取得するためにフープを通過しなければならない変数が必要なのか、それに変数を割り当てたい場合は、値を取得するために奇妙なことをしなければならなかった理由です。それらに。変数の要点は値を格納することだと私は思ったので、なぜ誰かがそれを複雑にしたかったのかは私を超えていました。 「そのため、ポインターを使用する場合は、*
演算子を使用してその値を取得する必要がありますか???これは、どのような間抜けな変数ですか?」、と思った。意味のない、しゃれは意図されていません。
複雑だったのは、ポインタがアドレスだとわからなかったから何かへです。それがアドレスであり、何かへのアドレスを含むものであり、そのアドレスを操作して有用なことを実行できることを説明すると、混乱が解消されると思います。
PCのポートにアクセス/変更するためにポインターを使用する必要があるクラス、ポインター演算を使用してさまざまなメモリの場所をアドレス指定し、引数を変更するより複雑なCコードを見ると、ポインターは無意味であるという考えを思いとどまらせました。
ここで、一時停止したポインタ/配列の例を示します。次の2つの配列があるとします。
uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];
そして、あなたの目標は、memcpy()を使用してソースの宛先からuint8_tの内容をコピーすることです。次のうちどれがその目標を達成するかを推測します。
memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));
答え(ネタバレ注意!)はそれらすべてです。「destination」、「&destination」、および「&destination [0]」はすべて同じ値です。「&destination」は別のタイプです他の2つですが、それでも同じ値です。「ソース」の順列についても同じことが言えます。
余談ですが、私は個人的には最初のバージョンを好みます。
sizeof(source)
場合ので、source
ポインタである、sizeof
それはあなたが望むものではありません。私はときどき(常にではありませんが)sizeof(source[0]) * number_of_elements_of_source
そのバグから遠く離れるように書くだけです。
まず、CとC ++は私が最初に学んだプログラミング言語だと言っておくべきです。私はCから始めて、学校でC ++を何度もやった後、Cに戻って流暢になった。
Cを学ぶときにポインタについて私を混乱させた最初のことは簡単でした:
char ch;
char str[100];
scanf("%c %s", &ch, str);
この混乱は、ポインターが適切に導入される前に、OUT引数に変数への参照を使用することに導入されたことが主な原因です。C for Dummiesで最初のいくつかの例を書くのをスキップしたのを覚えています。なぜなら、それらは単純すぎて、最初に書いたプログラムが機能しないからです(おそらくそのため)。
これについて混乱していたのは、&ch
実際に何を意味していたのか、またその理由でしたstr
それを必要としなかったのです。
それに慣れた後、ダイナミックアロケーションについて混乱したことを次に覚えます。ある時点で、データへのポインタを持つことは、あるタイプの動的割り当てなしではあまり有用ではないことに気づいたので、次のように書きました。
char * x = NULL;
if (y) {
char z[100];
x = z;
}
動的にスペースを割り当てようとします。うまくいきませんでした。それが機能するかどうかはわかりませんでしたが、他にどのように機能するかわかりませんでした。
私は後でについて学んだmalloc
とnew
、彼らは本当に私には不思議なメモリ・ジェネレータのように思えました。それらがどのように機能するかについては何も知りませんでした。
しばらくして、私は再帰を再び教えられていました(以前は自分でそれを学びましたが、今はクラスにいました)、私はそれが内部でどのように機能するかを尋ねました-個別の変数はどこに保存されましたか?私の教授は「山積み」と言って、多くのことが私に明らかになりました。以前にその用語を聞いたことがあり、ソフトウェアスタックを実装したことがありました。他の人がずっと前に「スタック」を参照していると聞いていましたが、それを忘れていました。
この頃、Cで多次元配列を使用すると、非常に混乱する可能性があることにも気付きました。それらがどのように機能するかは知っていましたが、絡み合うのが非常に簡単だったので、できる限りそれらを使用して回避することにしました。ここでの問題は主に構文(特に、関数への受け渡しまたは関数からの戻り)であったと思います。
翌年または2年間、学校向けにC ++を書いていたので、データ構造のポインターを使用して多くの経験を得ました。ここで私は新しい一連の問題を抱えていました-ポインターを混同しています。私は複数レベルのポインタ(などnode ***ptr;
)でつまずきます。私はポインタを間違った回数逆参照し、最終的に何回かを見つけることに頼ります*
試行錯誤によって必要なます。
ある時点で、プログラムのヒープがどのように機能するかを知りました(ある程度のことですが、それでも夜はもう上手くいかなくなりました)。malloc
特定のシステムで返されるポインタの前の数バイトを見ると、実際に割り当てられたデータの量を確認できることを読んだことを覚えています。のコードがmalloc
OSにさらに多くのメモリを要求する可能性があり、このメモリは実行可能ファイルの一部ではないことに気付きました。どのようにmalloc
機能するかについて、きちんとした実用的なアイデアを持っていることは、本当に役に立ちます。
この直後、私はアセンブリクラスを受講しましたが、ほとんどのプログラマーが考えるほどポインタについてはあまり教えられませんでした。私のコードがどのアセンブリに変換されるかについてもっと考えるようになりました。私はいつも効率的なコードを書こうとしましたが、今はその方法をよりよく理解することができました。
また、lispを書かなければならないクラスもいくつか受けました。lispを書くとき、私はCのときほど効率に関心がありませんでした。コンパイルした場合、このコードがどのように変換されるのかほとんどわかりませんでしたが、作成されたローカルの名前付きシンボル(変数)をたくさん使用しているように見えました物事はずっと簡単です。ある時点で、少しlispでいくつかのAVLツリーローテーションコードを記述しましたが、ポインターの問題のため、C ++での記述は非常に困難でした。過剰なローカル変数だと思っていた私の嫌悪感が、C ++でそれや他のいくつかのプログラムを書く私の能力を妨げていることに気づきました。
コンパイラクラスも受講しました。このクラスでは、高度な資料に移動して静的 単一 代入(SSA)とデッド変数について学びましたが、適切なコンパイラーが変数を処理する適切な仕事をすることを教えてくれたことを除いて、それほど重要ではありません使用されなくなりました。正しいタイプと適切な名前を持つ変数(ポインターを含む)を増やすと、頭の中で物事をまっすぐに進めるのに役立つことはすでにわかっていましたが、今では、効率の理由でそれらを避けることは、マイクロ最適化をあまり意識していない教授が言うよりも愚かであることも知っていました私。
ですから、私にとって、プログラムのメモリレイアウトについて少し知っていることは大きな助けとなりました。記号とハードウェアの両方で私のコードが何を意味するかを考えることは、私を助けてくれます。正しい型のローカルポインタを使用すると、非常に役立ちます。私はよく次のようなコードを書きます。
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
そのため、ポインタ型を台無しにした場合、コンパイラエラーによって問題が非常に明確になります。私がした場合:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
そこにポインター型が間違っていると、コンパイラー・エラーを理解するのがずっと難しくなります。欲求不満の試行錯誤の変更に頼りたくて、おそらく事態をさらに悪化させたくなるでしょう。
振り返ると、最終的にポインターを理解するのに本当に役立つ4つのことがありました。これまでは使用できましたが、完全には理解できませんでした。つまり、フォームに従うと、希望する結果が得られることはわかっていましたが、フォームの「理由」を完全には理解していませんでした。これはあなたが尋ねたとおりではないことを私は理解していますが、それは有用な帰結です。
整数へのポインターを受け取り、整数を変更するルーチンを作成します。これにより、ポインターの動作に関するメンタルモデルを構築するために必要なフォームが得られました。
1次元の動的メモリ割り当て。1次元のメモリ割り当てを理解することで、ポインタの概念を理解することができました。
2次元の動的メモリ割り当て。2次元メモリ割り当てを理解することでその概念が強化されましたが、ポインタ自体にもストレージが必要であり、考慮に入れる必要があることもわかりました。
スタック変数、グローバル変数、ヒープメモリの違い。これらの違いを理解することで、ポインタが指す/参照するメモリのタイプを知ることができました。
これらの各項目では、下位レベルで何が起こっているのかを想像する必要がありました。時間と労力がかかりましたが、それだけの価値がありました。ポインタを理解するためには、それらがどのように機能し、どのように実装されるかについて、そのメンタルモデルを構築する必要があると私は確信しています。
元の質問に戻ります。前のリストに基づいて、元々把握が難しかった項目がいくつかありました。
Cの一部のテレフォニープログラムで「ポインターモーメント」を使用していました。クラシックCのみを理解するプロトコルアナライザーを使用してAXE10交換エミュレーターを作成する必要がありました。すべてはポインターを知ることにかかっていました。私はそれらなしで自分のコードを書いてみました(ちょっと、私は "プレポインタ"で少し緩めました)と完全に失敗しました。
私にとって、それらを理解するための鍵は&(アドレス)演算子でした。&i
「iのアドレス」を意味することを理解したら、「iが*i
指すアドレスの内容」を意味することを少し後で理解しました。コードを書いたり読んだりするときはいつでも、「&」と「*」の意味を常に繰り返し、最終的には直感的に使用するようになりました。
残念なことに、私はVB、次にJavaに追い込まれたため、ポインターの知識は以前ほどシャープではありませんが、「ポストポインター」であることがうれしいです。ただし、* * p を理解する必要があるライブラリを使用するように要求しないでください。
&i
アドレスであり、*i
内容で、何がありますかi
?
少なくとも私にとって、ポインタの主な難点は、Cから始めていなかったことです。Javaから始めました。ポインターの概念全体は、私がCを知っていると期待されていた大学での2、3のクラスまでは本当に異質でした。それで、Cの基本と、ポインターの基本的な使い方を学びました。それでも、Cコードを読んでいるたびに、ポインタ構文を調べる必要があります。
非常に限られた経験(1年の実世界+大学での4年)では、教室以外の場所で実際に使用する必要がなかったので、ポインターが混乱を招きました。そして、CやC ++の代わりにJAVAを使ってCSを始めた生徒たちに共感できます。あなたが言ったように、あなたは「新石器時代」の時代に指針を学び、それ以来おそらくそれを使ってきました。新しい人々にとって、これらの言語はすべて抽象化されているため、メモリの割り当てとポインタ演算の概念は非常に異質です。
PSスポルスキーのエッセイを読んだ後、彼の「JavaSchools」の説明は、私がコーネル大学('05 -'09)で経験したものとはまったく異なりました。私は構造と関数型プログラミング(sml)、オペレーティングシステム(C)、アルゴリズム(ペンと紙)、およびJavaで教えられなかった他のクラスの全体を取り上げました。ただし、すべてのイントロクラスと選択科目はすべてJavaで行われました。これは、ポインター付きのハッシュテーブルを実装するよりも高いレベルのことを実行しようとするときに、ホイールを再発明しないことの価値があるためです。
void foo(Clazz obj) { obj = new Clazz(); }
はvoid bar(Clazz obj) { obj.quux = new Quux(); }
、引数を変更しているときに何も実行しない理由を理解することを意味します...
構文を大幅に変更することなく、コードに次元を追加します。これについて考える:
int a;
a = 5
変更する必要があるのは1つだけですa
。あなたは書くことができa = 6
、結果はほとんどの人にとって明白です。しかし今検討してください:
int *a;
a = &some_int;
a
さまざまなタイミングで関連する2つの事項があります。の実際の値a
、ポインタ、およびポインタの「背後」の値です。変更できますa
:
a = &some_other_int;
...そしてsome_int
同じ値でまだどこかにあります。しかし、それが指すものを変更することもできます:
*a = 6;
a = 6
局所的な副作用のみがあると*a = 6
、他の場所の他の多くの要素に影響を与える可能性があるとの間には、概念的なギャップがあります。ここでの私のポイントはありません間接の概念が本質的に難しいですが、あなたが行うことができますので、ということの両方ですぐに、地元の事をa
または間接の事*a
...それは人々が混乱するものであるかもしれません。
私は2年間ほどc ++でプログラミングし、その後Java(5年間)に変換しました。しかし、最近ネイティブアイテムを使用しなければならなくなったとき、私は(驚いて)ポインターについて何も忘れていないこと、そして使いやすいことさえわかった。これは、7年前に最初にコンセプトを理解しようとしたときに経験したものとは対照的です。それで、理解と好みはプログラミングの成熟度の問題だと思いますか?:)
または
ポインターは自転車に乗るようなものです。ポインターを操作する方法を理解したら、忘れることはありません。
全体として、把握するのが難しいかどうかにかかわらず、ポインターのアイデア全体は非常に教育的であり、ポインターを使用する言語でプログラミングするかどうかに関係なく、すべてのプログラマーが理解する必要があると思います。
ポインタは、オブジェクトへのハンドルとオブジェクト自体の違いを処理する方法です。(わかりました、必ずしもオブジェクトではありませんが、あなたは私が何を意味するか、そして私の心がどこにあるかを知っています)
ある時点で、おそらくこの2つの違いに対処する必要があります。現代の高級言語では、これは値によるコピーと参照によるコピーの違いになります。いずれにせよ、プログラマにとって理解しにくい概念です。
ただし、指摘したように、Cでこの問題を処理するための構文は、醜く、一貫性がなく、混乱します。最終的には、本当にそれを理解しようとするならば、ポインタは意味をなすでしょう。しかし、ポインタへのポインタを扱うようになり、悪心が続くと、私だけでなく他の人々にとっても非常に混乱します。
ポインタについて覚えておかなければならないもう1つの重要なことは、ポインタが危険であることです。Cはマスタープログラマーの言語です。それはあなたが何をしているのかをあなたが知っていると仮定し、それによってあなたに本当に物事を台無しにする力を与えます。一部のタイプのプログラムは依然としてCで作成する必要がありますが、ほとんどのプログラムはそうではなく、オブジェクトとそのハンドルの違いをより適切に抽象化する言語がある場合は、それを使用することをお勧めします。
実際、多くの最新のC ++アプリケーションでは、必要なポインター演算がカプセル化および抽象化されることがよくあります。開発者がいたるところでポインタ演算を行うことは望ましくありません。最下位レベルでポインター演算を行う、集中化された十分にテストされたAPIが必要です。このコードに変更を加えるには、細心の注意と広範なテストを行う必要があります。
Cポインターが難しい理由の1つは、実際には同等ではないいくつかの概念が混同されていることです。しかし、それらはすべてポインタを使用して実装されているため、概念を解くのに苦労する可能性があります。
Cでは、ポインターは他のものと同様に使用されます。
Cでは、次のように整数のリンクリストを定義します。
struct node {
int value;
struct node* next;
}
これはCで再帰的なデータ構造を定義する唯一の方法であり、概念がメモリアドレスなどの低レベルの詳細とはまったく関係がない場合にのみ、ポインターはそこにあります。ポインタの使用を必要としないHaskellの次の同等のものを検討してください。
data List = List Int List | Null
非常に簡単-リストは空であるか、値とリストの残りの部分から形成されています。
foo
Cの文字列のすべての文字に関数を適用する方法は次のとおりです。
char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }
ポインターをイテレーターとして使用しているにもかかわらず、この例は前の例とほとんど共通点がありません。増分できるイテレータを作成することは、再帰的なデータ構造を定義することとは異なる概念です。どちらの概念も、特にメモリアドレスの概念に関連付けられていません。
glibにある実際の関数シグネチャは次のとおりです。
typedef struct g_list GList;
void g_list_foreach (GList *list,
void (*func)(void *data, void *user_data),
void* user_data);
うわあ!それはかなりの一口ですvoid*
。そして、すべてのことを含むことができるリストを反復する関数を宣言し、各メンバーに関数を適用するだけです。map
Haskellで宣言されている方法と比較してください。
map::(a->b)->[a]->[b]
それははるかに簡単です:map
変換する関数取る関数であるa
にしb
て、リストにそれを適用するa
「のリスト得るためのb
s」を。C関数のg_list_foreach
場合と同様に、map
それはそれが適用される型についてそれ自身の定義で何かを知る必要はありません。
総括する:
最初に再帰的なデータ構造、イテレータ、ポリモーフィズムなどについて個別の概念として学び、次にこれらすべてをマッシュアップするのではなく、Cでこれらのアイデアを実装するためにポインタを使用する方法を学んだ場合、Cポインタはそれほど混乱しにくいと思います概念をまとめて「ポインタ」の単一の主題にします。
c != NULL
"Hello world"の例の誤用 ...を意味し*c != '\0'
ます。
おそらく、マシンレベルからの確かな基盤が必要だと思います。いくつかのマシンコード、アセンブリ、およびアイテムとデータ構造をRAMで表現する方法を紹介します。少し時間がかかります。宿題や問題解決の練習、そして思考も必要です。
しかし、人が最初に高水準言語を知っている場合(これは問題ありません-大工が斧を使用します。アトムを分割する必要がある人は別のものを使用します。大工である人が必要であり、アトムを研究する人がいます)高水準言語を知っているこの人には、2分間のポインターの紹介が与えられ、その後、ポインターの計算、ポインターへのポインター、可変サイズの文字列へのポインターの配列、および文字の配列の配列などを理解するのは難しいでしょう。低レベルの強固な基盤は大きな助けになります。
私がいつも抱えていた問題(主に独学)は、ポインターを使用する「タイミング」です。ポインタを構築するための構文に頭を悩ませることはできますが、どのような状況でポインタを使用する必要があるかを知る必要があります。
この考え方を持つのは私だけですか?;-)
最近、ポインタクリックの瞬間があり、混乱していることに驚いていました。それはみんながそれについてあまりにも多く話したことであり、私はいくつかのダークマジックが起こっていると思いました。
私がそれを得た方法はこれでした。定義されたすべての変数に、コンパイル時に(スタック上で)メモリ空間が与えられると想像してください。オーディオや画像などの大きなデータファイルを処理できるプログラムが必要な場合は、これらの潜在的な構造に対して固定量のメモリは必要ありません。したがって、このデータを(ヒープ上に)保持するために、実行時まで一定量のメモリを割り当てるまで待機します。
メモリにデータを保存したら、そのデータに対して操作を実行するたびに、そのデータをメモリバス全体にコピーする必要はありません。画像データにフィルターを適用したいとします。画像に割り当てたデータの先頭から始まるポインターがあり、関数がそのデータを横切って実行され、データが所定の位置に変更されます。私たちが何をしているかわからない場合は、操作を実行するときに、データの複製を作成することになります。
少なくとも今のところはそうです!
ここでC ++初心者として話す:
ポインターシステムは、概念のためではなく、Javaに関連するC ++構文のために、消化に時間がかかりました。私が混乱させたいくつかのことは:
(1)変数宣言:
A a(1);
対
A a = A(1);
対
A* a = new A(1);
そしてどうやら
A a();
関数宣言であり、変数宣言ではありません。他の言語では、基本的に変数を宣言する方法は1つだけです。
(2)アンパサンドはいくつかの異なる方法で使用されます。もしそれが
int* i = &a;
&aはメモリアドレスです。
OTOH、もしそうなら
void f(int &a) {}
その場合、&aは参照渡しパラメーターです。
これはささいなことのように思えるかもしれませんが、新しいユーザーを混乱させる可能性があります。
(3)配列ポインタ関係
理解するのに少しイライラするのは、ポインタが
int* i
intへのポインタにすることができます
int *i = &n; //
または
intの配列にすることができます
int* i = new int[5];
さらに、混乱を招くために、ポインターと配列はすべての場合に交換可能ではなく、ポインターを配列パラメーターとして渡すことはできません。
これは、C / C ++とそのポインタ(IMO)に関する基本的なフラストレーションのいくつかを要約したもので、C / C ++にはこれらの言語固有の癖がすべてあるという事実により、さらに複雑になります。
私は個人的に、卒業後、そして最初の仕事を終えた後でも指針を理解できませんでした。私が知っていた唯一のことは、リンクリスト、バイナリツリー、および配列を関数に渡すために必要であることです。これは私の最初の仕事でさえ状況でした。私がインタビューを始めたときだけ、私はポインターの概念が深く、途方もない使用と可能性を持っていることを理解しています。それから私はK&Rを読み、独自のテストプログラムを書き始めました。私の全体的な目標は、仕事主導でした。
この時点で、ポインタが良い方法で教えられていれば、ポインタは本当に悪くも難しくもないことがわかりました。残念ながら私が卒業でCを学んだとき、先生はポインターに気づかず、課題でさえポインターの使用量が少なかった。大学院レベルでは、ポインタの使用は実際にはバイナリツリーとリンクリストを作成することまでしかありません。ポインタを操作するためにポインタを適切に理解する必要がないというこの考えは、ポインタを学習するという考えを打ち砕きます。
主な問題の人々は、なぜポインタが必要なのか理解していません。スタックとヒープが明確でないためです。小さなメモリモードを備えたx86用の16ビットアセンブラから開始するのが良いでしょう。これは、多くの人々がスタック、ヒープ、および「アドレス」を理解するのに役立ちました。そして、byte :)現代のプログラマーは、32ビット空間をアドレス指定するために必要なバイト数を知らないことがあります。彼らはどのようにしてポインタを知ることができますか?
2番目の瞬間は表記法です。ポインタを*として宣言し、アドレスを&として取得しますが、これは一部の人にとって理解しにくいものです。
そして、私が最後に目にしたのはストレージの問題でした。彼らはヒープとスタックを理解していますが、「静的」については理解できません。