Cポインターについて人々は何が難しいと思いますか?[閉まっている]


173

ここに投稿された質問の数から、ポインタとポインタ演算について頭を悩ますとき、人々がいくつかのかなりの根本的な問題を抱えていることは明らかです。

その理由を知りたいです。彼らは私に大きな問題を実際に引き起こしたことはありません(私は最初に新石器時代にそれらについて知りましたが)。これらの質問に対するより良い答えを書くために、人々が難しいと思うことを知りたいのです。

それで、ポインタに苦労している場合、または最近「突然」気がついた場合、問題の原因となったポインタの側面は何でしたか?


54
ぎこちなく、誇張され、不必要に物議を醸し、真実の粒で答えます。彼らは、ダニエル・ブーンが意図した神のようなベアメタルでプログラミングすることから始めるべきです!
dmckee ---元モデレーターの子猫

3
それはので...とプログラマーが良いだろうだろう、あなたの最善の努力にもかかわらず、議論に発展します。
dmckee ---元モデレーターの子猫

これは議論の余地があり、主観的であり、N個の結果を生成しますが、私の難しいのは簡単です。おそらくプログラマで動作しますが、サイトはベータ版ではないため、これらの質問はまだ移行していません。
Sam Saffron

2
@Sam Saffron:私はこれがより多くのプログラマーであることに同意しますが、SEタイプの質問ですが、正直言って、人々が「簡単だと思う」と「ポインタを見るのが嫌い」だとスパムとしてマークしてもかまいません。彼らです。
jkerian

3
「指に集中しないでくださいそれは離れて月を指す指のようなものですか、あなたは天国の栄光があることすべてを欠場する」--Bruceリー:誰かがこのアップを持参しなければならない
ムーが短すぎる

回答:


86

人々は答えを深めすぎているのではないかと思います。スケジューリング、実際のCPU操作、またはアセンブリレベルのメモリ管理について理解する必要はありません。

私が教えていたとき、学生の理解の次の穴が最も一般的な問題の原因であることがわかりました。

  1. ヒープとスタックのストレージ。一般的な意味でさえ、これを理解していない人がどれほどいるのか、驚くほどです。
  2. フレームをスタックします。ローカル変数のスタックの専用セクションの一般的な概念と、それが「スタック」である理由...戻り場所の格納、例外ハンドラの詳細、以前のレジスタなどの詳細は、誰かが試みるまで安全に残すことができますコンパイラをビルドします。
  3. 「メモリはメモリはメモリである」キャスティングは、特定のメモリチャンクに対して、演算子のバージョンやコンパイラが与える領域を変更するだけです。人々が「(プリミティブ)変数Xが本当に何であるか」について話すとき、あなたはこの問題に対処していることを知っています。

私の学生のほとんどは、メモリのチャンク、一般的には現在のスコープのスタックのローカル変数セクションの簡略図を理解することができました。一般的には、さまざまな場所に架空の住所を明示的に提供することが役立ちました。

要約すると、ポインタを理解したい場合、変数を理解する必要があり、それらが現代のアーキテクチャで実際に何であるかを理解する必要があると私は言っています。


13
私見、スタックとヒープを理解することは、低レベルのCPU詳細と同じくらい不必要です。スタックとヒープは実装の詳細です。ISO C仕様では、「スタック」という単語についての単一の言及はなく、K&Rについても言及していません。
sigjuice

4
@sigjuice:あなたの反対は、質問と答えの両方の要点を逃しています。A)K&R Cは時代遅れですB)ISO Cはポインターを持つ唯一の言語ではありません。私のポイント1および2は非Cベースの言語に対して開発されましたC)アーキテクチャの95%(言語ではない)がヒープを使用します/ stackシステムでは、例外がそれに関連して説明されるのは十分に一般的です。D)質問のポイントは、「なぜISO Cを説明するのか」ではなく、「なぜポインタが理解されないのか」ということでした
jkerian

9
@ジョン・マルケッティ:さらに...質問が「ポインターに関する人々の問題の根本的な問題は何ですか」であるとすれば、ポインター関連の質問をする人々が「あなたはそうではない」とひどく感銘を受けるとは思わない答えとして本当に知る必要がある」明らかに彼らは同意しません。:)
jkerian

3
@jkerian古くなっているかもしれませんが、ポインタを説明するK&Rの3〜4ページは、実装の詳細を必要とせずに古くなっています。実装の詳細の知識はさまざまな理由で役立ちますが、IMHO、それは言語の主要な構成を理解するための前提条件であってはなりません。
sigjuice

