この質問は奇妙に聞こえるかもしれませんが、私はC++をすべて自分で学んでいます。メンタリングを依頼できる人がいないので、アドバイスをいただければ幸いです。
私は最近、C++でプログラミングを始めました(約3〜4か月、毎日約6〜8時間)。私の経歴はJavaで、私はJavaで10k以上のLOCを使用していくつかの大きなプロジェクトを実行しました(これは私のような大学生向けです)。
私は主にアルゴリズムの実装と視覚化にC++を使用しましたが、より大きなソフトウェアプロジェクトも目指しています。私が使用した唯一のライブラリは、Catch2、OpenCV、および少しのBoost用です。私のプログラミングスタイルの奇妙な点は、私の旅でポインターを使用したことがないということです。ポインタの使い方がわからないわけではありませんが、ポインタが役立つと思う瞬間は見当たりません。プリミティブデータを保存する必要があるときは、std::vector
配列の上。クラスのオブジェクトを使用する必要がある場合は、スタック上にオブジェクトを作成し、参照で渡します。新規/削除なし、スマートポインターなし。
私がこの(奇妙な)質問をするのは、C++プログラミングの大きな領域を見落としているように感じるからです。あなたの経験を教えてください、そして私にいくつかのヒントを教えてください。
あなたのように、可能な限りオブジェクトのコピーと参照を使用して、ポインタを回避することをお勧めします。この練習を続けてください。
ただし、細心の注意を払わないと、かなりうまくいかないことがいくつかあります。
return
の直後に破棄され、参照の使用はUBになります。参照をオブジェクトに挿入するときにも同じ種類の問題が発生します。それはカチカチ音をたてる爆弾です。vector
関数メンバーを持つクラスのvirtual
がある場合、 が原因で、コードが思ったように機能しない可能性がありますスライス 。これらは 非常に厄介なバグ であり、JavaバックグラウンドでC++を初めて使用する場合によくある間違いです。factory method パターンなど、ポインタなしでは実現できない非常に一般的なOOデザインパターンもあります。
ポインタを回避すること自体が目的であってはなりません。ビジュアライゼーションを使用している場合は、ポリモーフィズムがあなたの友人かもしれません。そして、ここでポインターは状況を解き放ちます。良い知らせは、最近のスマートポインターがメモリを安全に管理できることです。
そう、あなたの練習はとてもうまくいくかもしれません。しかし、気付かないバグが含まれている可能性があります。そして、遅かれ早かれ、あなたは非常に便利な機能を見逃すでしょう。
C++プログラミングで「一般的な意味での間接参照」を使用しています。何も問題はありません。これは、ポインタと参照(C++言語)を使用したプログラミングとほぼ同じくらい強力です。
ただし、vector
の使用方法には1つの技術的な落とし穴があります。
ソフトウェアの安全のために、最初にその技術的な落とし穴について説明します。主な質問に答えてもらうよりも、その落とし穴について知ることが重要です。
オブジェクトがベクターに配置されると、ベクターにはオブジェクトのコピーが含まれます。これらのオブジェクトにはアドレスがあり、そこから参照を作成できます(C++の意味で)。これらのアドレスは、次にベクトルを再割り当て(増加、クリア)するか、アイテムをシフトさせる(たとえば、中央のアイテムを削除する)まで安定しています。無効になったアドレス(ポインター無効化と呼ばれる)を使い続けると、「未定義の動作」(UB)がトリガーされる可能性があります。 UBが発生すると、何でも従うことができます-後続の操作の正確さはもはや保証されません。
C++コレクション内の個々のアイテムを安全に割り当て(追加)および割り当て解除(削除)できるようにするには、これらのアイテムをヒープに割り当てる必要があります。
ここで、「ヒープ」とは実際のヒープ、つまりC++の動的メモリ割り当てシステムを指します。動的メモリ割り当てシステムにより、個々の割り当てを要求して放棄することができます。
vector
sを事前に事前に割り当てることができるプログラミングスタイルは、静的メモリ割り当てスタイル(セマンティックレベル)に匹敵します。
静的メモリ割り当てスタイルは数十年前の標準であり、パワーチェーンおよび流体部品用の車両電子制御システム、特定の航空機アビオニクスシステム、および兵器システムなどの特定の安全上重要なシステムには依然として義務付けられています。ただし、静的メモリ割り当てスタイルは実装できるものに制限があるため、動的メモリ割り当てスタイルほど強力ではありません。つまり、明確に定義された、拡張不可能なスコープ(たとえば、機械システムの特定の側面を制御し、他のものとのインターフェースを必要としないもの)を持つアプリケーションの場合、静的メモリ割り当てスタイルを維持することが可能です。
あなたが言及した例のいくつかは "handle body idiom"(link) の例です。
C++では、ハンドルクラスにより、ユーザーは通常C++コピーセマンティクスを使用して、C++のポインターと参照に似たものを実現できます。
OpenCVのMat
クラスは「ハンドル」クラスです。これを説明するために、次のコードスニペットを検討してください。
cv::Mat matOne(cv::Size(640, 480), CV_8UC3);
cv::Mat matTwo = matOne;
これらの2行のコードの後、matTwo
とmatOne
はどちらも同じオブジェクト(行列またはイメージ)を参照します。これは、cv::Mat
クラスの設計と実装の詳細によるものです。
同様に動作するクラスを実装する場合は、C++のポインターと参照、つまり、興味があり欠けていると感じている知識について学ぶ必要があります。
「セマンティクス」という言葉の「言語学」について少し。
C++では、「コピーセマンティクス」および「参照セマンティクス」という語句は、どちらもC++構文の側面とその使用法を指します。したがって、英語の標準で判断すると、「セマンティクス」という単語の使用は誤称です。
参照とstd :: vecの間では、Cプログラムがポインターを使用する多くの場合、C++ではポインターを使用しません。 Swiftでは、「ポインタ」と呼ばれるものを見つけるために、標準ライブラリを深く掘り下げる必要があります。
ポインタをまったく使用しないのは少し珍しいことですが、かなり可能です。
クラスのオブジェクトを使用する必要がある場合は、スタック上にオブジェクトを作成し、それを参照で渡します。新規/削除なし、スマートポインターなし。
実際に誰も言及していないことに驚いています。 8MBを超えるスタックを使用できることはまれです(LinuxのデフォルトはAFAIK)。 1 MBしか利用できないと想定しても安全です(WindowsのデフォルトはAFAIK)。
とにかく、スタックの代わりにヒープを使用する主な理由は2つあります。
また、あなたは何かを誤解しているようです:ベクトルは不正行為です!ベクトルは内部でヒープを割り当て、管理し、割り当てを解除するためです。これは、生のポインタの非常に薄いラッパーであり、最大の違いは、ベクトルにサイズがあることです。それは一種のスマートポインタですよね?
Christoph's answer に加えて、ポインタなしでは簡単に構築および使用できないいくつかのデータ構造もあります。
オブジェクトの木
DAG
リンクされたリスト(委任チェーンなど)
もちろん、これらにポインタを使用することで回避できます。ツリーとDAGは、std::vector<>
に格納されたオブジェクトから構築でき、それらをそのstd::vector<>
内のそれぞれのインデックスでリンクします。リンクされたリストの場合は、std::list<>
を使用するだけです。後者は内部でポインターを使用し、前者は明示的なポインターを、まったく同じ機能を果たし、同じ問題を抱えるint
に置き換えます。内部では、ポインタはメモリ内のいくつかのバイトをアドレス指定するために使用される整数値にすぎないため、このようなインデックスの使用は実際にはvoid
ポインタの使用と同じくらい悪いものです。所有者を管理する必要がある場合は、型付きポインター、スマートポインターを使用する方が適切です。
このようなデータ構造、つまりポインタがどれだけ必要かは、ユースケースに大きく依存します。ですから、真の委任チェーンを必要とする問題を抱えたことはないでしょう。たとえば、物理シミュレーションを実行する場合、次のタイムステップを計算できる3D配列が必要です。派手なDAGは含まれません。ただし、分散バージョン管理ソフトウェアをプログラミングしている場合は、絶対にポインターを使用する必要があります。DAGからのコミットでは、それぞれが1つ以上の親コミットを参照し、ブランチはコミットを参照します。
結局、ポインタは単なるツールにすぎません。特定の目的のためのツール。問題がこのツールを必要とすることもあれば、別のツールを必要とすることもあります。前者の場合にポインタを使用しないことは、後者の場合に使用することは間違っているため、同様に間違っています。
C++の元の設計者であるBjarne Stroustrupが承認しました! 彼のCPPコアガイドライン で、彼はすべてのC++プログラマーに「スコープオブジェクトを優先し、不必要にヒープを割り当てないでください」とアドバイスしています。彼はリソースを管理するためにRAIIを使用することを推奨し、参照は非所有であるべきであると述べています。スタック上で関数スコープ内にローカルオブジェクトを作成し、それらを参照によって他の関数に渡す場合、すでにガイドラインに従っています!
ポインターを使用しないとC++で実行できない重要なことがいくつかあります。他の人がいくつか言及しています。コピー省略は、関数からオブジェクトを返す必要がある場合のほとんどをカバーしますが、すべてではなく、所有権を転送したい場合もカバーしません。 &Base supportsTheInterface
などの関数パラメーターを渡すことで、ポリモーフィズムの多くの用途を取得できますが、ポリモーフィックデータ構造を作成するには、ある種のポインターが必要です。ほとんどの大規模なプログラムでは、データを共有できるようにする必要があります。そして、おそらくポインタを使用するコードを維持またはインターフェイスするように求められます。まだ他の誰も取り上げていない、もう1つの使用例:スマートポインターは、オブジェクト間でデータをコピーするよりも効率的にデータを移動するための非常に便利な方法です。
this
ポインタを暗黙的に使用しているに違いありません。 ;-)
真剣に、C++は大きな言語であり、ほとんどのプログラムはそれをすべて使用しません。 Sean Parentは、C++プログラマは 生のループを作成しないでください で、標準ライブラリが提供するアルゴリズムの使用方法を学ぶべきだと示唆するやや有名な講演をしました。多くの人にとって、それはポインタを使用しないよりも極端に見えるかもしれません。
C++には、ジョブごとに異なるツールがあります。テンプレートメタプログラミング、継承、ラムダなどのツールは、可能な場合だけでなく、必要な場合にのみ使用してください。必要がない場合は、ポインターを使用しないでください。
特にベアメタルポインターでは、必要のないときにポインターを使用することがよくあります。 <vector>
などのライブラリに生のポインタと動的な割り当てを処理させることは、非常に賢明です。ある意味で、まだポインターを使用していることに注意してください。低レベルの詳細を管理するために他のC++プログラマーによって書かれたコードに依存しているだけで、より高いレベルの抽象化に集中できることに注意してください。
ポインタは自然であり、グラフ内のノードをリンクするなど、特定の種類のソリューションに適しています。一部のソリューションでは、ポリモーフィックオブジェクトを動的に割り当てるなど、それらは不可欠です。ほとんどの場合、std::vector
などのスマートポインターを使用して詳細を管理するクラスにすぎないスマートポインターに依存することで、低レベルの詳細を回避できます。
ある時点で、独自の抽象化クラス(専用のコンテナーまたは新しい種類のスマートポインター)を記述する必要がある場合があります。 Sean Parentでさえ、新しいアルゴリズムを実装するにはrawループが必要になる可能性があるが、それをカプセル化する必要があることを認めました。
ポインタ依存の抽象化を作成する必要がある場合(または、他の人のコードを理解したり、ポインタを交換したいライブラリとのインターフェースを作成したりする必要がある場合)、ポインタに関する十分な経験が必要です。あなたはその仕事に備えています。
ポインター(アドレス、ハンドル、または参照を装って)は、多くのプログラミング言語にとってかなり基本的な概念です。それらが適切なツールである場合、それらを回避するためにコードをゆがめることによってそれらについて学ぶ機会をお見逃しなく。
プリミティブデータを格納する必要がある場合は、配列よりも_
std::vector
_を使用します。クラスのオブジェクトを使用する必要がある場合は、スタック上にオブジェクトを作成し、参照で渡します。new
/delete
なし、スマートポインタなし。
ほとんどのC++開発者は、これらすべての点であなたに同意します。適切なコンテナはプリミティブ配列よりも安全です。ヒープに何かを割り当てることを回避できる場合は、そうする必要があります。
ただし、ポインタが必要な場合もあります。コードでそれらを回避できる場合、それは素晴らしいことですが、大規模なプロジェクトでは回避するのがより困難になります。
スコープ押し出し
これは、かなり単純な概念のファンシーな名前です。関数のscopeは、その関数の本体であり、関数が呼び出すすべての本体、つまりthey呼び出しなど。関数でローカル変数を宣言すると、その関数が終了するまでローカル変数が存続することがわかります。そのため、それを(参照により)その関数内の他の関数に自由に渡すことができ、私が安全なことをしていることを知っています。
しかし、関数内にオブジェクトを作成し、それをどこかに保存し、関数が終了した後も永続化させたい場合はどうなりますか? Javaを作成している場合、このようなもの(たとえば、ファクトリクラス)を作成することについては、二度と考えません。
_public BigObject createBigObject()
{
BigObject bigObject = new BigObject(param1, param2);
return bigObject;
}
_
createBigObject()
が実行された後でも、変数bigObject
がまだ生きていることが望まれます。 Javaはガベージコレクション言語なので、これで問題ありません。
C++で、書こうとした場合
_BigObject& createBigObject() const
{
BigObject bigObject(param1, param2);
return bigObject;
}
_
プログラムの動作は未定義です。せいぜい、コンパイラはローカル変数への参照を返すことを警告します。コードを実行しようとすると、bigObject
の存続期間はcreateBigObject
が戻ると終了するため、コードが機能することもあれば、クラッシュすることもあります。 new
またはスマートポインタを使用して、動的割り当てを使用する必要があります。例えば。:
_BigObject* createBigObject() const
{
BigObject* const bigObject = new BigObject(param1, param2);
return bigObject;
}
_
必要に応じて、代わりに_return *bigObject
_を使用して、ここで_BigObject&
_を返すことができます。
変更可能な参照
C++での参照はimmutableです。作成時に他のオブジェクトを参照するように初期化する必要があり、他のオブジェクトを指すことはできません。その点で、_T&
_は_T* const
_に少し似ています(_T const&
_は_T const* const
_に少し似ています)。ここで、クラスを設定していて、そのクラスのメンバー変数として他のオブジェクトへの参照を保持したいとします。クラスを構築するときに他のオブジェクトがすべて設定されている場合、これは問題ありません-参照をすぐに初期化できます-しかし、初期化手順の後半で設定する必要がある場合は、ポインターを使用する必要があります。オブジェクトが作成された後でのみ、オブジェクトに向けることができます。
ポリモーフィズム(一種)
Javaでプログラミングしたことがあれば、interface
sを多用したことでしょう。 C++では、同じことをしたい場合、通常はポインターを使用する必要があります(代わりに-templatesを代わりに使用したい場合があります)。 Javaでは、
_List<String> list = new ArrayList<String>();
_
list
がある種のリストであることのみを気にすることを示すために、それがArrayList
であるという事実は実装の詳細です。 C++では、
_List<std::string> list = ArrayList<std::string>();
_
(List
とArrayList
がコード内に型として存在すると仮定すると)はList
を作成し、それをArrayList
と等しく設定しようとします。 List
に純粋な仮想メンバーが含まれていると、コンパイルエラーが発生します。それ以降は、List
オブジェクトを作成することが不可能になります。 list
は、_List&
_参照または_List*
_ポインターのいずれかでなければなりません。
これで、(この例のように)参照を使用してポリモーフィズムを実行できる場合がありますが、そのような場合は通常、テンプレートを使用してコンパイル時のポリモーフィズムを実行する方が得策です。 C++では、通常、ポリモーフィックオブジェクトをいくつかのコンテナー(_std::vector
_など)に格納するときに、ランタイムポリモーフィズムを使用します。そして、これは前のポイントに戻ります。参照は作成時に初期化する必要があるため、参照をその場で保存することは不可能です。したがって、それらをポインタとして格納する必要があります。
ポインタは低レベルのプログラミングメカニズムです。プログラミングの「要点」必要な引用 下位レベルのメカニズムから上位レベルのメカニズムを作成することです。一般に、下位レベルはより強力であるが使いにくいことを意味し、上位レベルはより強力ではないが使いやすいことを意味します。
したがって、おそらく、一般的に、ポインタは低レベルのコードでは意味があり、高レベルのコードでは意味がありません。
いいえ、あなたは何も悪いことをしていません。 ポインタ(参照ではなく、参照カウンタではなく、一意のラッパーではありません)は、特定のメモリアドレス空間内の値の場所をモデル化するために使用される概念です。そう:
間違って定義します。コードが正しく実行されれば、単純化した意味で問題はありません。
ただし、ポインタをまったく使用しないと、多くの潜在的な問題が発生します。
1)ポインタを使用しないと、ポインタをよく理解できません。次に、ポインタを使用する誰かによって書かれたコードを理解したり、コードとやり取りしたりする必要がある場合はどうしますか?
2)ポインタは(私見-個人の意見は異なる場合があります)多くの場合、使用している代替案よりもはるかに理解しやすく、コーディングも簡単です。
3)ポインターは、多くの場合(もちろん常にではありませんが)これらの代替手段よりも効率的である可能性があるため、パフォーマンスが重要なコードを記述することが目標である場合は、自分自身に障害があります。