開発者が犯す最も一般的なC++の間違いトップ10
公開: 2022-03-11C++開発者が遭遇する可能性のある多くの落とし穴があります。 これにより、高品質のプログラミングが非常に困難になり、メンテナンスに非常に費用がかかる可能性があります。 言語構文を学び、C#やJavaなどの同様の言語で優れたプログラミングスキルを身に付けるだけでは、C++の可能性を最大限に活用するのに十分ではありません。 C ++のエラーを回避するには、長年の経験と優れた規律が必要です。 この記事では、C ++開発に十分注意していない場合に、すべてのレベルの開発者が犯す一般的な間違いのいくつかを見ていきます。
よくある間違い#1:「new」と「delete」のペアを誤って使用する
どれだけ試しても、動的に割り当てられたすべてのメモリを解放することは非常に困難です。 それができたとしても、例外から安全ではないことがよくあります。 簡単な例を見てみましょう。
void SomeMethod() { ClassA *a = new ClassA; SomeOtherMethod(); // it can throw an exception delete a; }
例外がスローされた場合、「a」オブジェクトが削除されることはありません。 次の例は、それを行うためのより安全で短い方法を示しています。 C ++ 11で廃止されたauto_ptrを使用しますが、古い標準はまだ広く使用されています。 可能であれば、BoostのC++11unique_ptrまたはscoped_ptrに置き換えることができます。
void SomeMethod() { std::auto_ptr<ClassA> a(new ClassA); // deprecated, please check the text SomeOtherMethod(); // it can throw an exception }
何が起こっても、「a」オブジェクトを作成した後、プログラムの実行がスコープを終了するとすぐに削除されます。
ただし、これはこのC++問題の最も単純な例にすぎません。 削除を他の場所、おそらく外部関数や別のスレッドで行う必要がある場合の例はたくさんあります。 そのため、new / deleteをペアで使用することは完全に避け、代わりに適切なスマートポインタを使用する必要があります。
よくある間違い#2:忘れられた仮想デストラクタ
これは、派生クラス内に動的メモリが割り当てられている場合に、派生クラス内のメモリリークにつながる最も一般的なエラーの1つです。 仮想デストラクタが望ましくない場合、つまり、クラスが継承を目的としておらず、そのサイズとパフォーマンスが重要である場合があります。 仮想デストラクタまたはその他の仮想関数は、クラス構造内に追加のデータを導入します。つまり、クラスのインスタンスのサイズを大きくする仮想テーブルへのポインタです。
ただし、ほとんどの場合、クラスは本来意図されていなくても継承できます。 したがって、クラスが宣言されたときに仮想デストラクタを追加することは非常に良い習慣です。 それ以外の場合、パフォーマンス上の理由でクラスに仮想関数を含める必要がない場合は、クラスを継承しないことを示すコメントをクラス宣言ファイル内に配置することをお勧めします。 この問題を回避するための最良のオプションの1つは、クラスの作成中に仮想デストラクタの作成をサポートするIDEを使用することです。
この主題に対するもう1つのポイントは、標準ライブラリのクラス/テンプレートです。 これらは継承を目的としたものではなく、仮想デストラクタもありません。 たとえば、std :: stringからパブリックに継承する新しい拡張文字列クラスを作成した場合、誰かがそれをポインタまたはstd :: stringへの参照で誤って使用し、メモリリークを引き起こす可能性があります。
class MyString : public std::string { ~MyString() { // ... } }; int main() { std::string *s = new MyString(); delete s; // May not invoke the destructor defined in MyString }
このようなC++の問題を回避するために、標準ライブラリからクラス/テンプレートを再利用するより安全な方法は、プライベート継承またはコンポジションを使用することです。
よくある間違い#3:「削除」またはスマートポインターを使用したアレイの削除
多くの場合、動的サイズの一時配列を作成する必要があります。 それらが不要になった後は、割り当てられたメモリを解放することが重要です。 ここでの大きな問題は、C ++には[]括弧付きの特別な削除演算子が必要であり、これは非常に簡単に忘れられることです。 delete []演算子は、配列に割り当てられたメモリを削除するだけでなく、最初に配列からすべてのオブジェクトのデストラクタを呼び出します。 プリミティブ型にはデストラクタがない場合でも、プリミティブ型に[]ブラケットなしでdelete演算子を使用することも正しくありません。 配列へのポインタが配列の最初の要素を指すという保証はすべてのコンパイラにありません。そのため、[]角かっこなしで削除を使用すると、未定義の動作が発生する可能性もあります。
auto_ptr、unique_ptr <T>、shared_ptrなどのスマートポインターを配列で使用することも正しくありません。 このようなスマートポインタがスコープから出ると、[]括弧なしで削除演算子が呼び出され、上記と同じ問題が発生します。 配列にスマートポインターの使用が必要な場合は、Boostのscoped_arrayまたはshared_array、あるいはunique_ptr<T[]>スペシャライゼーションを使用できます。
参照カウントの機能が必要ない場合(ほとんどの場合、配列の場合)、最も洗練された方法は、代わりにSTLベクトルを使用することです。 それらはメモリの解放を処理するだけでなく、追加の機能も提供します。
よくある間違い#4:参照によるローカルオブジェクトの返送
これは主に初心者の間違いですが、この問題に悩まされているレガシーコードがたくさんあるので、言及する価値があります。 プログラマーが不必要なコピーを回避することによってある種の最適化を行いたいと思った次のコードを見てみましょう。
Complex& SumComplex(const Complex& a, const Complex& b) { Complex result; ….. return result; } Complex& sum = SumComplex(a, b);
オブジェクト「sum」は、ローカルオブジェクト「result」を指すようになります。 しかし、SumComplex関数が実行された後のオブジェクト「結果」はどこにありますか? どこにも。 それはスタック上にありましたが、関数が返された後、スタックはラップ解除され、関数からのすべてのローカルオブジェクトが破棄されました。 これにより、プリミティブ型の場合でも、最終的には未定義の動作が発生します。 パフォーマンスの問題を回避するために、戻り値の最適化を使用できる場合があります。
Complex SumComplex(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imaginar + b.imaginar); } Complex sum = SumComplex(a, b);
今日のほとんどのコンパイラでは、戻り行にオブジェクトのコンストラクタが含まれている場合、コードは不要なコピーをすべて回避するように最適化されます。コンストラクタは「sum」オブジェクトに対して直接実行されます。
よくある間違い#5:削除されたリソースへの参照の使用
これらのC++の問題は、想像以上に頻繁に発生し、通常はマルチスレッドアプリケーションで見られます。 次のコードを考えてみましょう。
スレッド1:
Connection& connection= connections.GetConnection(connectionId); // ...
スレッド2:
connections.DeleteConnection(connectionId); // …
スレッド1:
connection.send(data);
この例では、両方のスレッドが同じ接続IDを使用している場合、これにより未定義の動作が発生します。 多くの場合、アクセス違反エラーを見つけるのは非常に困難です。
このような場合、複数のスレッドが同じリソースにアクセスするとき、他のスレッドがリソースを削除する可能性があるため、リソースへのポインターまたは参照を保持することは非常に危険です。 Boostのshared_ptrなど、参照カウントでスマートポインタを使用する方がはるかに安全です。 参照カウンターを増減するためにアトミック操作を使用するため、スレッドセーフです。
よくある間違い#6:例外を許可してデストラクタを残す
デストラクタから例外をスローする必要はあまりありません。 それでも、それを行うためのより良い方法があります。 ただし、ほとんどの場合、デストラクタから明示的に例外がスローされることはありません。 オブジェクトの破棄をログに記録する単純なコマンドによって、例外がスローされる場合があります。 次のコードを考えてみましょう。
class A { public: A(){} ~A() { writeToLog(); // could cause an exception to be thrown } }; // … try { A a1; A a2; } catch (std::exception& e) { std::cout << "exception caught"; }
上記のコードでは、両方のオブジェクトの破棄中など、例外が2回発生した場合、catchステートメントは実行されません。 並行して2つの例外があるため、それらが同じタイプであるか異なるタイプであるかに関係なく、C ++ランタイム環境はそれを処理する方法を知らず、プログラムの実行を終了する終了関数を呼び出します。

したがって、一般的なルールは次のとおりです。例外がデストラクタを離れることを決して許可しないでください。 醜い場合でも、潜在的な例外は次のように保護する必要があります。
try { writeToLog(); // could cause an exception to be thrown } catch (...) {}
よくある間違い#7:「auto_ptr」の使用(誤って)
auto_ptrテンプレートは、いくつかの理由によりC++11から非推奨になりました。 ほとんどのプロジェクトはまだC++98で開発されているため、これはまだ広く使用されています。 これには、おそらくすべてのC ++開発者にはなじみのない特定の特性があり、注意しない人にとっては深刻な問題を引き起こす可能性があります。 auto_ptrオブジェクトをコピーすると、所有権が1つのオブジェクトから別のオブジェクトに譲渡されます。 たとえば、次のコードです。
auto_ptr<ClassA> a(new ClassA); // deprecated, please check the text auto_ptr<ClassA> b = a; a->SomeMethod(); // will result in access violation error
…アクセス違反エラーが発生します。 オブジェクト「b」のみにクラスAのオブジェクトへのポインタが含まれ、「a」は空になります。 オブジェクト「a」のクラスメンバーにアクセスしようとすると、アクセス違反エラーが発生します。 auto_ptrを誤って使用する方法はたくさんあります。 それらについて覚えておくべき4つの非常に重要なことは次のとおりです。
STLコンテナ内でauto_ptrを使用しないでください。 コンテナをコピーすると、ソースコンテナに無効なデータが残ります。 一部のSTLアルゴリズムは、「auto_ptr」の無効化にもつながる可能性があります。
auto_ptrを関数の引数として使用しないでください。コピーが発生し、関数の呼び出し後に引数に渡された値が無効のままになります。
クラスのデータメンバーにauto_ptrを使用する場合は、コピーコンストラクターと代入演算子の内部で適切なコピーを作成するか、これらの操作を非公開にして禁止してください。
可能な限り、auto_ptrの代わりに他の最新のスマートポインターを使用してください。
よくある間違い#8:無効化されたイテレータと参照の使用
この主題に関する本全体を書くことは可能だろう。 すべてのSTLコンテナには、イテレータと参照を無効にする特定の条件があります。 操作を使用するときは、これらの詳細に注意することが重要です。 以前のC++の問題と同様に、これはマルチスレッド環境でも非常に頻繁に発生する可能性があるため、同期メカニズムを使用して回避する必要があります。 例として、次のシーケンシャルコードを見てみましょう。
vector<string> v; v.push_back(“string1”); string& s1 = v[0]; // assign a reference to the 1st element vector<string>::iterator iter = v.begin(); // assign an iterator to the 1st element v.push_back(“string2”); cout << s1; // access to a reference of the 1st element cout << *iter; // access to an iterator of the 1st element
論理的な観点からは、コードは完全に問題ないようです。 ただし、2番目の要素をベクトルに追加すると、ベクトルのメモリが再割り当てされ、イテレータと参照の両方が無効になり、最後の2行でそれらにアクセスしようとするとアクセス違反エラーが発生する可能性があります。
よくある間違い#9:値によるオブジェクトの受け渡し
パフォーマンスに影響するため、オブジェクトを値で渡すのは悪い考えであることをおそらくご存知でしょう。 多くの人は、余分な文字を入力しないようにするために、またはおそらく後で最適化を行うために戻ることを考えるために、そのままにしておきます。 通常、これは実行されないため、パフォーマンスの低いコードや予期しない動作が発生しやすいコードになります。
class A { public: virtual std::string GetName() const {return "A";} … }; class B: public A { public: virtual std::string GetName() const {return "B";} ... }; void func1(A a) { std::string name = a.GetName(); ... } B b; func1(b);
このコードはコンパイルされます。 「func1」関数を呼び出すと、オブジェクト「b」の部分的なコピーが作成されます。つまり、オブジェクト「b」のクラス「A」の部分のみがオブジェクト「a」にコピーされます(「スライスの問題」)。 したがって、関数内では、クラス「B」のメソッドではなく、クラス「A」のメソッドも呼び出します。これは、関数を呼び出す人が期待するものではない可能性があります。
例外をキャッチしようとすると、同様の問題が発生します。 例えば:
class ExceptionA: public std::exception; class ExceptionB: public ExceptionA; try { func2(); // can throw an ExceptionB exception } catch (ExceptionA ex) { writeToLog(ex.GetDescription()); throw; }
関数「func2」からタイプExceptionBの例外がスローされると、catchブロックによってキャッチされますが、スライスの問題のため、ExceptionAクラスの一部のみがコピーされ、誤ったメソッドが呼び出され、再スローされます。外部のtry-catchブロックに誤った例外をスローします。
要約すると、オブジェクトは常に値ではなく参照によって渡されます。
よくある間違い#10:コンストラクターと変換演算子によるユーザー定義の変換の使用
ユーザー定義の変換でさえ非常に役立つ場合がありますが、予測できない変換につながる可能性があり、見つけるのが非常に困難です。 誰かが文字列クラスを持つライブラリを作成したとしましょう:
class String { public: String(int n); String(const char *s); …. }
最初のメソッドは長さnの文字列を作成することを目的としており、2番目のメソッドは指定された文字を含む文字列を作成することを目的としています。 しかし、次のようなものがあるとすぐに問題が始まります。
String s1 = 123; String s2 = 'abc';
上記の例では、s1は、文字「123」を含む文字列ではなく、サイズ123の文字列になります。 2番目の例には、二重引用符の代わりに一重引用符が含まれています(これは偶然に発生する可能性があります)。これにより、最初のコンストラクターが呼び出され、非常に大きなサイズの文字列が作成されます。 これらは本当に単純な例であり、混乱や予測できない変換につながる、見つけるのが非常に難しい、より複雑なケースがたくさんあります。 このような問題を回避する方法には、2つの一般的なルールがあります。
暗黙的な変換を禁止する明示的なキーワードを使用してコンストラクターを定義します。
変換演算子を使用する代わりに、明示的な会話メソッドを使用してください。 もう少し入力が必要ですが、読みやすく、予測できない結果を回避するのに役立ちます。
結論
C++は強力な言語です。 実際、コンピュータで毎日使用し、愛されるようになったアプリケーションの多くは、おそらくC++を使用して構築されています。 言語として、C ++は、オブジェクト指向プログラミング言語に見られる最も洗練された機能のいくつかを通じて、開発者に非常に大きな柔軟性を提供します。 ただし、これらの高度な機能や柔軟性は、責任を持って使用しないと、多くの開発者にとって混乱やフラストレーションの原因となることがよくあります。 このリストが、これらの一般的な間違いのいくつかがC++で達成できることにどのように影響するかを理解するのに役立つことを願っています。
Toptal Engineeringブログでさらに読む:
- CおよびC++言語を学ぶ方法:究極のリスト
- C#とC ++:コアとは何ですか?