3
「一般的に、さまざまな場所に架空の住所を明示的に提供することは助けになった。」-> +1
fredoverflow 2010年

146

私が最初にそれらを使用し始めたとき、私が抱えていた最大の問題は構文でした。

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で何かをしたので久しぶりです。ちょっと寂しいですが、それから私は少しマゾヒストです)


21
最初のコメントへのコメント>> * ip = 4; // ipの値を「4」に設定します<<が間違っています。>> // ipが指すものの値を「4」に設定する必要があります
aaaa bbbb

8
どんな言語でも、型が多すぎるのは良くない考えです。"foo(&(*** ipppArr));"と書いてあるかもしれません。Cでは奇妙ですが、「std :: map <std :: pair <int、int>、std :: pair <std :: vector <int>、std :: tuple <int、double、std :: list C ++の<int >>>> "も非常に複雑です。これは、C ++のCまたはSTLコンテナーのポインターが複雑であることを意味しません。これは、コードの読者が理解できるように、より適切な型定義を使用する必要があることを意味します。
Patrick

20
私は構文の誤解が最も多く投票された答えであると心から信じられません。これは、ポインターについて最も簡単な部分です。
ジェイソン

4
この答えを読んでも、私は一枚の紙を手に入れて絵を描きたくなりました。Cでは、いつも絵を描いていました。
マイケルイースター

19
@ジェイソン大多数の人が難しいと思う以外に、難しさの客観的な尺度は何ですか?
Rupert Madden-Abbott

52

ポインタを正しく理解するには、基盤となるマシンのアーキテクチャに関する知識が必要です。

車の運転方法を知っているほとんどの人がエンジンについて何も知らないのと同じように、今日の多くのプログラマーは自分の機械がどのように機能するかを知りません。


18
@dmckee:まあ、私は間違っているのですか?何人のJavaプログラマがsegfaultに対処できますか?
Robert Harvey、

5
segfaultはスティックシフトと関係がありますか?-Javaプログラマー
トムアンダーソン、

6
@ロバート:それは本物の補完物として意図されていました。これは人々の気持ちを損なうことなく議論するのは難しいテーマです。そして、私のコメントが、あなたがどうにか避けたと思っていた非常に矛盾を引き起こしたと思います。Mea cupla。
dmckee ---元モデレーターの子猫2010

30
同意しません; ポインターを取得するために基礎となるアーキテクチャーを理解する必要はありません(とにかくそれらは抽象化です)。
ジェイソン

11
@ジェイソン:Cでは、ポインターは基本的にメモリアドレスです。機械のアーキテクチャを理解しないと、安全に作業することはできません。en.wikipedia.org/wiki/Pointer_(computing)およびboredzo.org/pointers/#definitionを
Robert Harvey

42

ポインターを扱うとき、混乱する人々は2つのキャンプの1つに広くいます。私は両方に行ったことがありますか?

array[]群衆

これは、まっすぐにポインタ表記から配列表記に変換する方法がわからない(またはそれらが関連していることさえ知らない)群集です。配列の要素にアクセスするには、次の4つの方法があります。

  1. 配列名による配列表記(インデックス)
  2. ポインタ名を持つ配列表記(インデックス)
  3. ポインター名を含むポインター表記(*)
  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 pointerpointer 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()だけで、おそらくそのような残虐行為を書くことで罪を犯しますが、それは単に良いコードではなく、確実に保守可能ではありません。

さて、この答えは私が期待していたよりも長くなりました...


5
ここで、Cの質問に対するC ++の回答を示したと思います...少なくとも2番目の部分。
detly

ポインターへのポインターはCにも適用されます:p
David Titarenco

1
えっ?配列を渡すときにポインタへのポインタのみが表示されます。2番目の例は、まともなCコードには実際には適用できません。また、CをC ++の混乱にドラッグしています。Cの参照は存在しません。
new123456 2011

多くの正当なケースでは、ポインタへのポインタを処理する必要があります。たとえば、ポインタを返す関数を指す関数ポインタを処理する場合。別の例:可変数の他の構造体を保持できる構造体。その他多数
デビッドティタレンコ

2
「参照へのポインタはCでは違法です」-「不在」のようなものです:)
Kos

29

私は参考資料の質と教えをしている人々を個人的に責めます。Cのほとんどの概念(ただし、特にポインター)は、ひどく教えられているだけです。私は自分のCブック(「世界が必要する最後のもの」はCプログラミング言語に関する別のブックです)を書くと脅し続けていますが、そのための時間も忍耐力もありません。だから私はここでぶらぶらして、人々から標準からのランダムな引用を投げます。

