オブジェクト自体ではなくポインタを使用する必要があるのはなぜですか?


1602

私はJavaの出身で、C ++でオブジェクトの操作を開始しました。しかし、私に起こったことの1つは、人々はしばしばオブジェクト自体ではなくオブジェクトへのポインタを使用することです。たとえば、次の宣言:

Object *myObject = new Object;

のではなく:

Object myObject;

または、関数を使用する代わりにtestFunc()、次のようにしましょう:

myObject.testFunc();

私たちは書く必要があります:

myObject->testFunc();

しかし、なぜこのようにする必要があるのか​​理解できません。メモリアドレスに直接アクセスできるので、効率と速度に関係していると思います。私は正しいですか?


405
この慣習に従うだけでなく、この慣習に疑問を投げかけたことに対して、称賛を送ります。ほとんどの場合、ポインタは過剰に使用されます。
Luchian Grigore 14

120
ポインタを使用する理由が見当たらない場合は、見ないでください。オブジェクトを優先します。raw_pointsの前にshared_ptrの前にunique_ptrの前にオブジェクトを優先します。
ステファン2014

113
注:Javaでは、すべて(基本タイプを除く)はポインターです。したがって、反対の質問をする必要があります。なぜ単純なオブジェクトが必要なのですか。
Karoly Horvath 14

119
Javaでは、ポインタは構文によって隠されていることに注意してください。C ++では、ポインターと非ポインターの違いがコードで明示されます。Javaはどこでもポインタを使用します。
ダニエル・マルティン

216
近くに広すぎますか?マジ?このJava ++のプログラミング方法は非常に一般的であり、C ++コミュニティで最も重要な問題の1つであることに注意してください。それは真剣に扱われるべきです。
Manu343726 2014

回答:


1574

動的割り当てが頻繁に発生するのは非常に残念です。これは、悪質なC ++プログラマの数を示しています。

ある意味では、2つの質問が1つにまとめられています。1つ目は、いつ(を使用してnew)動的割り当てを使用する必要があるかです。2番目は、いつポインターを使用する必要があるかです。

重要な持ち帰りメッセージは、仕事に適切なツールを常に使用する必要があることです。ほとんどすべての状況で、手動の動的割り当てを実行したり、生のポインタを使用したりするよりも適切で安全な方法があります。

動的割り当て

あなたの質問では、オブジェクトを作成する2つの方法を示しました。主な違いは、オブジェクトの保存期間です。Object myObject;ブロック内で実行する場合、オブジェクトは自動ストレージ期間で作成されます。つまり、オブジェクトはスコープ外になると自動的に破棄されます。実行するnew Object()と、オブジェクトには動的な保存期間がありますdelete。つまり、明示的に指定するまでオブジェクトは存続します。動的ストレージ期間は、必要な場合にのみ使用してください。つまり、可能な場合は、常に自動保存期間を持つオブジェクトを作成することをお勧めします

動的割り当てが必要になる主な2つの状況:

  1. 現在のスコープよりも長く存続するには、オブジェクトのコピーではなく、その特定のメモリ位置にある特定のオブジェクトが必要です。オブジェクトのコピー/移動に問題がない場合(ほとんどの場合はそうする必要があります)、自動オブジェクトを使用することをお勧めします。
  2. 大量のメモリを割り当てる必要があり、スタックがいっぱいになる可能性があります。これは、C ++の範囲外にあるので、ほとんど気にする必要がなかったとしたらいいのですが(残念ながら、システムの現実に対処する必要があります)。私たちは開発中です。

動的割り当てが絶対に必要な場合は、それをスマートポインタまたはRAIIを実行する他のタイプ(標準コンテナなど)にカプセル化する必要があります。スマートポインターは、動的に割り当てられたオブジェクトの所有権セマンティクスを提供します。たとえば、std::unique_ptrとを見てくださいstd::shared_ptr。それらを適切に使用すれば、独自のメモリ管理を実行することをほぼ完全に回避できます(ゼロルールを参照)。

ポインタ

