序文
誇大広告とは対照的に、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
キーワードをシミュレートできます。明確にするために、キャストが失敗すると、無効なポインターが返されます。つまり!test
、test
がNULLであるか無効なポインタであるかを確認するための省略形です。つまり、キャストが失敗したことを意味します。
自動変数の利点
ダイナミックアロケーションが実行できるすべての優れた機能を確認した後、ダイナミックアロケーションを常に誰も使用しないのはなぜだろうと思っているでしょう。理由の1つは既に述べましたが、ヒープが遅いのです。そして、あなたがそのすべてのメモリを必要としないなら、あなたはそれを乱用すべきではありません。だから、ここに特定の順序ではないいくつかの欠点があります:
エラーが発生しやすいです。手動のメモリ割り当ては危険であり、リークが発生しやすくなります。デバッガまたはvalgrind
(メモリリークツール)の使用に習熟していない場合は、髪の毛を頭から抜くことがあります。幸いなことに、RAIIのイディオムとスマートポインターはこれを少し軽減しますが、3つのルールや5つのルールなどの慣例に精通している必要があります。それは取り入れるべき多くの情報であり、知らないか気にしない初心者はこの罠に陥ります。
それは必要ない。new
キーワードをどこでも使用するのが慣用的であるJavaおよびC#とは異なり、C ++では、必要な場合にのみ使用する必要があります。一般的な言い回しは、ハンマーを持っている場合、すべてが釘のように見えます。C ++で始まる初心者はポインタが怖いし、習慣によってスタック変数の使い方を学ぶのに対し、JavaとC#のプログラマは、ポインタを理解せずに使い始めます。それは文字通り間違った足で踏み出している。構文は1つであり、言語の学習は別のものであるため、知っていることはすべて放棄する必要があります。
1.(N)RVO-別名、(名前付き)戻り値の最適化
多くのコンパイラが行う最適化の1つは、省略と戻り値の最適化と呼ばれるものです。これらにより、多くの要素を含むベクトルなど、非常に大きいオブジェクトに役立つ不要なコピーを取り除くことができます。通常、一般的な方法は、大きなオブジェクトをコピーして移動するのではなく、ポインタを使用して所有権を転送することです。これにより、移動のセマンティクスとスマートポインターが導入されました。
ポインターを使用している場合、(N)RVOは発生しません。最適化が心配な場合は、ポインターを返したり渡したりするよりも、(N)RVOを利用する方がより有益で、エラーが発生しにくくなります。関数の呼び出し元がdelete
動的に割り当てられたオブジェクトなどを呼び出す責任がある場合、エラーリークが発生する可能性があります。ホットポテトのようにポインタが渡されている場合、オブジェクトの所有権を追跡するのは難しい場合があります。スタック変数は単純で優れているため、使用してください。