Cが最初に設計されたときに、日常の作業でそれを回避する方法がなかったという理由だけでマシンアーキテクチャをかなり詳細なレベルまで理解していると想定されていたという事実もあります(メモリが非常にタイトでプロセッサが非常に低速でした)作成した内容がパフォーマンスにどのように影響するかを理解する必要がありました)。


3
はい。'int foo = 5; int * pfoo =&foo; それがどれほど便利かをご覧ください。さて、次に進みます... '自分の二重リンクリストライブラリを作成するまで、実際にはポインタを使用しませんでした。
John Lopez

2
+1。私はCS100の生徒の家庭教師をしていたので、彼らの問題の多くは、理解しやすい方法でポインターを調べるだけで解決されました。
ベンザド

1
歴史的な文脈のための+1。それからずっと後から始まったこのことは、私には決して起こりませんでした。
ルミ

26

Joel Spolskyのサイト(JavaSchoolsの危険)にポインターが難しいという考えを支持する素晴らしい記事があります。ます。

[免責事項- それ自体はJava嫌いではありません。]


2
@ジェイソン-それは本当ですが議論を否定しません。
Steve Townsend、

4
スポルスキー氏は、JavaSchoolsがポインターを難しくする理由だとは言っていない。彼は、それらがコンピュータサイエンスの学位を持つポインターの読み書きができない人々をもたらすと言っています。
ベンザド

1
@benzado-公平な点-「ポインタが難しいという考えを支持する素晴らしい記事」を読んだら、私の短い投稿は改善されるでしょう。この記事が意味することは、「「良い学校」の学位を取得すること」は、以前のように以前の開発者ほど成功の予測因子ではなく、「ポインターを理解すること」(および再帰)は依然としてそうであるということです。
スティーブタウンゼント

1
@Steve Townsend:スポルスキー氏の主張の要点を逃していると思います。
Jason

2
@Steve Townsend:Spolsky氏は、Javaスクールがポインターと再帰を知らない世代のプログラマーを育てていると主張しています。Javaスクールが普及しているためにポインターが難しいということではありません。「なぜこれが難しいのかについての素晴らしい記事がある」と述べ、その記事にリンクしているように、後者の解釈があるようです。私が間違っていたら私を許してください。
ジェイソン

24

「下」にある知識に基づいていない場合、ほとんどのことは理解するのが難しくなります。私がCSを教えたとき、学生が非常に単純な「マシン」、つまり10進レジスタと10進アドレスで構成されるメモリを持つ10進オペコードを備えたシミュレートされた10進コンピュータのプログラミングを始めたとき、はるかに簡単になりました。彼らは非常に短いプログラムを入れて、例えば、合計を得るために一連の数値を追加します。それから彼らはそれを一歩踏み込んで何が起こっているのかを見守っていました。彼らは、「Enter」キーを押したまま、「速く」実行されるのを見ることができます。

SOのほとんどの人は、なぜ基本的なものにすると便利なのか疑問に思います。プログラミングの仕方がわからなかったのを忘れます。そのようなおもちゃのコンピューターで遊ぶと、計算がステップバイステップのプロセスであるという考え、少数の基本的なプリミティブを使用してプログラムを構築するという考え、メモリの概念など、プログラミングなしではできない概念が導入されます変数が格納される場所としての変数。変数のアドレスまたは名前は、変数に含まれる番号とは異なります。プログラムに入る時間とプログラムが「実行される」時間には違いがあります。私は、非常に単純なプログラム、ループとサブルーチン、配列、シーケンシャルI / O、ポインタ、データ構造など、一連の「スピードバンプ」を横断するようにプログラムすることを学ぶのが好きです。

最後に、Cに到達したとき、K&Rは非常に良い説明をしてくれましたが、ポインターは混乱しています。私がCでそれらを学習した方法は、右から左に読む方法を知ることでした。int *p頭の中で見たときのように、「をp指す」と言いintます。Cは、アセンブリ言語からの1つのステップとして発明されたもので、それが私が気に入っているところです。Cはその「基礎」に近いものです。他のものと同様に、その基礎がない場合、ポインターは理解するのが難しくなります。


1
これを学ぶ良い方法は、8ビットマイクロコントローラーをプログラムすることです。彼らは理解しやすいです。Atmel AVRコントローラーを使用してください。gccでもサポートされています。
Xenu、

@Xenu:同意する。私にとってそれはIntel 8008と8051
でした