ただし、生のポインターには、動的割り当て以外にも他のより一般的な使用方法がありますが、ほとんどの場合、推奨される代替手段があります。以前と同様に、本当にポインタが必要な場合を除いて常に代替案を優先してください

  1. 参照セマンティクスが必要です。渡された関数がその特定のオブジェクト(そのコピーではなく)にアクセスできるようにするために、(割り当て方法に関係なく)ポインターを使用してオブジェクトを渡したい場合があります。ただし、ほとんどの状況では、ポインタへの参照型を優先する必要があります。これは、ポインタが特別に設計されているためです。これは、上記の状況1のように、必ずしもオブジェクトの存続期間を現在のスコープを超えて延長することについてではないことに注意してください。以前と同じように、オブジェクトのコピーを渡すことに問題がなければ、参照セマンティクスは必要ありません。

  2. 多態性が必要です。ポインターまたはオブジェクトへの参照を介して、関数をポリモーフィックに(つまり、オブジェクトの動的タイプに従って)呼び出すことができます。それが必要な動作である場合は、ポインタまたは参照を使用する必要があります。繰り返しになりますが、参照が優先されます。

  3. オブジェクトが省略されnullptrているときにを渡せるようにすることで、オブジェクトがオプションであることを表現したいとします。引数の場合は、デフォルトの引数または関数のオーバーロードを使用することをお勧めします。それ以外の場合は、この動作をカプセル化するタイプを使用することをお勧めしますstd::optional(C ++ 17で導入-以前のC ++標準ではを使用boost::optional)。

  4. コンパイル時間を改善するために、コンパイル単位を分離したい。ポインターの便利なプロパティは、ポイントされた型の前方宣言のみを必要とすることです(実際にオブジェクトを使用するには、定義が必要です)。これにより、コンパイルプロセスの一部を分離できるため、コンパイル時間が大幅に改善される可能性があります。Pimplイディオムを参照してください。

  5. CライブラリまたはCスタイルのライブラリとインターフェイスする必要があります。この時点で、未加工のポインターを使用する必要があります。あなたができる最善のことは、あなたの生のポインタを可能な限り最後に緩めることだけを確実にすることです。たとえば、getメンバー関数を使用して、スマートポインターから生のポインターを取得できます。ライブラリがハンドルを介して割り当てを解除することを期待している割り当てを実行する場合、オブジェクトを適切に割り当て解除するカスタムの削除機能を使用して、スマートポインターでハンドルをラップすることができます。


83
「現在のスコープを存続させるには、オブジェクトが必要です。」-これに関する追加の注記:現在のスコープを長持ちさせるにはオブジェクトが必要と思われる場合がありますが、実際にはそうではありません。たとえば、オブジェクトをベクター内に配置すると、オブジェクトはベクターにコピー(または移動)され、元のオブジェクトはそのスコープが終了したときに安全に破棄できます。

25
今では多くの場所でs / copy / move /になっていることを覚えておいてください。オブジェクトを返すことは間違いなく移動を意味しません。ポインタを介してオブジェクトにアクセスすることは、それが作成された方法に直交することにも注意する必要があります。
パピー

15
この回答では、RAIIへの明示的な言及がありません。C ++はすべて(ほぼすべて)のリソース管理であり、RAIIはC ++でそれを行う方法です(そして、生のポインターが生成する主な問題:Breaking RAII)
Manu343726

11
C ++ 11の前にはスマートポインタが存在していました。他のプロジェクトには独自の同等物があります。移動のセマンティクスを取得できず、std :: auto_ptrの割り当てに欠陥があるため、C ++ 11は物事を改善しますが、それでもアドバイスは優れています。(そして悲しいことに C ++ 11コンパイラにアクセスするだけでは十分ではありません。コードで動作させる可能性のあるすべてのコンパイラがC ++ 11をサポートしている必要があります。はい、Oracle Solaris Studio、私はあなたを見ています。)
armb

7
@ MDMoore313書くことができますObject myObject(param1, etc...)
user000001

173

ポインタには多くの使用例があります。

ポリモーフィック行動。多相型の場合、スライスを回避するためにポインター(または参照)が使用されます。

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

参照セマンティクスとコピーの回避。非ポリモーフィック型の場合、ポインター(または参照)は、潜在的に高価なオブジェクトのコピーを回避します

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

C ++ 11には、高価なオブジェクトの多数のコピーを関数の引数に、戻り値として回避できる移動セマンティクスがあることに注意してください。ただし、ポインターを使用すると、それらは確実に回避され、同じオブジェクト上で複数のポインターが許可されます(一方、オブジェクトは一度だけ移動できます)。

資源獲得new演算子を使用してリソースへのポインターを作成することは、最新のC ++のアンチパターンです。特殊リソースクラス(標準コンテナの1つ)またはスマートポインタstd::unique_ptr<>またはstd::shared_ptr<>)を使用します。考慮してください:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

生のポインタは「ビュー」としてのみ使用する必要があり、直接的な作成によるものであれ、戻り値による暗黙的なものであれ、所有権に関与するものではありません。C ++ FAQのこのQ&Aも参照してください

よりきめの細かいライフタイムコントロール共有ポインターが(たとえば、関数の引数として)コピーされるたびに、それが指すリソースは存続し続けます。通常のオブジェクト(new直接ではなく、またはリソースクラス内でによって作成されたものではない)は、スコープ外に出ると破棄されます。


17
「newオペレーターを使用してリソースへのポインターを作成することはアンチパターンです」生のポインターが何かを所有することをアンチパターンに拡張することもできると思います。作成だけでなく、所有権移転の私見を暗示引数や戻り値として生のポインタを渡すことで廃止されましたunique_ptr/移動セマンティクス
DYP

1
@dyp tnx、更新、およびこのトピックに関するC ++ FAQ Q&Aへの参照。
TemplateRex

