C または C++ でプログラムを作成し、メモリ管理、型チェック、またはバッファー オーバーラン保護などのマネージ言語の利点がなく、ポインター演算を使用している場合、プログラムが安全であることをどのように確認しますか? 多くの単体テストを使用しますか、それとも用心深いコーダーですか? 他の方法はありますか?
9 に答える
上記のすべて。私が使う:
- いろいろ注意
- 可能な限りスマートポインター
- テスト済みのデータ構造、多くの標準ライブラリ
- 単体テストはいつでも
- MemValidator や AppVerifier などのメモリ検証ツール
- 顧客サイトでクラッシュしないことを毎晩祈ってください。
実際、私は誇張しているだけです。コードを適切に構成すれば、リソースの制御を維持するのはそれほど難しくなく、実際にはそれほど難しくありません。
興味深いメモ。DCOM を使用し、マネージ モジュールとアンマネージ モジュールを持つ大規模なアプリケーションがあります。一般に、アンマネージ モジュールは開発中にデバッグするのが難しくなりますが、多くのテストが実行されるため、顧客のサイトでは非常にうまく機能します。マネージド モジュールは、ガベージ コレクターが非常に柔軟であるため、プログラマーがリソースの使用状況をチェックするのを怠ってしまうため、不適切なコードに悩まされることがあります。
私はたくさんのアサートを使用し、「デバッグ」バージョンと「リリース」バージョンの両方をビルドします。私のデバッグ バージョンは、すべてのチェックを行っても、リリース バージョンよりもはるかに遅く実行されます。
私はValgrindで頻繁に実行していますが、コードのメモリ リークはゼロです。ゼロ。バグのあるプログラムを取り上げてすべてのリークを修正するよりも、プログラムをリークのない状態に保つ方がはるかに簡単です。
また、追加の警告を表示するようにコンパイラを設定しているにもかかわらず、私のコードは警告なしでコンパイルされます。警告はばかげている場合もありますが、バグを指摘している場合もあるため、デバッガーで見つける必要なく修正します。
私は純粋な C を書いています (このプロジェクトでは C++ を使用できません) が、非常に一貫した方法で C を実行しています。コンストラクタとデストラクタを備えたオブジェクト指向のクラスがあります。手で呼び出す必要がありますが、一貫性が役立ちます。デストラクタを呼び出すのを忘れると、修正するまで Valgrind に頭を殴られます。
コンストラクタとデストラクタに加えて、オブジェクトを調べて正常かどうかを判断するセルフチェック関数を作成します。たとえば、ファイル ハンドルが null であるが、関連付けられたファイル データがゼロに設定されていない場合は、何らかのエラーが発生したことを示しています (ハンドルが壊れているか、ファイルが開かれていないのにオブジェクト内のフィールドにゴミが含まれている)。また、私のオブジェクトのほとんどには、特定の値 (それぞれの異なるオブジェクトに固有) に設定する必要がある「署名」フィールドがあります。オブジェクトを使用する関数は通常、オブジェクトが正常であることをアサートします。
メモリがあるときはいつでもmalloc()
、関数はメモリを0xDC
値で埋めます。完全に初期化されていない構造体は明らかです。カウントが大きすぎ、ポインターが無効であり ( 0xDCDCDCDC
)、デバッガーで構造体を見ると、初期化されていないことが明らかです。これは、 を呼び出すときにメモリをゼロで埋めるよりもはるかに優れていますmalloc()
。(もちろん、0xDC
フィルはデバッグ ビルドでのみ行われます。リリース ビルドでその時間を無駄にする必要はありません。)
メモリを解放するたびに、ポインターを消去します。そうすれば、メモリが解放された後にコードがポインターを使用しようとする愚かなバグがある場合、すぐにヌルポインター例外が発生し、バグを正しく指摘します。私のデストラクタ関数は、オブジェクトへのポインタを取らず、ポインタへのポインタを取り、オブジェクトを破棄した後にポインタを上書きします。また、デストラクタはオブジェクトを解放する前にワイプするため、コードの一部にポインタのコピーがあり、オブジェクトを使用しようとすると、サニティ チェック アサートが即座に起動します。
Valgrind は、コードがバッファの末尾を書き出すかどうかを教えてくれます。それがなければ、バッファの末尾に「カナリア」値を配置し、サニティ チェックでそれらをテストしていたでしょう。これらのカナリア値は、署名値と同様にデバッグ ビルド専用であるため、リリース バージョンではメモリが膨張しません。
私は単体テストのコレクションを持っており、コードに大きな変更を加えるとき、単体テストを実行することは非常に快適であり、物事をひどく壊すことはなかったという自信があります. もちろん、リリース バージョンだけでなくデバッグ バージョンでも単体テストを実行するので、すべてのアサートで問題を見つけることができます。
このすべての構造を所定の位置に配置するのは少し余分な労力でしたが、毎日報われます。また、デバッガーでバグを実行するのではなく、アサートが発生してバグを指摘すると、非常に満足しています。長い目で見れば、物事を常にきれいに保つのは簡単なことではありません。
最後に、私は実際にハンガリー語表記法が好きだと言わざるを得ません。私は数年前に Microsoft で働いていましたが、Joel のように Apps Hungarian を学びましたが、壊れたバリアントではありませんでした。本当に間違ったコードが間違っているように見えます。
同様に関連性があります-ファイルとソケットが閉じられていること、ロックが解放されていることをどのように確認しますか. メモリは唯一のリソースではなく、GC を使用すると、信頼性の高い/タイムリーな破棄が本質的に失われます。
GC も非 GC も自動的に優れているわけではありません。それぞれに利点があり、それぞれに価格があり、優れたプログラマーは両方に対処できる必要があります。
私はこの質問への回答で同じことを言いました。
私はC++を10年間使用しています。私はC、Perl、Lisp、Delphi、Visual Basic 6、C#、Java、および頭の中で思い出せない他のさまざまな言語を使用しました。
あなたの質問への答えは簡単です:あなたはC#/ Javaよりも、あなたが何をしているのかを知る必要があります。ジェフ・アトウッドの「Javaスクール」に関する暴言を生み出すのはそれだけではありません。
ある意味で、あなたの質問のほとんどは無意味です。あなたが提起する「問題」は、ハードウェアが実際にどのように機能するかという事実にすぎません。CPUとRAMをVHDL/Verilogで記述し、実際に単純化された場合でも、実際にどのように機能するかを確認してください。C#/ Javaの方法は、ハードウェアを介した抽象化ペーパーであることに気付くでしょう。
より簡単な課題は、最初の電源投入から組み込みシステムの基本オペレーティングシステムをプログラムすることです。知っておくべきことも表示されます。
(私はC#とJavaも書いています)
アンドリューの答えは良いものですが、リストに規律も追加します. C++ で十分な練習を重ねると、何が安全で、何がヴェロキラプトルに食べられそうになっているのか、かなり良い感触を得られることがわかりました。 安全な慣行に従うと快適に感じるコーディングスタイルを開発する傾向があり、たとえば、スマートポインターを生のポインターにキャストして別のものに渡そうとすると、ヘビージービーを感じます。
お店の電動工具のようなものだと思っています。正しく使用することを学び、常にすべての安全規則に従う限り、十分に安全です。怪我をするのは、安全ゴーグルを忘れてもいいと思ったときです。
私は C++ と C# の両方を行ってきましたが、マネージ コードに関する誇大広告はまったく見当たりません。
そうそう、メモリ用のガベージ コレクターがあります。これは役に立ちます... もちろん、C++ で単純な古いポインターを使用することを控えない限り、smart_pointers のみを使用する場合は、それほど問題はありません。
しかし、私は知りたいです...あなたのガベージコレクターはあなたを以下から保護しますか?
- データベース接続を開いたままにしますか?
- ファイルのロックを維持しますか?
- ...
リソース管理には、メモリ管理よりもはるかに多くの機能があります。C++ の良い点は、リソース管理と RAII が何を意味するかをすばやく学習できることです。
- ポインターが必要な場合は、auto_ptr、shared_ptr、またはweak_ptrが必要です
- DB接続が必要な場合は、オブジェクト「接続」が必要です
- ファイルを開く場合、オブジェクト「ファイル」が必要です
- ...
バッファ オーバーランに関しては、どこでも char* と size_t を使用しているわけではありません。「string」、「iostream」、そしてもちろん前述の vector::at メソッドと呼ばれるものがいくつかあり、これらの制約から解放されます。
テスト済みのライブラリ (stl、boost) は優れているため、それらを使用して、より機能的な問題に取り組みます。