私にとってそれは、MITの薄暗い霧の中のカスタム8ビットコンピューター(「多分」)でした。
QuantumMechanic 2012

マイク-生徒に心臓をつける必要があります:)
QuantumMechanic 2012

1
@Quantum:CARDIAC-いいですね、聞いたことはありませんでした。"多分"-推測させてください、Sussman(et al)がMead-Conwayの本を読んで、自分のLSIチップを作成していたときですか?そこに行ってから少し経ちました。
Mike Dunlavey、2012

17

K&Rで説明を読むまで、ポインタはありませんでした。その時点まで、ポインタは意味がありませんでした。「ポインタを覚えないでください、混乱し、頭が痛くなり、動脈瘤ができます」と人々が言っ​​たたくさんのことを読んだので、私は長い間それを避け、この不必要な難しい概念の空気を作りました。

そうでなければ、主に私が思っていたのは、なぜ地球上で値を取得するためにフープを通過しなければならない変数が必要なのか、それに変数を割り当てたい場合は、値を取得するために奇妙なことをしなければならなかった理由です。それらに。変数の要点は値を格納することだと私は思ったので、なぜ誰かがそれを複雑にしたかったのかは私を超えていました。 「そのため、ポインターを使用する場合は、*演算子を使用してその値を取得する必要がありますか???これは、どのような間抜けな変数ですか?」、と思った。意味のない、しゃれは意図されていません。

複雑だったのは、ポインタがアドレスだとわからなかったから何かへです。それがアドレスであり、何かへのアドレスを含むものであり、そのアドレスを操作して有用なことを実行できることを説明すると、混乱が解消されると思います。

PCのポートにアクセス/変更するためにポインターを使用する必要があるクラス、ポインター演算を使用してさまざまなメモリの場所をアドレス指定し、引数を変更するより複雑なCコードを見ると、ポインターは無意味であるという考えを思いとどまらせました。


4
組み込みアプリケーションなど、使用するリソース(RAM、ROM、CPU)が限られている場合、ポインターは非常に理にかなっています。
Nick T

Nickのコメントの+1-特に構造体を渡す場合。
new123456 2011

12

ここで、一時停止したポインタ/配列の例を示します。次の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つですが、それでも同じ値です。「ソース」の順列についても同じことが言えます。

余談ですが、私は個人的には最初のバージョンを好みます。


私も最初のバージョンを好む(句読点が少ない)。
sigjuice

++だから私はやるが、あなたは本当にと注意する必要があるsizeof(source)場合ので、sourceポインタである、sizeofそれはあなたが望むものではありません。私はときどき(常にではありませんが)sizeof(source[0]) * number_of_elements_of_sourceそのバグから遠く離れるように書くだけです。
Mike Dunlavey、2011年

destination、&destination、&destination [0]はまったく同じではありませんが、memcpyで使用すると、それぞれ異なるメカニズムを介して同じvoid *に変換されます。ただし、sizeofの引数として使用すると、2つの異なる結果が得られ、3つの異なる結果が可能です。
gnasher729 2014年

オペレーターの住所が必要だと思いましたか?
MarcusJ 2015

7

まず、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;
}

動的にスペースを割り当てようとします。うまくいきませんでした。それが機能するかどうかはわかりませんでしたが、他にどのように機能するかわかりませんでした。

私は後でについて学んだmallocnew、彼らは本当に私には不思議なメモリ・ジェネレータのように思えました。それらがどのように機能するかについては何も知りませんでした。

しばらくして、私は再帰を再び教えられていました(以前は自分でそれを学びましたが、今はクラスにいました)、私はそれが内部でどのように機能するかを尋ねました-個別の変数はどこに保存されましたか?私の教授は「山積み」と言って、多くのことが私に明らかになりました。以前にその用語を聞いたことがあり、ソフトウェアスタックを実装したことがありました。他の人がずっと前に「スタック」を参照していると聞いていましたが、それを忘れていました。

この頃、Cで多次元配列を使用すると、非常に混乱する可能性があることにも気付きました。それらがどのように機能するかは知っていましたが、絡み合うのが非常に簡単だったので、できる限りそれらを使用して回避することにしました。ここでの問題は主に構文(特に、関数への受け渡しまたは関数からの戻り)であったと思います。

翌年または2年間、学校向けにC ++を書いていたので、データ構造のポインターを使用して多くの経験を得ました。ここで私は新しい一連の問題を抱えていました-ポインターを混同しています。私は複数レベルのポインタ(などnode ***ptr;)でつまずきます。私はポインタを間違った回数逆参照し、最終的に何回かを見つけることに頼ります*試行錯誤によって必要なます。