4
あらゆる場所でスマートポインタを使用することは、アンチパターンです。適用できるいくつかの特殊なケースがありますが、ほとんどの場合、動的割り当て(任意の有効期間)を主張するのと同じ理由で、通常のスマートポインターのいずれかに対しても反対します。
James Kanze

2
@JamesKanze私は、スマートポインターが所有権のためだけにあらゆる場所で使用されるべきであり、生のポインターが所有権のためではなくビューのためだけに使用されるべきであることを示唆するつもりはありませんでした。
TemplateRex

2
@TemplateRex hun(b)コンパイルするまで間違った型を指定したことを知らなくても問題がなければ、署名の知識も必要とするため、少しばかげているように見えます。参照の問題は通常コンパイル時に捕捉されず、デバッグにさらに労力を要しますが、署名をチェックして引数が正しいことを確認している場合は、引数のいずれかが参照であるかどうかも確認できます。したがって、参照ビットは問題のないものになります(特に、選択した関数のシグネチャを表示するIDEまたはテキストエディタを使用する場合)。また、const&
JAB 2014

130

この質問には、前方宣言やポリモーフィズムなどの重要な使用例など、多くの優れた答えがありますが、質問の「魂」の一部、つまりJavaとC ++での異なる構文の意味が答えられていないように感じます。

2つの言語を比較して状況を調べてみましょう。

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

これに最も近いものは次のとおりです。

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

代替のC ++の方法を見てみましょう。

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

それを考える最良の方法は、多かれ少なかれJavaがオブジェクトへのポインタを(暗黙的に)処理する一方で、C ++はオブジェクトへのポインタまたはオブジェクト自体を処理することです。これには例外があります。たとえば、Javaの「プリミティブ」タイプを宣言した場合、それらはコピーされる実際の値であり、ポインタではありません。そう、

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

とは言っても、ポインターを使用することは、物事を処理するための正しい方法でも間違った方法でもありません。しかし、他の回答はそれを十分にカバーしています。ただし、一般的な考え方としては、C ++ではオブジェクトの存続期間とオブジェクトの生存場所をより細かく制御できるということです。

