私は大学を卒業したばかりで、しばらくの間 C++ で作業しています。私は C++ の基本をすべて理解して使用していますが、ポインターやクラスなどのより高度なトピックを理解するのに苦労しています。私はいくつかの本やチュートリアルを読み、それらの例を理解しましたが、高度な実際の例を見ると、それらを理解できません。C++プログラミングを次のレベルに引き上げるのを妨げているような気がするので、これは私を殺しています。他の誰かがこの問題を抱えていましたか?もしそうなら、どのようにそれを突破しましたか?ポインターとクラスの概念を実際によく説明している本やチュートリアルを知っている人はいますか? または、高度なポインターとクラス手法を使用した適切な説明コメント付きのサンプル コードですか? どんな助けでも大歓迎です。
27 に答える
ポインタとクラスは、C++では実際には高度なトピックではありません。それらはかなり基本的なものです。
私にとって、矢印の付いたボックスを描き始めたとき、ポインターは固まりました。intのボックスを描画します。また、int *は、intボックスを指す矢印の付いた別のボックスになりました。
それで:
int foo = 3; // integer
int* bar = &foo; // assigns the address of foo to my pointer bar
ポインタボックス(バー)を使用して、ボックス内のアドレスを確認するかどうかを選択できます。(これはfooのメモリアドレスです)。または、アドレスがあるものは何でも操作できます。その操作は、私がその矢印に従って整数(foo)をたどっていることを意味します。
*bar = 5; // asterix means "dereference" (follow the arrow), foo is now 5
bar = 0; // I just changed the address that bar points to
クラスは完全に別のトピックです。オブジェクト指向デザインに関する本はいくつかありますが、頭のてっぺんの初心者に適した本はわかりません。Javaの紹介本で運が良かったかもしれません。
C/C++ のポインターを理解する
ポインターがどのように機能するかを理解する前に、プログラムで変数がどのように格納され、アクセスされるかを理解する必要があります。すべての変数には、(1) データが格納されるメモリ アドレスと (2) 格納されるデータの値の 2 つの部分があります。
メモリアドレスは変数の左辺値と呼ばれることが多く、格納されたデータの値は右辺値と呼ばれます (l と r は左と右を意味します)。
次のステートメントを検討してください。
int x = 10;
内部的に、プログラムはメモリ アドレスを変数 x に関連付けます。この場合、プログラムがアドレス 1001 に存在するように x を割り当てると仮定します (現実的なアドレスではありませんが、簡単にするために選択されています)。したがって、x の左辺値 (メモリ アドレス) は 1001 で、x の右辺値 (データ値) は 10 です。
右辺値には、変数「x」を使用するだけでアクセスできます。左辺値にアクセスするには、「アドレス」演算子 ('&') が必要です。&x は「x のアドレス」と読みます。
Expression Value
----------------------------------
x 10
&x 1001
x に格納された値はいつでも変更できますが (例: x = 20)、x のアドレス (&x) は決して変更できません。
ポインターは、別の変数を変更するために使用できる単純な変数です。これは、右辺値のメモリ アドレスを持つことによって行われます。つまり、メモリ内の別の場所を指します。
「x」へのポインターの作成は、次のように行われます。
int* xptr = &x;
「int*」は、整数値へのポインターを作成していることをコンパイラーに伝えます。「= &x」の部分は、x のアドレスを xptr の右辺値に割り当てることをコンパイラに伝えます。したがって、xptr が x を「指している」ことをコンパイラに伝えています。
xptr が 1002 のメモリ アドレスに割り当てられていると仮定すると、プログラムのメモリは次のようになります。
Variable lvalue rvalue
--------------------------------------------
x 1001 10
xptr 1002 1001
パズルの次のピースは、次のように使用される「間接演算子」('*') です。
int y = *xptr;
間接演算子は、xptr の右辺値をデータ値ではなくメモリ アドレスとして解釈するようにプログラムに指示します。つまり、プログラムは、xptr (1001) によって提供されるアドレスに格納されているデータ値 (10) を探します。
すべてを一緒に入れて:
Expression Value
--------------------------------------------
x 10
&x 1001
xptr 1001
&xptr 1002
*xptr 10
概念が説明されたので、ポインターの力を示すコードを次に示します。
int x = 10;
int *xptr = &x;
printf("x = %d\n", x);
printf("&x = %d\n", &x);
printf("xptr = %d\n", xptr);
printf("*xptr = %d\n", *xptr);
*xptr = 20;
printf("x = %d\n", x);
printf("*xptr = %d\n", *xptr);
出力については、次のように表示されます (注: メモリ アドレスは毎回異なります)。
x = 10
&x = 3537176
xptr = 3537176
*xptr = 10
x = 20
*xptr = 20
'*xptr' に値を割り当てると、'x' の値がどのように変化するかに注目してください。これは、'&x' と 'xptr' が同じ値を持つことから明らかなように、'*xptr' と 'x' がメモリ内の同じ場所を参照しているためです。
以前は、パスカルウェイでポインタを理解するのに問題がありました:)アセンブラポインタを使い始めたら、メモリにアクセスする唯一の方法でしたが、それが私を襲いました。遠い話のように聞こえるかもしれませんが、アセンブラーを試してみると(コンピューターが実際に何であるかを理解することは常に良い考えです)、おそらくポインターを教えてくれるでしょう。クラス-私はあなたの問題を理解していません-あなたの学校教育は純粋な構造化プログラミングでしたか?クラスは、実際のモデルを論理的に見る方法です。多くのオブジェクト/クラスにまとめられる可能性のある問題を解決しようとしています。
ポインタとクラスはまったく別のトピックなので、このようにまとめることはあまりありません。2つのうち、ポインタの方が基本的だと思います。
ポインタが何であるかを学ぶための良い練習は次のとおりです。
- リンクリストを作成する
- 最初から最後まで繰り返します
- 頭が後ろになり、後ろが頭になるように逆にします
最初にすべてをホワイトボードで実行します。これを簡単に行うことができれば、ポインタが何であるかを理解するのに問題はもうありません。
ポインターは、他の回答で既に対処されているようです (しゃれた意図はありません)。
クラスは OO の基本です。私は OO に頭を悩ませるのに非常に苦労しました。最終的に私を助けてくれたのは、Craig Larman の「UML とパターンの適用」という本でした。何か違うことを言っているように聞こえるかもしれませんが、クラスとオブジェクトの世界に簡単に入ることができます。
昼食時に C++ と OO のいくつかの側面について話し合ったところ、誰か (実際には偉大なエンジニア) が、C++ を学ぶ前に本当に強力なプログラミングのバックグラウンドを持っていない限り、文字通りあなたを台無しにするだろうと言っていました。
最初に別の言語を学習してから、必要に応じて C++ に移行することを強くお勧めします。ポインターについて何か素晴らしいことがあるわけではありません。ポインターは、コンパイラーが操作をアセンブリーに効率的に変換することが困難だったときの痕跡にすぎません。
最近、コンパイラがポインタを使用するよりも配列操作を最適化できない場合、コンパイラは壊れています。
誤解しないでほしいのですが、私は C++ がひどいなどと言っているのではなく、擁護の議論を始めたくないと言っているのでもありません。私はそれを使ってきましたし、今でもときどき使っています。別のものから始めることをお勧めします。 .
マニュアル車の運転を学び、それをオートマチック車に簡単に適用できるようになるようなものではありません。巨大な建設用クレーンの 1 つを運転する方法を学び、それが自動車の運転を開始したときに適用されると想定するようなものです。非常灯をつけたまま、道の真ん中を時速 5 マイルで車を運転していることに気づきます。
[編集] 最後のパラグラフを見直して、これまでで最も正確な類推だったと思います!
アセンブリ言語を学び、次にCを学びます。そうすれば、機械の基本原理が何であるか(そしてそれ以前の指針)を知ることができます。
ポインターとクラスは、C++の基本的な側面です。それらを理解していない場合は、C++を本当に理解していないことを意味します。
個人的には、Cと、アセンブリ言語の内部で何が起こっているのかをしっかりと理解できるようになるまで、C++を数年間控えていました。これはかなり昔のことですが、コンピュータが低レベルでどのように機能するかを理解することは、私のキャリアにとって本当に有益だったと思います。
プログラムを学ぶには何年もかかることがありますが、それは非常にやりがいのあるキャリアなので、それを続ける必要があります。
ポインタを理解するために、私はK&Rの本を十分に推薦することはできません。
ポインタの場合:
この投稿には、ポインタについて非常に思慮深い議論があったことがわかりました。多分それは助けになるでしょう。C#などの参照に精通していますか?それは実際に何か他のものを参照しているものですか?それはおそらくポインタを理解するための良いスタートです。
また、ポインタを紹介する別の方法については、以下のKentFredricの投稿を参照してください。
練習に勝るものはありません。
本を読んだり、講義を聞いたりして、何が起こっているのかをフォローしているように感じるのは簡単です。
私がお勧めするのは、いくつかのコード例(ディスクのどこかにあると思います)を取り、それらをコンパイルして実行してから、別のことをするように変更してみることです。
- 階層に別のサブクラスを追加する
- 既存のクラスにメソッドを追加します
- コレクションを順方向に繰り返すアルゴリズムを変更して、代わりに逆方向に移動します。
それをする「銀の弾丸」の本はないと思います。
私にとって、ポインターの意味を理解するのはアセンブリーでの作業であり、ポインターが実際には単なるアドレスであり、ポインターがあるからといって、それが指すものが意味のあるオブジェクトであるとは限りませんでした。
ある意味で、「ポインタ」は、ソフトウェアの2つの最も基本的なタイプの1つであり、もう1つは、一意にアドレス指定可能なメモリ位置の巨大なブロックに存在する「値」(または「データ」)であると見なすことができます。考えてみてください。オブジェクトや構造体などは実際にはメモリに存在せず、値とポインタのみが存在します。実際、ポインタも値です。メモリアドレスの値であり、メモリアドレスには別の値が含まれています。
したがって、C / C ++では、「int」(intA)を宣言すると、値(数値)を含む32ビットのメモリチャンクを定義することになります。次に「intポインタ」(intB)を宣言すると、intのアドレスを含む32ビットのメモリチャンクを定義することになります。「intB=&intA」と指定することで、後者を前者を指すように割り当てることができます。これで、intBとして定義された32ビットのメモリに、メモリ内のintAの位置に対応するアドレスが含まれます。
intBポインタを「逆参照」すると、intBのメモリに格納されているアドレスを調べ、その場所を見つけて、そこに格納されている値(数値)を調べることになります。
一般的に、「&」、「*」、「->」の演算子を使用しているときに、何を扱っているのかを正確に把握できなくなると、混乱が生じます。これは、アドレス、値、または何ですか。メモリアドレスは単なる場所であり、値はそこに格納されているバイナリ情報であるという事実に焦点を合わせ続ける必要があります。
ポインタとクラスについては、これが私の例えです。カードのデッキを使用します。カードのデッキには額面とタイプ(ハート9枚、スペード4枚など)があります。したがって、「DeckofCards」のプログラミング言語のようなC++では、次のように言います。
HeartCard card = 4; // 4 of hearts!
さて、あなたはハートの4つがどこにあるかを知っています。なぜなら、ゴリーによって、あなたはデッキを持って、手に上向きになっていて、それが一番上にあるからです!したがって、残りのカードに関しては、ハートの4つがBEGINNINGにあるとだけ言います。ですから、BEGINNINGのカードを聞いたら、「もちろんハートの4つ!」と言うでしょう。さて、あなたはカードがどこにあるかを私に「指さした」だけです。私たちの「DeckofCards」プログラミング言語では、次のように言うこともできます。
HeartCard card = 4; // 4 of hearts!
print &card // the address is BEGINNING!
次に、カードのデッキを裏返します。裏側が始まり、カードが何であるかわかりません。しかし、あなたは魔法に満ちているので、あなたが望むものを何でも作ることができるとしましょう。これを「DeckofCards」言語で実行しましょう。
HeartCard *pointerToCard = MakeMyCard( "10 of hearts" );
print pointerToCard // the value of this is BEGINNING!
print *pointerToCard // this will be 10 of hearts!
さて、MakeMyCard( "10 of hearts")はあなたが魔法を使っていて、BEGINNINGを指さしたいと思っていたので、カードを10 of heartにしました!カードを裏返して、出来上がり!さて、*はあなたを捨てるかもしれません。もしそうなら、これをチェックしてください:
HeartCard *pointerToCard = MakeMyCard( "10 of hearts" );
HeartCard card = 4; // 4 of hearts!
print *pointerToCard; // prints 10 of hearts
print pointerToCard; // prints BEGINNING
print card; // prints 4 of hearts
print &card; // prints END - the 4 of hearts used to be on top but we flipped over the deck!
クラスに関しては、例ではタイプをHeartCardとして定義することでクラスを使用しています。私たちはハートカードが何であるかを知っています...それは価値とハートのタイプを備えたカードです!そのため、これをハートカードとして分類しました。各言語には、必要なものを定義または「分類」する方法が似ていますが、すべて同じ概念を共有しています。これがお役に立てば幸いです...
クラスの場合、実際のオブジェクト指向プログラミングに飛び込むのに本当に役立った 3 つのテクニックがありました。
1 つ目は、クラスとオブジェクトを多用するゲーム プロジェクトに取り組んだことです。一般化 (ある種の関係または is-a 関係、例: 学生は一種の人物) と構成 (has-a 関係、 ex.学生は学生ローンを持っています)。このコードを分解するには多くの作業が必要でしたが、全体像が見えてきました。
2 番目に役立ったのは、システム分析のクラスで、http://www.agilemodeling.com/artifacts/classDiagram.htm">UML クラス図を作成する必要がありました。これらは、クラスの構造を理解するのに本当に役立ちました。プログラムで。
最後に、私は自分の大学の学生にプログラミングの家庭教師を手伝っています。これについて私が本当に言えることは、教えることと、問題に対する他の人々のアプローチを見ることによって、多くのことを学ぶということです. 多くの場合、学生は私が思いもよらなかったことを試みますが、通常は非常に理にかなっていて、自分のアイデアを実装するのに問題があるだけです.
私の最高のアドバイスは、多くの練習が必要であり、プログラミングすればするほど理解が深まるということです。
SOに関する同様の質問に対するlassevekの回答から:
ポインターは、多くの人にとって最初は混乱する可能性がある概念です。特に、ポインター値をコピーして同じメモリ ブロックを参照する場合は特にそうです。
私は、ポインタを家の住所が書かれた一枚の紙と見なし、ポインタが参照するメモリブロックを実際の家と見なすのが最も良い例えだと思いました。したがって、あらゆる種類の操作を簡単に説明できます。
- ポインタ値をコピーし、アドレスを新しい紙に書くだけです
- リンクされたリスト、次の家の住所が書かれた家の紙片
- 記憶を解き放ち、家を壊して住所を消す
- メモリリーク、紙を失くして家が見つからない
- メモリを解放するが、(現在は無効な) 参照を保持する、家を取り壊す、1 枚の紙を消去するが、古い住所が記載された別の紙を用意する、その住所に行っても家が見つからない、しかし、あなたは1つの廃墟に似たものを見つけるかもしれません
- バッファ オーバーラン、収まりきらないほど多くのものを家に移動し、隣の家にこぼれます。
私にヒントを与えてくれた本は、Donald AlcockのIllustrating Ansi Cでした。ポインター、ポインター演算、配列、文字列関数などを示す手描きスタイルのボックスとアロー ダイアグラムが満載です。
明らかに「C」の本ですが、コアのファンダメンタルズについては打ち負かすのは難しいです
これらのトピックについて私が読んだ最高の本は、BruceEckelによるThinkinginC++です。こちらから無料でダウンロードできます。
ポインタが配列アドレスであると偽ってください。
x = 500; // memory address for hello;
MEMORY[x] = "hello";
print MEMORY[x];
グラフィックが単純化しすぎていますが、ほとんどの場合、その番号が何であるかを知りたくない、または手動で設定したくない限り、問題はありません。
CIにいくつかのマクロがあることを理解したとき、それらがメモリ内の配列インデックスであるかのように、多かれ少なかれポインタを使用できるようになりました。しかし、私はそのコードを失ってからずっと、そして忘れてからずっと経ちました。
私はそれがで始まったことを思い出します
#define MEMORY 0;
#define MEMORYADDRESS( a ) *a;
それだけではほとんど役に立ちません。うまくいけば、他の誰かがそのロジックを拡張できます。
クラスは比較的簡単に把握できます。OOPには何年もかかる場合があります。個人的には、昨年まで真のOOPを完全に把握していませんでした。Smalltalkが大学で普及していないのは残念です。これは、OOPが、関数を備えた自己完結型のグローバル変数であるクラスではなく、メッセージを交換するオブジェクトに関するものであるという点を実感させます。
本当にクラスに慣れていない場合は、概念を理解するのに時間がかかることがあります。私が10年生で最初に彼らに出会ったとき、彼らが何をしているのかを知っている誰かがコードをステップスルーし、何が起こっているのかを説明するまで、私はそれを理解しませんでした。それを試してみることをお勧めします。
ポインターは魔法のようなものではなく、常に使用しています。
あなたが言う時:
int a;
コンパイラは「a」のストレージを生成します。実際には
、int を宣言していて、そのメモリ位置に「a」という名前を付けたいと言っています。
あなたが言う時:
int *a;
int のメモリ位置を保持できる変数を宣言しています。それはとても簡単です。また、ポインター演算を怖がらないでください。ポインターを扱うときは常に「メモリー マップ」を念頭に置いて、メモリー アドレスをたどるという観点から考えてください。
C++ のクラスは、抽象データ型を定義する 1 つの方法にすぎません。良い OOP の本を読んで概念を理解することをお勧めします。興味があれば、C++ コンパイラが OOP をシミュレートするコードを生成する方法を学んでください。しかし、C++ を十分長く使い続ければ、この知識はすぐに得られます :)
あなたの問題は、C++自体ではなく、C++のCコアにあるようです。Kernighan & Ritchie ( The C Programming Language ) を入手してください。吸い込む。これは非常に優れたものであり、これまでに書かれた最高のプログラミング言語の本の 1 つです。
私が実際に指針を得たのは、FatMac (1984 年頃) で TurboPascal をコーディングしたときでした。FatMac は当時の Mac のネイティブ言語でした。
Mac には奇妙なメモリ モデルがあり、アドレスが割り当てられると、メモリはヒープ上のポインタに格納されますが、その場所自体は保証されず、代わりにメモリ処理ルーチンがポインタへのポインタ (ハンドルと呼ばれる) を返しました。 . その結果、割り当てられたメモリの任意の部分にアクセスするには、ハンドルを 2 回逆参照する必要がありました。しばらく時間がかかりましたが、絶え間ない練習により、最終的にレッスンが家に帰りました。
Pascal のポインター処理は、構文が初心者に役立たない C++ よりも簡単に把握できます。C のポインタの理解に本当に行き詰まっている場合は、Pascal コンパイラのコピーを入手し、それに基本的なポインタ コードを書いてみることをお勧めします (Pascal は C に十分近いので、数時間で基本を理解できます)。 )。リンクされたリストなどは良い選択です。これらを C++ に戻し、概念をマスターすれば、崖はそれほど険しくはないように見えるでしょう。
これらの概念を理解するのに本当に役立ったことの 1 つは、UML (統一モデリング言語) を学ぶことです。オブジェクト指向設計の概念をグラフィカルな形式で見ることで、その意味を理解することができました。これらの概念を実装するソース コードを見て純粋にこれらの概念を理解しようとすると、理解するのが難しい場合があります。
継承のようなオブジェクト指向のパラダイムをグラフィカルな形式で見ることは、概念を理解するための非常に強力な方法です。
Martin Fowler のUML Distilledは、優れた簡潔な紹介です。
ポインターをよりよく理解するには、アセンブリ言語がポインターをどのように扱うかを調べると役立つと思います。ポインターの概念は、アセンブリ言語と x86 プロセッサ命令アーキテクチャの基本的な部分の 1 つです。たぶん、ポインターがプログラムの自然な部分であると感じるかもしれません。
クラスに関しては、OO パラダイムは別として、低レベルのバイナリーの観点からクラスを見るのは興味深いかもしれません。この点では、基本的なレベルではそれほど複雑ではありません。
C++ オブジェクト モデルの下にあるものをよりよく理解したい場合は、Inside the C++ Object Modelをお読みください。
クラスの場合:
私にとってブレイクスルーの瞬間は、インターフェイスについて学んだときでした。問題の解決方法の詳細を抽象化して、クラスと対話するメソッドのリストを提供するというアイデアは、非常に洞察に満ちていました。
実際、私の教授は、私たちのクラスを彼のテスト ハーネスにプラグインすることによって、プログラムを評価すると明言しました。格付けは、彼が私たちに与えた要件と、プログラムがクラッシュしたかどうかに基づいて行われます。
簡単に言えば、クラスを使用すると、機能をまとめて、よりクリーンな方法で呼び出すことができます (ほとんどの場合、常に例外があります)。
Bjarne Stroustrup のThe C++ Programming Languageを読みましたか? 彼は C++ を作成しました。
C++ FAQ Lite も良いです。
Joel によるこの記事は参考になるかもしれません。余談ですが、「しばらくの間 C++ で働いていて」、CS を卒業したことがある場合は、JavaSchool に通っている可能性があります (C++ でまったく働いたことがないと私は主張します。 C で作業していましたが、C++ コンパイラを使用しています)。
また、hojou と nsanders の回答に続いて、ポインターは C++ にとって非常に基本的なものです。ポインターを理解していないということは、C++ の基本を理解していないということです (ちなみに、この事実を認めることは、C++ を理解するための出発点です)。同様に、クラスを理解していない場合は、C++ (またはオブジェクト指向) の基本を理解していません。
ポインタについては、ボックスで描画するのも良い考えだと思いますが、アセンブリで作業するのも良い考えです。相対アドレッシングを使用する命令は、ポインターが何であるかをすぐに理解できるようになると思います。
クラス (およびより一般的なオブジェクト指向プログラミング) については、Stroustraps の「The C++ Programming Language」最新版をお勧めします。これは標準的な C++ の参考資料であるだけでなく、基本的なオブジェクト指向のクラス階層や継承から大規模システムの設計原則に至るまで、他の多くの事柄に関するかなりの量の資料を含んでいます。それは非常に良い読み物です(少し分厚くて簡潔ではないにしても)。