ある時点で、プログラムのヒープがどのように機能するかを知りました(ある程度のことですが、それでも夜はもう上手くいかなくなりました)。malloc特定のシステムで返されるポインタの前の数バイトを見ると、実際に割り当てられたデータの量を確認できることを読んだことを覚えています。のコードがmallocOSにさらに多くのメモリを要求する可能性があり、このメモリは実行可能ファイルの一部ではないことに気付きました。どのように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);

そこにポインター型が間違っていると、コンパイラー・エラーを理解するのがずっと難しくなります。欲求不満の試行錯誤の変更に頼りたくて、おそらく事態をさらに悪化させたくなるでしょう。


1
+1。徹底的で洞察に満ちています。scanfのことは忘れていましたが、あなたがそれを取り上げたので、同じ混乱を覚えています。
ジョーホワイト

6

振り返ると、最終的にポインターを理解するのに本当に役立つ4つのことがありました。これまでは使用できましたが、完全には理解できませんでした。つまり、フォームに従うと、希望する結果が得られることはわかっていましたが、フォームの「理由」を完全には理解していませんでした。これはあなたが尋ねたとおりではないことを私は理解していますが、それは有用な帰結です。

  1. 整数へのポインターを受け取り、整数を変更するルーチンを作成します。これにより、ポインターの動作に関するメンタルモデルを構築するために必要なフォームが得られました。

  2. 1次元の動的メモリ割り当て。1次元のメモリ割り当てを理解することで、ポインタの概念を理解することができました。

  3. 2次元の動的メモリ割り当て。2次元メモリ割り当てを理解することでその概念が強化されましたが、ポインタ自体にもストレージが必要であり、考慮に入れる必要があることもわかりました。

  4. スタック変数、グローバル変数、ヒープメモリの違い。これらの違いを理解することで、ポインタが指す/参照するメモリのタイプを知ることができました。

これらの各項目では、下位レベルで何が起こっているのかを想像する必要がありました。時間と労力がかかりましたが、それだけの価値がありました。ポインタを理解するためには、それらがどのように機能し、どのように実装されるかについて、そのメンタルモデルを構築する必要があると私は確信しています。

元の質問に戻ります。前のリストに基づいて、元々把握が難しかった項目がいくつかありました。

  1. ポインタを使用する方法と理由。
  2. どのように違いますが、配列と似ていますか。
  3. ポインター情報が格納されている場所を理解する。
  4. ポインタが何をどこで指しているのかを理解する。

ねえ、あなたがあなたの答えで説明したのと同じような方法で私が学ぶことができる記事/本/あなたの絵/落書き/何かを私に教えてもらえますか?これは基本的には何でも、よく学べる方法だと固く信じています。深い理解と優れたメンタルモデル
Alexander Starbuck

1
@AlexStarbuck-これが軽快に聞こえるという意味ではありませんが、科学的方法は優れたツールです。特定のシナリオで発生する可能性のあることについて自分自身の絵を描きます。何かをプログラムしてテストし、得られた結果を分析します。それはあなたが期待するものと一致しましたか?そうでない場合、どこが異なるかを特定しますか?必要に応じて繰り返し、理解度とメンタルモデルの両方をテストするために徐々に複雑さを増やします。
Sparky

6

Cの一部のテレフォニープログラムで「ポインターモーメント」を使用していました。クラシックCのみを理解するプロトコルアナライザーを使用してAXE10交換エミュレーターを作成する必要がありました。すべてはポインターを知ることにかかっていました。私はそれらなしで自分のコードを書いてみました(ちょっと、私は "プレポインタ"で少し緩めました)と完全に失敗しました。

私にとって、それらを理解するための鍵は&(アドレス)演算子でした。&i「iのアドレス」を意味することを理解したら、「iが*i指すアドレスの内容」を意味することを少し後で理解しました。コードを書いたり読んだりするときはいつでも、「&」と「*」の意味を常に繰り返し、最終的には直感的に使用するようになりました。

残念なことに、私はVB、次にJavaに追い込まれたため、ポインターの知識は以前ほどシャープではありませんが、「ポストポインター」であることがうれしいです。ただし、* * p を理解する必要があるライブラリを使用するように要求しないでください。


場合は&iアドレスであり、*i内容で、何がありますかi
Thomas Ahle

2
私はiの使用法に過負荷をかけています。任意の変数iの場合、&iは「のアドレス」iを意味し、iはそれ自体が「&iの内容」を意味し、* iは「&iの内容をアドレスとして扱い、そのアドレスに移動し、内容」。
Gary Rowe