要点を説明します- Object * object = new Object()構造は実際には、典型的なJava(またはそのことについてはC#)セマンティクスに最も近いものです。


7
Object2 is now "dead":私はあなたがmyObject1より正確に意味していると思いますthe object pointed to by myObject1
クレメント2014年

2
確かに!少し言い換えました。
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();非常に悪いコードです。2番目のnewまたは2番目のObjectコンストラクターがスローする場合があり、object1がリークされます。rawを使用している場合は、RAIIラッパーでオブジェクトをできるだけ早くnewラップする必要がありますnew
PSkocik 2016

8
実際、これがプログラムであり、その周りで他に何も起こっていなかった場合です。ありがたいことに、これはC ++のポインターがどのように動作するかを示す単なる説明の断片であり、RAIIオブジェクトを生のポインターの代わりに使用できない数少ない場所の1つは、生のポインターについて学習しています...
Gerasimos R

80

ポインタを使用するもう1つの理由は、前方宣言のためです。十分に大きなプロジェクトでは、コンパイル時間を本当に高速化できます。


7
これは本当に役立つ情報の組み合わせに追加されているので、あなたがそれを答えにしてくれてうれしいです!
TemplateRex

3
std :: shared_ptrの<T>もT.の前方宣言で動作します(STD :: unique_ptrを<T> しない
Berkus氏

13
@berkus:のstd::unique_ptr<T>前方宣言で機能しTます。のデストラクタstd::unique_ptr<T>が呼び出されたときTに、完全な型であることを確認する必要があるだけです。これは通常std::unique_ptr<T>、ヘッダーファイルでデストラクタを宣言し、それをcppファイルで実装するクラスを意味します(実装が空の場合でも)。
David Stone、

モジュールはこれを修正しますか?
Trevor Hickey、2015年

@TrevorHickey古いコメント私は知っていますが、とにかくそれに答える。モジュールは依存関係を削除しませんが、依存関係を非常に安価に、パフォーマンスコストの点でほぼ無料で含める必要があります。また、モジュールからの一般的なスピードアップでコンパイル時間を許容範囲内に収めるのに十分であれば、それも問題ではなくなりました。
アイディアカピ2017年

79

序文

誇大広告とは対照的に、JavaはC ++のようなものではありません。Javaの誇大広告マシンは、JavaにはC ++に似た構文があるため、言語は類似していると信じ込ませたいと思います。真実から遠ざかることはできません。この誤った情報は、JavaプログラマーがC ++にアクセスして、コードの影響を理解せずにJavaのような構文を使用する理由の一部です。

先に行く

しかし、なぜこのようにする必要があるのか​​理解できません。メモリアドレスに直接アクセスできるので、効率と速度に関係していると思います。私は正しいですか?

それどころか、実際には。スタックはヒープに比べて非常にシンプルであるため、ヒープはスタックよりはるかに低速です。自動ストレージ変数(スタック変数とも呼ばれます)では、スコープから外れるとデストラクタが呼び出されます。例えば:

{
    std::string s;
}
// s is destroyed here

一方、動的に割り当てられたポインタを使用する場合は、そのデストラクタを手動で呼び出す必要があります。deleteこのデストラクタを呼び出します。

{
    std::string* s = new std::string;
}
delete s; // destructor called

これはnew、C#およびJavaで一般的な構文とは関係ありません。それらは完全に異なる目的で使用されます。

ダイナミックアロケーションのメリット

1.事前に配列のサイズを知っている必要はありません

多くのC ++プログラマーが遭遇する最初の問題の1つは、ユーザーからの任意の入力を受け入れるとき、スタック変数に固定サイズしか割り当てられないことです。配列のサイズも変更できません。例えば:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

もちろん、std::string代わりにを使用した場合は、std::string内部でサイズが変更されるため、問題ありません。しかし、本質的にこの問題の解決策は動的割り当てです。次のように、ユーザーの入力に基づいて動的メモリを割り当てることができます。

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

補足:多くの初心者が犯す1つの間違いは、可変長配列の使用です。これはGNU拡張機能であり、GCCの拡張機能の多くをミラーリングしているため、Clangの拡張機能でもあります。したがって、次の int arr[n]ものに依存するべきではありません。

ヒープはスタックよりもはるかに大きいため、スタックには制限がありますが、必要なだけのメモリを任意に割り当てたり再割り当てしたりできます。

2.配列はポインタではありません

これはあなたが求める利点ですか?配列とポインタの背後にある混乱/神話を理解すれば、答えは明らかになります。通常は同じであると想定されていますが、同じではありません。この神話は、ポインタは配列と同じように添え字を付けることができるという事実と、配列が関数宣言の最上位レベルでポインタに減衰するために発生します。ただし、配列がポインタに減衰すると、ポインタはそのsizeof情報を失います。したがってsizeof(pointer)、ポインタのサイズをバイト単位で示します。これは通常、64ビットシステムでは8バイトです。

配列に割り当てることはできません。初期化するだけです。例えば:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

一方、ポインタを使用すると、好きなことができます。残念ながら、ポインタと配列の違いはJavaとC#で手で振られるため、初心者は違いを理解できません。

3.ポリモーフィズム

JavaとC#には、たとえばasキーワードを使用して、オブジェクトを別のオブジェクトとして扱うことができる機能があります。したがって、誰かがEntityオブジェクトをオブジェクトとして扱いたい場合Playerは、Player player = Entity as Player;これを行うことができます。これは、特定のタイプにのみ適用される同種のコンテナで関数を呼び出す場合に非常に役立ちます。この機能は、以下の同様の方法で実現できます。

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

したがって、TrianglesだけにRotate関数がある場合、クラスのすべてのオブジェクトでそれを呼び出そうとすると、コンパイラエラーになります。を使用dynamic_castすると、asキーワードをシミュレートできます。明確にするために、キャストが失敗すると、無効なポインターが返されます。つまり!testtestがNULLであるか無効なポインタであるかを確認するための省略形です。つまり、キャストが失敗したことを意味します。

自動変数の利点

ダイナミックアロケーションが実行できるすべての優れた機能を確認した後、ダイナミックアロケーションを常に誰も使用しないのはなぜだろうと思っているでしょう。理由の1つは既に述べましたが、ヒープが遅いのです。そして、あなたがそのすべてのメモリを必要としないなら、あなたはそれを乱用すべきではありません。だから、ここに特定の順序ではないいくつかの欠点があります:

  • エラーが発生しやすいです。手動のメモリ割り当ては危険であり、リークが発生しやすくなります。デバッガまたはvalgrind(メモリリークツール)の使用に習熟していない場合は、髪の毛を頭から抜くことがあります。幸いなことに、RAIIのイディオムとスマートポインターはこれを少し軽減しますが、3つのルールや5つのルールなどの慣例に精通している必要があります。それは取り入れるべき多くの情報であり、知らないか気にしない初心者はこの罠に陥ります。

  • それは必要ない。newキーワードをどこでも使用するのが慣用的であるJavaおよびC#とは異なり、C ++では、必要な場合にのみ使用する必要があります。一般的な言い回しは、ハンマーを持っている場合、すべてが釘のように見えます。C ++で始まる初心者はポインタが怖いし、習慣によってスタック変数の使い方を学ぶのに対し、JavaとC#のプログラマは、ポインタを理解せずに使い始めます。それは文字通り間違った足で踏み出している。構文は1つであり、言語の学習は別のものであるため、知っていることはすべて放棄する必要があります。

1.(N)RVO-別名、(名前付き)戻り値の最適化

多くのコンパイラが行う最適化の1つは、省略戻り値の最適化と呼ばれるものです。これらにより、多くの要素を含むベクトルなど、非常に大きいオブジェクトに役立つ不要なコピーを取り除くことができます。通常、一般的な方法は、大きなオブジェクトをコピーして移動するのではなく、ポインタを使用して所有権転送することです。これにより、移動のセマンティクススマートポインターが導入されました

ポインターを使用している場合、(N)RVOは発生しません。最適化が心配な場合は、ポインターを返したり渡したりするよりも、(N)RVOを利用する方がより有益で、エラーが発生しにくくなります。関数の呼び出し元がdelete動的に割り当てられたオブジェクトなどを呼び出す責任がある場合、エラーリークが発生する可能性があります。ホットポテトのようにポインタが渡されている場合、オブジェクトの所有権を追跡するのは難しい場合があります。スタック変数は単純で優れているため、使用してください。


「したがって、!testは本質的に、testがNULLであるか無効なポインターであるかを確認するための省略形です。つまり、キャストが失敗したことを意味します。」この文は明確にするために書き直す必要があると思います。
バーカス

4
「Javaの誇大広告のマシンは、あなたが信じたい」 -多分1997年に、これは、今時代錯誤である、2014年にC ++へのJavaを比較するために、もはや動機はありません
マットR

15
古い質問ですが、コードセグメントでは{ std::string* s = new std::string; } delete s; // destructor called...確かにこれは機能しません。deleteコンパイラsがもう何を知っているのか分からないからでしょうか。
Badger5000 14年

2
私は-1を与えていませんが、書かれている冒頭の発言には同意しません。まず、「誇大広告」があることに同意しません。2000年頃だったかもしれませんが、現在は両方の言語がよく理解されています。次に、私はそれらが非常に似ていると主張します-C ++はSimulaと結婚したCの子であり、Javaは仮想マシン、ガベージコレクターを追加し、HEAVLYは機能を削減し、C#は不足している機能を合理化してJavaに再導入します。はい、これによりパターンと有効な使用法が大きく異なりますが、一般的なインフラストラクチャ/設計を理解して、違いを確認できるようにすることは有益です。
Gerasimos R

1
@James Matta:もちろんメモリはメモリであり、両方とも同じ物理メモリから割り当てられますが、スタック割り当てオブジェクトを使用してパフォーマンス特性を向上させることは非常に一般的であり、または少なくともその最高レベル-関数が出入りするときにキャッシュで「ホット」になる可能性が非常に高いが、ヒープにはそのような利点がないため、ヒープ内でポインターを追跡している場合、複数のキャッシュミスが発生する可能性があるあなたはおそらくスタックにいないでしょう。しかし、このすべての「ランダム性」は通常、スタックを優先します。
Gerasimos R

23

C ++では、オブジェクトを渡す方法が3つあります。ポインタによる方法、参照による方法、値による方法です。Javaは後者を制限します(唯一の例外は、int、booleanなどのプリミティブ型です)。奇妙なおもちゃではなくC ++を使用したい場合は、これら3つの方法の違いを理解する方がよいでしょう。

Javaは、「誰が、いつこれを破壊する必要があるのか​​」などの問題はないと偽装します。答えは次のとおりです。ガベージコレクター。それでも、メモリリークに対する100%の保護を提供することはできません(はい、Java メモリリークを引き起こす可能性があります)。実際、GCは誤った安心感を与えてくれます。SUVが大きいほど、避難所までの道のりは長くなります。

C ++では、オブジェクトのライフサイクル管理を直接行うことができます。まあ、それに対処する手段はありますが(スマートポインターファミリー、QtのQObjectなど)、GCのように「ファイアアンドフォーゲット」の方法で使用することはできません。常にメモリ処理に留意する必要があります。オブジェクトを破棄することに注意するだけでなく、同じオブジェクトを複数回破棄しないようにする必要もあります。

まだ怖くない?OK:循環参照-人間が自分で処理します。そして覚えておいてください:各オブジェクトを正確に1回強制終了してください。

さて、あなたの質問に戻りましょう。

ポインターや参照ではなく値でオブジェクトを渡す場合、オブジェクト(オブジェクト全体が、数バイトか巨大なデータベースダンプかを問わず、コピーしないでください)をコピーします。 t ??)「=」を実行するたびに。オブジェクトのメンバーにアクセスするには、「。」を使用します。(ドット)。

ポインタでオブジェクトを渡すと、数バイト(32ビットシステムでは4バイト、64ビットシステムでは8バイト)、つまりこのオブジェクトのアドレスがコピーされます。これを全員に表示するには、メンバーにアクセスするときにこの派手な「->」演算子を使用します。または、「*」と「。」の組み合わせを使用できます。

参照を使用すると、値になりすますポインタを取得します。これはポインタですが、「。」を使用してメンバーにアクセスします。

そして、もう一度あなたの心を吹き飛ばすために:あなたがカンマで区切られたいくつかの変数を宣言するとき、そして(手に注意してください):

  • タイプはみんなに与えられます
  • 値/ポインター/参照修飾子は個別です

例:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrは非推奨です。使用しないでください。
Neil

2
参照変数を含む初期化リストをコンストラクターに提供することなく、メンバーとして参照を持つことはできません。(参照がすぐに初期化されなければならないとしても、コンストラクタ本体は、それを設定することが遅すぎる、IIRC。。)
チャオ

20

C ++では、(Object object;ブロック内のステートメントを使用して)スタックに割り当てられたオブジェクトは、それらが宣言されているスコープ内にのみ存在します。コードのブロックの実行が完了すると、宣言されたオブジェクトは破棄されます。一方、を使用してヒープにメモリを割り当てる場合Object* obj = new Object()、それらはを呼び出すまでヒープ内に存在し続けますdelete obj

オブジェクトを宣言/割り当てたコードのブロックだけでなく使用したい場合は、オブジェクトをヒープ上に作成します。


6
Object obj常にスタックにあるとは限りません-たとえば、グローバルやメンバー変数。
テンフォー2014

2
@LightnessRacesinOrbitブロックで割り当てられたオブジェクトについてのみ言及し、グローバル変数やメンバー変数については言及しませんでした。物事ははっきりしていませんでしたが、修正されました-回答に「ブロック内」が追加されました。その偽りのない情報が今
欲しい

20

しかし、なぜこのように使用する必要があるのか​​わかりません。

以下を使用する場合、関数本体の内部でどのように機能するかを比較します。

Object myObject;

関数内では、 myObjectこの関数が戻るとは破壊されます。したがって、これは、関数の外でオブジェクトが必要ない場合に役立ちます。このオブジェクトは現在のスレッドスタックに配置されます。

関数本体の中に書くと:

 Object *myObject = new Object;

その場合myObject、関数が終了すると、が指すオブジェクトクラスインスタンスは破棄されず、割り当てはヒープ上にあります。

次に、Javaプログラマーの場合、2番目の例は、オブジェクト割り当てがJavaでどのように機能するかにより近いものです。この行:Object *myObject = new Object;java :と同等ですObject myObject = new Object();。違いは、java myObjectではガベージコレクションが行われ、c ++では解放されないため、どこかで明示的に「delete myObject;」を呼び出す必要があることです。そうしないと、メモリリークが発生します。

c ++ 11以降ではnew Object、shared_ptr / unique_ptrに値を格納することにより、動的割り当ての安全な方法を使用できます。

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

また、オブジェクトはマップやベクターなどのコンテナに保存されることが多く、オブジェクトの寿命を自動的に管理します。


1
then myObject will not get destroyed once function endsそれは絶対になります。
オービットのライトネスレース

6
ポインターの場合は、myObject他のローカル変数と同様に破棄されます。違いは、その値はオブジェクト自体へのポインタではなく、オブジェクトへのポインタであり、ダムポインタの破棄はその指示先に影響を与えないことです。したがって、オブジェクトは上記の破壊を生き延びます
cHao

もちろん、ローカル変数(ポインタを含む)はもちろん解放されます-それらはスタック上にあります。
marcinj 14

13

技術的にはメモリ割り当ての問題ですが、これにはさらに2つの実用的な側面があります。それは2つのことと関係があります:1)スコープ、ポインターなしでオブジェクトを定義すると、それが定義されているコードブロックの後でオブジェクトにアクセスできなくなりますが、「新規」でポインターを定義すると同じメモリで「削除」を呼び出すまで、このメモリへのポインタがあればどこからでもアクセスできます。2)引数を関数に渡したい場合、より効率的にするためにポインタまたは参照を渡したい。オブジェクトを渡すと、オブジェクトがコピーされます。これが大量のメモリを使用するオブジェクトである場合、CPUを消費する可能性があります(たとえば、データでいっぱいのベクトルをコピーします)。ポインタを渡す場合、渡すすべては1つのintです(実装によって異なりますが、それらのほとんどは1つのintです)。

