要約:
Cの関数は、NULL
ポインターを逆参照していないことを常に確認する必要がありますか?そうでない場合、これらのチェックをスキップするのが適切なのはいつですか?
詳細:
プログラミングインタビューに関する本を何冊か読んでいますが、Cの関数引数の適切な入力検証の程度は何なのでしょうか。明らかに、ユーザーから入力を受け取るすべての関数は、逆参照する前にNULL
ポインターをチェックするなど、検証を実行する必要があります。しかし、APIを通じて公開することを期待していない同じファイル内の関数の場合はどうでしょうか。
たとえば following はgitのソースコードに表示されます。
static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
if (!want_color(graph->revs->diffopt.use_color))
return column_colors_max;
return graph->default_column_color;
}
*graph
is NULL
の場合、nullポインターが逆参照され、おそらくプログラムがクラッシュしますが、その他の予測できない動作が発生する可能性があります。一方、関数はstatic
であるため、おそらくプログラマはすでに入力を検証しています。わかりません。Cで記述されたアプリケーションプログラムの短い例だったので、ランダムに選択しました。NULLをチェックせずにポインターが使用される他の多くの場所を見てきました。私の質問は、このコードセグメントに固有ではありません。
例外処理 のコンテキスト内で同様の質問が行われるのを見ました。ただし、CやC++などの安全でない言語の場合、未処理の例外による自動エラー伝搬はありません。
一方、オープンソースプロジェクト(上記の例など)には、使用する前にポインターのチェックを行わないコードがたくさんあります。関数が正しい引数で呼び出されたと仮定するのではなく、関数にチェックを入れるタイミングに関するガイドラインについて誰かが考えているかどうか疑問に思っています。
私は、プロダクションコードを書くために、この質問全般に興味があります。しかし、私はプログラミングインタビューのコンテキストにも興味があります。たとえば、多くのアルゴリズムの教科書(CLRなど)では、エラーチェックなしでアルゴリズムを疑似コードで提示する傾向があります。ただし、これはアルゴリズムのコアを理解するのに適していますが、プログラミングの実践としては明らかに良くありません。したがって、私はコード例を単純化するためにエラーチェックをスキップしていることを(教科書がそうであるように)インタビュアーに伝えたくありません。しかし、過度のエラーチェックを伴う非効率的なコードを生成するようにも思わないでください。たとえばgraph_get_current_column_color
をチェックするように変更されている可能性があります*graph
はnullですが、*graph
はnullでしたが、逆参照してはなりません。
無効なnullポインタは、プログラマエラーまたは実行時エラーが原因である可能性があります。ランタイムエラーは、メモリ不足やネットワークがパケットをドロップしたりユーザーが愚かなものを入力したりするためにmalloc
が失敗するなど、プログラマーが修正できない問題です。プログラマーのエラーは、プログラマーが関数を誤って使用したことが原因です。
私が見た一般的な経験則では、ランタイムエラーは常にチェックする必要がありますが、プログラマエラーは毎回チェックする必要はありません。 graph_get_current_column_color(0)
と直接呼ばれる馬鹿なプログラマがいるとしましょう。初めて呼び出されたときにセグメンテーション違反が発生しますが、一度修正すると、修正は永続的にコンパイルされます。実行するたびに確認する必要はありません。
ときどき、特にサードパーティのライブラリでは、assert
ステートメントの代わりに、プログラマエラーをチェックするためにif
が表示されます。これにより、開発中にチェックをコンパイルし、プロダクションコードに残すことができます。また、潜在的なプログラマエラーの原因が症状から遠く離れている、不必要なチェックもときどき見ました。
明らかに、あなたは常に誰かをより知識のある人に見つけることができますが、私が知っているほとんどのCプログラマーは、わずかに安全なコードよりも雑然としたコードを好みません。そして「より安全」は主観的な用語です。開発中の露骨なsegfaultは、フィールドでの微妙な破損エラーよりも望ましいです。
Kernighan&Plaugerは、「ソフトウェアツール」ですべてをチェックし、実際には決して起こらないと思われる状況では、「できません」というエラーメッセージで中断することを書きました。
彼らは、「出来ない」が彼らの端末に出てくるのを見た回数によって急速に謙虚になったと報告している。
ポインターを逆参照する(試行する)前に、ポインターのNULLを必ずチェックする必要があります。 [〜#〜]常に[〜#〜]。発生しないNULLのチェックを重複して実行するコードの量、および「無駄」なプロセッササイクルは、クラッシュダンプ以外からデバッグする必要がないクラッシュの数によって支払われます-運が良ければ.
ポインターがループ内で不変である場合は、ループの外でチェックするだけで十分ですが、ループで使用できるように、スコープ限定のローカル変数に「コピー」して、適切なconst装飾を追加する必要があります。この場合、ループ本体から呼び出されるすべての関数に、プロトタイプに必要なconst装飾がすべて含まれていることを確認する必要があります。できない場合、またはできない場合(たとえば、ベンダーパッケージや頑固な同僚が原因で)、NULLを確認する必要がありますEVERY TIME IT COULD BE MODIFIED、COL Murphyは確かに不治の楽天家、誰か[〜#〜] is [〜#〜]あなたが見ていないときにそれを打つでしょう。
関数内にいて、ポインタがNULL以外になると想定されている場合は、それを確認する必要があります。
関数から受け取っていて、それが非NULLであると思われる場合は、それを確認する必要があります。 malloc()はこのことで特に悪名高いです。 (Nortel Networksは、現在は機能していませんが、これに関するコーディング標準が厳格に記述されていました。ある時点でクラッシュをデバッグする必要がありました。malloc()までたどって、NULLポインターを返し、バカなコーダーがチェックする必要がありませんでした。彼がそれを書く前に、彼はそれを知っていたので彼はたくさんの記憶を持っていたのでそれを書きました...私が最終的にそれを見つけたとき、私はいくつかの非常に嫌なことを言いました)
ポインタがnullになる可能性がないことを何らかの方法で納得できる場合は、チェックをスキップできます。
通常、nullポインターチェックは、nullがオブジェクトが現在使用できないことを示すインジケーターとして表示されることが期待されるコードに実装されています。 Nullは、リンクされたリストやポインターの配列などを終了するために、センチネル値として使用されます。 argv
に渡される文字列のmain
ベクトルは、文字列がnull文字で終了するのと同様に、ポインターでnull終了する必要があります。argv[argc]
はnullポインターです、そしてコマンドラインを解析するときにこれを信頼することができます。
while (*argv) {
/* process argument string *argv */
argv++; /* increment to next one */
}
したがって、nullをチェックする状況は、それが期待値である状況です。 nullチェックは、リンクリストの検索を停止するなど、nullポインターの意味を実装します。これらは、コードがポインターを逆参照するのを防ぎます。
Nullポインタ値が設計上予期されていない状況では、それをチェックしても意味がありません。無効なポインター値が発生した場合、それはnull以外のように見える可能性が高く、移植可能な方法で有効な値と区別できません。たとえば、ポインタ型として解釈される初期化されていないストレージの読み取りから取得されたポインタ値、何らかの怪しい変換を介して取得されたポインタ、または範囲外でインクリメントされたポインタ。
graph *
などのデータ型について:これは、null値が有効なグラフになるように設計できます(エッジもノードもないもの)。この場合、graph *
ポインターを受け取るすべての関数は、その値を処理する必要があります。これは、グラフの表現において正しいドメイン値であるためです。一方、graph *
は、グラフを保持している場合はnullになることのないコンテナのようなオブジェクトへのポインタになる可能性があります。次に、nullポインターは、「グラフオブジェクトが存在しない、まだ割り当てていない、または解放した、または現在、グラフオブジェクトに関連付けられていない」と通知する場合があります。後者のポインタの使用は、ブール値とサテライトの組み合わせです。nullでないポインタは、「この姉妹オブジェクトがある」ことを示しますandこれは、そのオブジェクトを提供します。
オブジェクトを解放していない場合でも、ポインタをnullに設定して、あるオブジェクトを別のオブジェクトから分離することができます。
tty_driver->tty = NULL; /* detach low level driver from the tty device */
フーガにもう1つ声を入れましょう。
他の多くの回答と同様に、この時点では確認しないでください。それは呼び出し側の責任です。しかし、私は単純な便宜(およびCプログラミングの傲慢さ)ではなく、上に構築するための基盤を持っています。
私は、プログラムを可能な限り脆弱にするというドナルドクヌースの原則に従っています。何か問題が発生した場合は、クラッシュさせますbig。通常、nullポインターを参照することは、これを行うための良い方法です。一般的な考え方は、クラッシュまたは無限ループは、間違ったデータを作成するよりもfar優れているということです。そしてそれはプログラマーの注意を引く!
ただし、nullポインタを参照すると(特に大きなデータ構造の場合)、必ずしもクラッシュが発生するわけではありません。はぁ。それは本当だ。そして、それがAssertsが該当する場所です。Assertsは単純で、プログラムを即座にクラッシュさせる可能性があり(「nullが検出された場合、メソッドは何をすべきか」という質問に答えます)、さまざまな状況でオン/オフを切り替えることができます(推奨それらをオフにしないでください。顧客がクラッシュして、暗号化されたメッセージを表示するほうが、悪いデータを持っているよりも良いからです。
それは私の2セントです。
私の意見では、入力(事前/事後条件)を検証することは、プログラミングエラーを検出するのに適していますが、無視できない種類の大規模で不愉快なショーストップエラーが発生する場合に限られます。 assert
は通常、その効果があります。
これを下回るものはすべて、非常に注意深く調整されたチームなしでは悪夢に変わる可能性があります。そしてもちろん、理想的にはすべてのチームが非常に注意深く調整され、厳しい基準の下で統一されていますが、私が取り組んできたほとんどの環境は、それをはるかに下回っています。
例として、私は1人の同僚と協力して、ヌルポインターの存在を確実にチェックする必要があると考えていたため、次のようなコードをたくさん振りかけました。
void vertex_move(Vertex* v)
{
if (!v)
return;
...
}
...エラーコードを返したり設定したりせずに、時々そのようにすることもできます。そして、これは数十年前のコードベースにあり、多くのサードパーティプラグインを取得していました。また、多くのバグに悩まされているコードベースでもあり、問題の直接の原因から遠く離れたサイトでクラッシュする傾向があったため、根本原因を突き止めることが非常に困難なバグがよくありました。
そして、この実践が理由の1つでした。上記の確立された前提条件の違反ですmove_vertex
関数はnull頂点をそれに渡しますが、そのような関数は黙ってそれを受け入れ、何も応答しませんでした。つまり、プラグインにプログラマーのミスがあり、プラグインが上記の関数にnullを渡して、それを検出せず、その後多くのことを行うだけで、最終的にシステムがフレーキングまたはクラッシュする可能性がありました。
しかし、ここでの実際の問題は、この問題を簡単に検出できないことでした。したがって、上記のようなコードをassert
に変更するとどうなるかを確認しようとしました。
void vertex_move(Vertex* v)
{
assert(v && "Vertex should never be null!");
...
}
...そして恐ろしいことに、アプリケーションを起動しても、アサーションが左右に失敗することがわかりました。最初のいくつかの呼び出しサイトを修正した後、私はさらにいくつかのことを行い、それからより多くのアサーションエラーがボートロードされました。あまりにも多くのコードを変更して、変更が煩わしくなり、やっとのことでそのnullポインターチェックを保持していたため、関数がnull頂点の受け入れを許可することを文書化するまで、私は変更を元に戻しました。
しかし、これは最悪のシナリオではありますが、事前/事後条件の違反を簡単に検出できない場合の危険です。その後、何年にもわたって、テストのレーダーの下で飛行しているときに、そのような事前/事後条件に違反する大量のコードを静かに蓄積できます。私の意見では、そのようなnullポインタチェックは、露骨で不愉快なアサーションの失敗の外で実際にはるかに害を及ぼす可能性があります。
いつヌルポインターをチェックする必要があるかという本質的な問題については、プログラマーのエラーを検出するように設計されていて、それを黙らせて検出するのが難しいのではないかと思います。それがプログラミングエラーではなく、メモリ不足の障害など、プログラマーの制御が及ばないものである場合は、nullをチェックしてエラー処理を使用することは理にかなっています。それを超えてそれは設計の質問であり、関数が有効な事前/事後条件であると見なすものに基づいています。
私は通常、ポインターが割り当てられたときのみチェックします。これは、通常、実際にポインターに対して何かを実行し、それが無効な場合に回復できる唯一の時間です。
たとえばウィンドウへのハンドルを取得した場合は、それがnullであることを時々確認し、null条件について何かを行いますが、毎回nullであることを確認するつもりはありません私はポインターを使用します。ポインターが渡されるすべての関数で、そうでなければ、エラー処理コードの山が重複してしまいます。
graph_get_current_column_color
のような関数は、不正なポインターが発生した場合、状況に役立つ何かを完全に実行できない可能性があるため、呼び出し側にNULLのチェックを任せます。
それは以下に依存すると私は言うでしょう:
CPU Utilization/Odds Pointer is NULL NULLを確認するたびに時間がかかります。このため、ポインターの値が変更されている可能性がある場所にチェックを制限しようとしています。
プリエンプティブシステムコードが実行されていて、別のタスクがそれを中断し、潜在的に値を変更する可能性がある場合、チェックを行うとよいでしょう。
密結合モジュールシステムが密結合されている場合は、より多くのチェックがあることは理にかなっています。つまり、複数のモジュール間で共有されるデータ構造がある場合、1つのモジュールが別のモジュールの下から何かを変更する可能性があります。これらの状況では、より頻繁にチェックすることは理にかなっています。
自動チェック/ハードウェアアシスト考慮すべき最後のことは、実行中のハードウェアに、NULLをチェックできる何らかのメカニズムがあるかどうかです。特に私はページフォルト検出について言及しています。システムにページフォールト検出機能がある場合、CPU自体がNULLアクセスをチェックできます。個人的には、これが常に実行され、プログラマーが明示的なチェックを行う必要がないため、これが最良のメカニズムであると思います。また、実質的にオーバーヘッドがゼロという利点もあります。これが利用可能な場合、私はそれをお勧めします。デバッグは少し難しくなりますが、それほど難しくはありません。
使用可能かどうかをテストするには、ポインタを使用してプログラムを作成します。ポインターを0に設定してから、読み取り/書き込みを試みてください。
Nullポインタチェックは、割り当てメソッドが失敗するとゼロを返すため、割り当てられていないデータに適しています。設定されていないポインタのランダムなガベージやデータの上書きにはあまり使用されません。
システムがサポートしている場合は、実際のメモリ範囲を見つけ、その範囲内にあるポインタを確認します。ターゲットが組み込みデバイス(マイクロコントローラー)の場合、RAMおよびフラッシュアドレス範囲は通常固定されているため、すべてのコードは既知のプログラムおよびデータアドレス範囲で実行されます。おそらくメモリ管理ユニット(または同様の)これを壊すか、それをサポートします。
組み込みシステムには「大きな失敗」はありません。エラー割り込みから無限ループに置かれるだけです。 「また死んだ」にはスタックダンプデータはありません。
範囲内にあるかどうか、ポインタと配列インデックスを常にチェックします。たとえば、32ビットシステムの2MBのメモリブロックでは、有効なアドレスはわずか0.05%です。これは迅速で、比較的小さいので、これを追加するために現代のマイクロコントローラーを確認してください。
1つの方法は、既にチェックしていない限り、常にnullチェックを実行することです。したがって、入力が関数から渡されている場合A() to B()であり、A()はすでにポインタを検証していますand =確かですB()は他の場所で呼び出されないので、B()信頼できるA()データを無害化しました。