5

少なくとも私にとって、ポインタの主な難点は、Cから始めていなかったことです。Javaから始めました。ポインターの概念全体は、私がCを知っていると期待されていた大学での2、3のクラスまでは本当に異質でした。それで、Cの基本と、ポインターの基本的な使い方を学びました。それでも、Cコードを読んでいるたびに、ポインタ構文を調べる必要があります。

非常に限られた経験(1年の実世界+大学での4年)では、教室以外の場所で実際に使用する必要がなかったので、ポインターが混乱を招きました。そして、CやC ++の代わりにJAVAを使ってCSを始めた生徒たちに共感できます。あなたが言ったように、あなたは「新石器時代」の時代に指針を学び、それ以来おそらくそれを使ってきました。新しい人々にとって、これらの言語はすべて抽象化されているため、メモリの割り当てとポインタ演算の概念は非常に異質です。

PSスポルスキーのエッセイを読んだ後、彼の「JavaSchools」の説明は、私がコーネル大学('05 -'09)で経験したものとはまったく異なりました。私は構造と関数型プログラミング(sml)、オペレーティングシステム(C)、アルゴリズム(ペンと紙)、およびJavaで教えられなかった他のクラスの全体を取り上げました。ただし、すべてのイントロクラスと選択科目はすべてJavaで行われました。これは、ポインター付きのハッシュテーブルを実装するよりも高いレベルのことを実行しようとするときに、ホイールを再発明しないことの価値があるためです。


4
正直なところ、まだ指針に問題があることを考えると、Cornellでの経験がJoelの記事と実質的に矛盾しているとは思えません。言うまでもなく、あなたの脳の十分な部分がJavaの考え方に結び付けられて、彼の主張を裏付けています。
jkerian

5
ワット?Java(またはC#、Python、またはおそらく数十の他の言語)での参照は、算術演算を行わない単なるポインタです。ポインタを理解するということvoid foo(Clazz obj) { obj = new Clazz(); }void bar(Clazz obj) { obj.quux = new Quux(); }、引数を変更しているときに何も実行しない理由を理解することを意味します...

1
私はJavaの参照が何であるかを知っていますが、Javaでリフレクションを行うか、CIで意味のあるものを書くように依頼された場合、それを完全に排除することはできません。毎回初めて学ぶような多くの研究が必要です。
shoebox639

1
C言語に堪能になることなく、C言語のオペレーティングシステムクラスをどのように通過したのですか。悪意はありません。単純なオペレーティングシステムを最初から開発しなければならないことを覚えているだけです。私はポインタを1000回使用したに違いありません...
Gravity

5

これは答えではありません:cdecl(またはc ++ decl)を使用してそれを把握します。

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

4

構文を大幅に変更することなく、コードに次元を追加します。これについて考える:

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...それは人々が混乱するものであるかもしれません。


4

私は2年間ほどc ++でプログラミングし、その後Java(5年間)に変換しました。しかし、最近ネイティブアイテムを使用しなければならなくなったとき、私は(驚いて)ポインターについて何も忘れていないこと、そして使いやすいことさえわかった。これは、7年前に最初にコンセプトを理解しようとしたときに経験したものとは対照的です。それで、理解と好みはプログラミングの成熟度の問題だと思いますか?:)

または

ポインターは自転車に乗るようなものです。ポインターを操作する方法を理解したら、忘れることはありません。

全体として、把握するのが難しいかどうかにかかわらず、ポインターのアイデア全体は非常に教育的であり、ポインターを使用する言語でプログラミングするかどうかに関係なく、すべてのプログラマーが理解する必要があると思います。


3

間接的なため、ポインタは困難です。