それ以外は、「新しい」が、ある時点で解放する必要があるヒープにメモリを割り当てることを理解する必要があります。「新規」を使用する必要がない場合は、「スタック上」の通常のオブジェクト定義を使用することをお勧めします。


6

さて、主な質問は、なぜオブジェクト自体ではなくポインタを使用する必要があるのかということです。C ++には参照があるため、ポインタよりも安全であり、ポインタと同じパフォーマンスが保証されます。

あなたがあなたの質問で述べたもう一つのこと:

Object *myObject = new Object;

どのように機能しますか?それはObjectタイプのポインターを作成し、1つのオブジェクトに合うようにメモリを割り当て、デフォルトのコンストラクターを呼び出します、いいですね?しかし、実際にはあまり良くありません。動的にメモリを割り当てた場合(keywordを使用new)、メモリを手動で解放する必要があります。つまり、コード内に次のものが必要です。

delete myObject;

この呼び出しのデストラクタとメモリを解放し、一つのスレッドがメモリを解放したりしない場合は簡単に見えますが、しかし、大規模なプロジェクトでは検出が困難かもしれないが、その目的のためにあなたが試すことができます共有ポインタを、これらがわずかにパフォーマンスが低下しますが、それはとの仕事にはるかに簡単ですそれら。


そして今、いくつかの紹介は終わり、質問に戻ります。