「コンピューターサイエンスにはもう1レベルの間接参照では解決できない問題はないと言われています」(誰が最初に言ったか
典型的なポール

それは魔法のようなもので、誤った方向が人々を混乱させるものです(しかし、それはまったく素晴らしいものです)
Nick T

3

ポインター(低レベルの作業の他のいくつかの側面と共に)は、ユーザーに魔法を奪うことを要求します。

ほとんどの高レベルのプログラマーは魔法が好きです。


3

ポインタは、オブジェクトへのハンドルとオブジェクト自体の違いを処理する方法です。(わかりました、必ずしもオブジェクトではありませんが、あなたは私が何を意味するか、そして私の心がどこにあるかを知っています)

ある時点で、おそらくこの2つの違いに対処する必要があります。現代の高級言語では、これは値によるコピーと参照によるコピーの違いになります。いずれにせよ、プログラマにとって理解しにくい概念です。

ただし、指摘したように、Cでこの問題を処理するための構文は、醜く、一貫性がなく、混乱します。最終的には、本当にそれを理解しようとするならば、ポインタは意味をなすでしょう。しかし、ポインタへのポインタを扱うようになり、悪心が続くと、私だけでなく他の人々にとっても非常に混乱します。

ポインタについて覚えておかなければならないもう1つの重要なことは、ポインタが危険であることですCはマスタープログラマーの言語です。それはあなたが何をしているのかをあなたが知っていると仮定し、それによってあなたに本当に物事を台無しにする力を与えます。一部のタイプのプログラムは依然としてCで作成する必要がありますが、ほとんどのプログラムはそうではなく、オブジェクトとそのハンドルの違いをより適切に抽象化する言語がある場合は、それを使用することをお勧めします。

実際、多くの最新のC ++アプリケーションでは、必要なポインター演算がカプセル化および抽象化されることがよくあります。開発者がいたるところでポインタ演算を行うことは望ましくありません。最下位レベルでポインター演算を行う、集中化された十分にテストされたAPIが必要です。このコードに変更を加えるには、細心の注意と広範なテストを行う必要があります。


3

Cポインターが難しい理由の1つは、実際には同等ではないいくつかの概念が混同されていることです。しかし、それらはすべてポインタを使用して実装されているため、概念を解くのに苦労する可能性があります。

Cでは、ポインターは他のものと同様に使用されます。

  • 再帰的なデータ構造を定義する

Cでは、次のように整数のリンクリストを定義します。

struct node {
  int value;
  struct node* next;
}

これはCで再帰的なデータ構造を定義する唯一の方法であり、概念がメモリアドレスなどの低レベルの詳細とはまったく関係がない場合にのみ、ポインターはそこにあります。ポインタの使用を必要としないHaskellの次の同等のものを検討してください。

data List = List Int List | Null

非常に簡単-リストは空であるか、値とリストの残りの部分から形成されています。

  • 文字列と配列を反復する

fooCの文字列のすべての文字に関数を適用する方法は次のとおりです。

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*。そして、すべてのことを含むことができるリストを反復する関数を宣言し、各メンバーに関数を適用するだけです。mapHaskellで宣言されている方法と比較してください。

map::(a->b)->[a]->[b]

それははるかに簡単です:map変換する関数取る関数であるaにしbて、リストにそれを適用するa「のリスト得るためのbs」を。C関数のg_list_foreach場合と同様に、mapそれはそれが適用される型についてそれ自身の定義で何かを知る必要はありません。

総括する:

最初に再帰的なデータ構造、イテレータ、ポリモーフィズムなどについて個別の概念として学び、次にこれらすべてをマッシュアップするのではなく、Cこれらのアイデアを実装するためにポインタを使用する方法を学んだ場合、Cポインタはそれほど混乱しにくいと思います概念をまとめて「ポインタ」の単一の主題にします。


c != NULL"Hello world"の例の誤用 ...を意味し*c != '\0'ます。
Olaf Seibert

2

おそらく、マシンレベルからの確かな基盤が必要だと思います。いくつかのマシンコード、アセンブリ、およびアイテムとデータ構造をRAMで表現する方法を紹介します。少し時間がかかります。宿題や問題解決の練習、そして思考も必要です。

しかし、人が最初に高水準言語を知っている場合(これは問題ありません-大工が斧を使用します。アトムを分割する必要がある人は別のものを使用します。大工である人が必要であり、アトムを研究する人がいます)高水準言語を知っているこの人には、2分間のポインターの紹介が与えられ、その後、ポインターの計算、ポインターへのポインター、可変サイズの文字列へのポインターの配列、および文字の配列の配列などを理解するのは難しいでしょう。低レベルの強固な基盤は大きな助けになります。


2
ポインタのグローキングは、マシンコードまたはアセンブリの理解を必要としません。
ジェイソン

いいえ、必要ありません。しかし、アセンブリを理解している人は、必要なメンタルコネクションのほとんど(すべてではないにしても)をすでに作成しているので、ポインタがはるかに簡単になるでしょう。
cHao

2

私がいつも抱えていた問題(主に独学)は、ポインターを使用する「タイミング」です。ポインタを構築するための構文に頭を悩ませることはできますが、どのような状況でポインタを使用する必要があるかを知る必要があります。

この考え方を持つのは私だけですか?;-)


わかった。私の反応はそれを扱っています。
J. Polfer、

2

むかしむかし...私たちは8ビットのマイクロプロセッサを持っていて、全員がアセンブリで書き込みました。ほとんどのプロセッサには、ジャンプテーブルとカーネルに使用されるある種の間接アドレス指定が含まれていました。高水準言語が登場したとき、抽象化の薄い層を追加し、それらをポインタと呼びました。長年にわたり、ハードウェアから離れる機会がますます増えています。これは必ずしも悪いことではありません。これらは、理由により高水準言語と呼ばれています。細かいことではなく、やりたいことに集中できるほど、うまくいく。


2

多くの学生が間接参照の概念に問題を抱えているようです。特に、間接参照の概念に初めて出会ったときはそうです。私が学生時代に覚えていたのは、私のコースの+100人の学生のうち、ほんの一握りの人だけが実際に指針を理解していたことを覚えています。

間接参照の概念は、実際に日常的に使用するものではないため、最初に理解するのは難しい概念です。


2

最近、ポインタクリックの瞬間があり、混乱していることに驚いていました。それはみんながそれについてあまりにも多く話したことであり、私はいくつかのダークマジックが起こっていると思いました。

私がそれを得た方法はこれでした。定義されたすべての変数に、コンパイル時に(スタック上で)メモリ空間が与えられると想像してください。オーディオや画像などの大きなデータファイルを処理できるプログラムが必要な場合は、これらの潜在的な構造に対して固定量のメモリは必要ありません。したがって、このデータを(ヒープ上に)保持するために、実行時まで一定量のメモリを割り当てるまで待機します。

メモリにデータを保存したら、そのデータに対して操作を実行するたびに、そのデータをメモリバス全体にコピーする必要はありません。画像データにフィルターを適用したいとします。画像に割り当てたデータの先頭から始まるポインターがあり、関数がそのデータを横切って実行され、データが所定の位置に変更されます。私たちが何をしているかわからない場合は、操作を実行するときに、データの複製を作成することになります。

少なくとも今のところはそうです!


後で考えると、たとえばメモリ制限のあるデバイスで、画像/オーディオ/ビデオを保持するための固定量のメモリを定義することができますが、その場合は、ある種のストリーミングメモリの内外のソリューションに対処する必要があります。
Chris Barry

1

ここで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 ++にはこれらの言語固有の癖がすべてあるという事実により、さらに複雑になります。


C ++ 2011では、変数宣言に関する限り、かなり改善されています。
gnasher729 2014年

0

私は個人的に、卒業後、そして最初の仕事を終えた後でも指針を理解できませんでした。私が知っていた唯一のことは、リンクリスト、バイナリツリー、および配列を関数に渡すために必要であることです。これは私の最初の仕事でさえ状況でした。私がインタビューを始めたときだけ、私はポインターの概念が深く、途方もない使用と可能性を持っていることを理解しています。それから私はK&Rを読み、独自のテストプログラムを書き始めました。私の全体的な目標は、仕事主導でした。
この時点で、ポインタが良い方法で教えられていれば、ポインタは本当に悪くも難しくもないことがわかりました。残念ながら私が卒業でCを学んだとき、先生はポインターに気づかず、課題でさえポインターの使用量が少なかった。大学院レベルでは、ポインタの使用は実際にはバイナリツリーとリンクリストを作成することまでしかありません。ポインタを操作するためにポインタを適切に理解する必要がないというこの考えは、ポインタを学習するという考えを打ち砕きます。


0

ポインタ..ハァ..私の頭の中のポインタのすべては、それが参照するものの実際の値がどこにあるかというメモリアドレスを与えるということです..それについて魔法はありません..いくつかのアセンブリを学ぶ場合、それほど多くの学習をする必要はありませんどのようにポインタが機能するか..みんなに来て... Javaでもすべてが参照です..


0

主な問題の人々は、なぜポインタが必要なのか理解していません。スタックとヒープが明確でないためです。小さなメモリモードを備えたx86用の16ビットアセンブラから開始するのが良いでしょう。これは、多くの人々がスタック、ヒープ、および「アドレス」を理解するのに役立ちました。そして、byte :)現代のプログラマーは、32ビット空間をアドレス指定するために必要なバイト数を知らないことがあります。彼らはどのようにしてポインタを知ることができますか?

2番目の瞬間は表記法です。ポインタを*として宣言し、アドレスを&として取得しますが、これは一部の人にとって理解しにくいものです。

そして、私が最後に目にしたのはストレージの問題でした。彼らはヒープとスタックを理解していますが、「静的」については理解できません。

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