オブジェクト間ではなくポインターを使用して、関数間でデータを転送する際のパフォーマンスを向上させることができます。

見てください、あなたは持っていますstd::string(それはオブジェクトでもあります)そしてそれは本当に大きなデータ、例えば大きなXMLを含んでいます、今それを解析する必要がありますが、そのためにあなたはvoid foo(...)さまざまな方法で宣言できる関数を持っています:

  1. void foo(std::string xml); この場合、変数から関数スタックにすべてのデータをコピーします。これには時間がかかるため、パフォーマンスが低下します。
  2. void foo(std::string* xml); この場合、size_t変数を渡すのと同じ速度でオブジェクトにポインターを渡しますが、NULLポインターまたは無効なポインターを渡すことができるため、この宣言はエラーが発生しやすくなります。C参照がないため、通常はで使用されるポインタ。
  3. void foo(std::string& xml); ここで参照を渡すと、基本的にはポインターを渡すのと同じですが、コンパイラーはいくつかの処理を行い、無効な参照を渡すことができません(実際には、無効な参照で状況を作成することは可能ですが、コンパイラーを騙しています)。
  4. void foo(const std::string* xml); これは2番目と同じですが、ポインタ値を変更することはできません。
  5. void foo(const std::string& xml); これは3番目と同じですが、オブジェクトの値は変更できません。

より多くの私が言及したいと思いますどのような、あなたは関係なく、あなたが(と選択している配分方法のデータを渡さないためにこれらの5つの方法を使用することができますnewまたは定期的)。


もう1つ、オブジェクトを通常の方法で作成すると、メモリがスタックに割り当てられますが、作成中にnewヒープが割り当てられます。スタックの割り当てははるかに高速ですが、データの非常に大きな配列の場合は少し小さいので、大きなオブジェクトが必要な場合はスタックオーバーフローが発生する可能性があるため、ヒープを使用する必要がありますが、通常、この問題はSTLコンテナーを使用して解決さ、覚えておいてくださいstd::stringコンテナでもあり、一部の人はそれを忘れていました:)


5

あなたがclass Aそれを含んでいるとしましょう外部のclass Bいくつかの関数を呼び出したいとき、あなたは単にこのクラスへのポインタを取得し、あなたは好きなことをすることができ、それはまたあなたの中でのコンテキストを変更しますclass Bclass Aclass Bclass A

ただし、動的オブジェクトには注意してください


5

オブジェクトへのポインタを使用することには多くの利点があります-

  1. 効率(既に指摘したとおり)。オブジェクトを関数に渡すことは、オブジェクトの新しいコピーを作成することを意味します。
  2. サードパーティのライブラリのオブジェクトを操作する。オブジェクトがサードパーティのコードに属していて、作成者がオブジェクトの使用をポインタのみで(コピーコンストラクタなどではなく)意図している場合、このオブジェクトを渡す唯一の方法はポインタを使用することです。値渡しは問題を引き起こす可能性があります。(深いコピー/浅いコピーの問題)。
  3. オブジェクトがリソースを所有していて、その所有権を他のオブジェクトと共有しないようにする場合。

3

これについては詳細に説明しましたが、Javaではすべてがポインタです。スタックとヒープの割り当ては区別されないため(すべてのオブジェクトはヒープに割り当てられます)、ポインターを使用していることに気づきません。C ++では、メモリ要件に応じて、2つを混在させることができます。C ++では、パフォーマンスとメモリ使用量がより確定的になります(duh)。


3
Object *myObject = new Object;

これを行うと、メモリリークを回避するために明示的に削除する必要がある(ヒープ上の)オブジェクトへの参照が作成されます。

Object myObject;

これを行うと、(スタック上に)自動タイプのオブジェクト(myObject)が作成され、オブジェクト(myObject)がスコープ外になると自動的に削除されます。


1

ポインタは、オブジェクトのメモリ位置を直接参照します。Javaにはこのようなものはありません。Javaには、ハッシュテーブルを介してオブジェクトの場所を参照する参照があります。これらの参照を使用して、Javaのポインター演算のようなことはできません。

あなたの質問に答えるために、それはあなたの好みです。私はJavaのような構文を使用することを好みます。


ハッシュテーブル?たぶん、いくつかのJVMにはありますが、それを当てにしてはいけません。
Zan Lynx 2014年

Javaに付属するJVMはどうですか?もちろん、ポインタを直接使用するJVM、またはポインタ計算を行うメソッドのようなものを実装できます。「風邪で人は死なない」と言って、「多分ほとんどの人はそうではないが、それを当てにしないでください!」という返答を得るようなものです。ははは。
RioRicoRick 2015年

2
ネイティブポインタとして@RioRicoRickのHotSpot実装するJavaの参照、参照docs.oracle.com/javase/7/docs/technotes/guides/vm/...私の知る限り見ることができるように、JRockitは同じことを行います。どちらもOOP圧縮をサポートしますが、どちらもハッシュテーブルを使用しません。パフォーマンスへの影響はおそらく悲惨です。また、「それはあなたの好みにすぎない」ということは、この2つは同等の動作に対して単に異なる構文であることを意味しているようですが、もちろんそうではありません。
Max Barraclough


0

ポインタで

  • 直接メモリと話すことができます。

  • ポインタを操作することで、プログラムの多くのメモリリークを防ぐことができます。


4
C ++では、ポインターを使用して、独自のプログラム用のカスタムガベージコレクターを作成できます」これはひどい考えのように聞こえます。
2016

0

ポインターを使用する理由の1つは、C関数とのインターフェースです。もう1つの理由は、メモリを節約することです。たとえば、多くのデータを含み、プロセッサを多用するコピーコンストラクターを持つオブジェクトを関数に渡す代わりに、オブジェクトにポインターを渡すだけで、特にループしている場合はメモリと速度を節約できます。 Cスタイルの配列を使用している場合を除き、その場合はリファレンスの方が適しています。


0

メモリ使用量が非常に限られている領域では、ポインタが便利です。たとえば、ミニマックスアルゴリズムを考えます。このアルゴリズムでは、再帰的なルーチンを使用して数千のノードが生成され、後でそれらを使用してゲームの次善の動きを評価します。(スマートポインターのように)割り当てを解除またはリセットする機能により、メモリ消費が大幅に削減されます。一方、非ポインター変数は、再帰呼び出しが値を返すまでスペースを占有し続けます。


0

ポインタの重要な使用例を1つ含めます。基本クラスにオブジェクトを格納しているが、それが多態的である可能性がある場合。

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

したがって、この場合、bObjを直接オブジェクトとして宣言することはできません。ポインタが必要です。


-5

"必要は発明の母。" 私が指摘したい最も重要な違いは、自分のコーディング経験の結果です。オブジェクトを関数に渡す必要がある場合があります。その場合、オブジェクトが非常に大きなクラスのものである場合、オブジェクトとしてオブジェクトを渡すと、その状態がコピーされます(これは望ましくない可能性があり、また。 4バイトサイズ(32ビットを想定)。他の理由はすでに上で述べられています...


14
参照渡しを優先する必要があります
bolov

2
std::string test;が持っている変数のように定数参照で渡すことをお勧めしますvoid func(const std::string &) {}が、関数が入力を変更する必要がない限り、ポインタの使用をお勧めします(コードを読んでいる人が気づき&、関数が入力を変更する可能性があることを理解できるようにするため)
Top-マスター

-7

すでに多くの優れた答えがありますが、一例を挙げましょう。

私は単純なItemクラスを持っています:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

それらの束を保持するベクトルを作成します。

std::vector<Item> inventory;

100万個のItemオブジェクトを作成し、ベクターにプッシュします。ベクトルを名前で並べ替えてから、特定のアイテム名に対して単純な反復バイナリ検索を行います。プログラムをテストすると、実行が完了するまでに8分以上かかります。次に、在庫ベクトルを次のように変更します。

std::vector<Item *> inventory;

...そして、新しい100万個のItemオブジェクトを作成します。コードに加えた唯一の変更は、最後にメモリをクリーンアップするために追加したループを除いて、Itemsへのポインターを使用することです。このプログラムは40秒未満で実行され、10倍の速度向上よりも優れています。編集:コードはhttp://pastebin.com/DK24SPeW にあります。コンパイラの最適化により、テストしたマシンで3.4倍の増加のみが示されています。


2
では、ポインタを比較していますか、それとも実際のオブジェクトを比較していますか?別のレベルの間接参照がパフォーマンスを向上させることができるかどうか、私は非常に疑っています。コードを入力してください!その後はきちんと片付けますか?
ステファン2015

1
@stefanソートと検索の両方でオブジェクトのデータ(具体的には、名前フィールド)を比較します。投稿で既に述べたように、私はきちんと片付けます。スピードアップはおそらく2つの要因によるものです:1)std :: vector push_back()はオブジェクトをコピーするので、ポインターバージョンはオブジェクトごとに単一のポインターをコピーするだけで済みます。コピーされるデータが減るだけでなく、ベクタークラスのメモリアロケーターのスラッシングが減るため、これはパフォーマンスに複数の影響を与えます。
ダレン

2
あなたの例で実際に違いがないことを示すコードは次のとおりです:並べ替え。ポインターコードは、並べ替えのみの非ポインターコードよりも6%高速ですが、全体的には非ポインターコードよりも10%低速です。ideone.com/G0c7zw
ステファン

3
キーワード:push_back。もちろんこれはコピーです。あなたはされている必要がありますemplace(あなたが他の場所でキャッシュするそれらを必要としない限り)あなたのオブジェクトを作成するときにその場でINGの。
underscore_d

1
ポインタのベクトルはほとんどの場合間違っています。警告と長所と短所を詳細に説明せずに推奨しないでください。あなたは悪いコード化された反例の唯一の結果である、プロの1を発見したように見える、そしてそれを偽っ
軌道上での明度レース
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.