CでNULLのポインタをいつチェックする必要がありますか?


18

まとめ

Cの関数は、NULLポインターを間接参照していないことを常に確認する必要がありますか?そうでない場合、これらのチェックをスキップするのが適切ですか?

詳細

プログラミングのインタビューに関する本をいくつか読んでいますが、Cの関数引数の入力検証の適切な程度はどうなっているのでしょうか?明らかに、ユーザーから入力を受け取る関数は、NULL参照を解除する前にポインターをチェックするなど、検証を実行する必要があります。しかし、APIを介して公開することを期待しない同じファイル内の関数の場合はどうでしょうか?

たとえば、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であるNULLヌル・ポインタは、おそらくプログラムをクラッシュが、おそらく他のいくつかの予期しない動作が得られ、逆参照されます。一方、関数はstaticそうなので、プログラマーはすでに入力を検証しているかもしれません。Cで書かれたアプリケーションプログラムの短い例であるため、ランダムに選択しただけです。NULLをチェックせずにポインターが使用される場所は他にもたくさんあります。私の質問は、一般にこのコードセグメントに固有のものではありません。

例外処理の文脈の中で同様の質問があるのを見ました。ただし、CやC ++などの安全でない言語では、未処理の例外の自動エラー伝播はありません。

一方、オープンソースプロジェクト(上記の例など)では、使用する前にポインターのチェックを行わないコードがたくさん見られました。関数にチェックを入れるタイミングと、関数が正しい引数で呼び出されたと仮定するタイミングに関するガイドラインについて誰かが考えているかどうか疑問に思っています。

私は本番コードを書くためにこの質問全般に興味があります。しかし、私はプログラミングインタビューのコンテキストにも興味があります。たとえば、多くのアルゴリズムの教科書(CLRなど)は、エラーチェックなしで擬似コードでアルゴリズムを提示する傾向があります。ただし、これはアルゴリズムの中核を理解するのには適していますが、明らかにプログラミングの実践としては適切ではありません。そのため、(教科書のように)コード例を単純化するためにエラーチェックをスキップしていることをインタビュアーに伝えたくありません。しかし、私はまた、過度のエラーチェックを伴う非効率なコードを生成するようには見えません。たとえば、null graph_get_current_column_colorをチェックするように変更された可能性があり*graphますが、*graphnullである場合の動作は明確ではありません。


7
呼び出し側が内部を理解する必要がないAPIの関数を作成している場合、これはドキュメントが重要な場所の1つです。引数が有効な非NULLポインターでなければならないことを文書化する場合、それを確認することは呼び出し側の責任になります。
Blrfl


質問とほとんどの回答は2013年に書かれていることを念頭に置いて、2017年の後ろを振り返って、コンパイラの最適化による時間旅行の未定義の動作の問題に対処する回答はありますか?
rwong

有効なポインター引数を期待するAPI呼び出しの場合、NULLのみをテストすることの価値は何でしょうか?間接参照される無効なポインターは、NULLと同様に不良であり、segfaultはすべて同じです。
PaulHK

回答:


15

無効なNULLポインターは、プログラマーエラーまたはランタイムエラーのいずれかによって発生します。ランタイムエラーは、mallocメモリ不足、ネットワークでのパケットのドロップ、またはユーザーが愚かなものを入力したために失敗するなど、プログラマーが修正できないものです。プログラマーのエラーは、プログラマーが関数を誤って使用したことが原因です。

私が経験した一般的な経験則では、ランタイムエラーは常にチェックする必要がありますが、プログラマエラーは毎回チェックする必要はありません。馬鹿プログラマーが直接呼び出したとしましょうgraph_get_current_column_color(0)。最初に呼び出されたときにセグメンテーション違反になりますが、一度修正すると、修正は永続的にコンパイルされます。実行するたびにチェックする必要はありません。

場合によっては、特にサードパーティのライブラリでassertは、ifステートメントの代わりにプログラマのエラーを確認する必要があります。これにより、開発中にチェックをコンパイルし、本番コードにチェックを残すことができます。また、潜在的なプログラマーのエラーの原因が症状から大幅に削除されている無償のチェックを見ることもあります。

明らかに、つまらない人を常に見つけることができますが、私が知っているほとんどのCプログラマーは、わずかに安全なコードよりも混乱の少ないコードを好みます。そして、「より安全」は主観的な用語です。開発中の露骨なセグメンテーション違反は、現場での微妙な破損エラーよりも望ましいです。


質問は多少主観的ですが、これは今のところ最良の答えのように見えました。この質問について考えてくれた皆さんに感謝します。
ガブリエルサザン

1
iOSでは、mallocは決してNULLを返しません。メモリが見つからない場合、アプリケーションに最初にメモリを解放するように要求し、次にオペレーティングシステムに要求し(他のアプリにメモリを解放し、場合によってはそれらを殺すように要求します)、まだメモリがない場合はアプリを殺します。チェックは不要です。
gnasher729

11

Kernighan&Plaugerは、「ソフトウェアツール」で、すべてをチェックし、実際には決して起こり得ないと考えていた条件については、「できません」というエラーメッセージで中止することを書きました。

彼らは、「起こりえない」が端末に出てくるのを見た回数によって急速に謙虚になっていると報告しています。

間接参照する(試みる)前に、常にNULLのポインターをチェックする必要があります。 常に。発生しないNULLのチェックを複製するコードの量、およびプロセッササイクルの「無駄」は、クラッシュダンプ以外からデバッグする必要のないクラッシュの数によって支払われます。運がよければ

ポインターがループ内で不変の場合、ループ外でチェックするだけで十分ですが、ループで使用するために適切なconst装飾を追加するスコープ限定のローカル変数に「コピー」する必要があります。この場合、ループ本体から呼び出されるすべての関数に、プロトタイプに必要なconst装飾が含まれていることを確認する必要があります。そうでない、またはできない場合は、あなたがNULLのためにそれをチェックしなければなりません(例えばAベンダーのパッケージや頑固なの同僚のために)EVERY TIME ITを修正することができる COLマーフィーは不治の楽天をしているのを確かめているので、誰かがIS行きます見ていないときにザップする

関数内にいて、ポインターが非NULLであることになっている場合は、検証する必要があります。

関数から受け取っており、非NULLであると想定される場合は、検証する必要があります。malloc()はこれについて特に悪名が高い。(Nortel Networksは、現在は機能していませんが、これについては非常に高速なコーディング標準がありました。ある時点でクラッシュをデバッグすることができました。malloc()に戻ってNULLポインターと、彼がそれを書く前に、それは彼が彼がたくさんの記憶を持っていたので知っていたので...私が最終的にそれを見つけたとき、私はいくつかの非常に厄介なことを言った。)


8
NULL以外のポインターを必要とする関数を使用しているが、とにかくチェックしてNULLになった場合...
確実に

1
@detlyは、あなたがしていることを止めてエラーコードを返すか、アサートをトリップします
ジェームズ

1
@ジェームズ-考えていなかったassert、確かに。NULLただし、既存のコードを変更してチェックを含めることについて話している場合、エラーコードのアイデアは好きではありません。
確実に

10
@detlyあなたがエラーコード気に入らない場合はC devのように非常に遠くを取得するつもりはない
ジェームズ

5
@ JohnR.Strohm-これはCであり、アサーションか何もない:P-
確実に

5

ポインターをnullにできない可能性があると自分で確信できる場合は、チェックをスキップできます。

通常、ヌルポインターチェックは、オブジェクトが現在利用できないことを示すインジケーターとしてヌルが表示されることが予想されるコードに実装されます。Nullは、リンクリストやポインターの配列などを終了するために、センチネル値として使用されます。argv渡される文字列のベクトルは、文字列mainがヌル文字で終了する方法と同様に、ポインターでヌル終了する必要があります。argv[argc]ヌルポインターであり、コマンドラインを解析するときにこれに依存できます。

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

したがって、nullをチェックする状況は、それが期待値である状況です。nullチェックは、リンクリストの検索を停止するなど、nullポインターの意味を実装します。これらは、コードがポインターを逆参照するのを防ぎます。

NULLポインター値が設計で予期されていない状況では、それをチェックしても意味がありません。無効なポインター値が発生した場合、それは非ヌルに見える可能性が高く、移植可能な方法で有効な値と区別することはできません。たとえば、ポインター型として解釈される初期化されていないストレージの読み取りから取得されたポインター値、何らかの怪しい変換を介して取得されたポインター、または範囲外にインクリメントされたポインター。

次のようなデータ型についてgraph *:これは、null値が有効なグラフになるように設計できます:エッジもノードもないものです。この場合、graph *ポインターを取るすべての関数はその値を処理する必要があります。これは、グラフの表現では正しいドメイン値であるためです。一方、a graph *は、グラフを保持している場合にnullにならないコンテナのようなオブジェクトへのポインタである可能性があります。nullポインタは、「グラフオブジェクトが存在しない、まだ割り当てていない、または解放した、または現在グラフに関連付けられていない」ことを示す場合があります。ポインタのこの後者の使用は、組み合わせブーリアン/衛星である:非NULLであるポインタは、「私はこの姉妹オブジェクトを持っている」を示し、そしてそれは、そのオブジェクトを提供します。

オブジェクトを解放していない場合でも、1つのオブジェクトを別のオブジェクトから分離するために、ポインターをnullに設定できます。

tty_driver->tty = NULL; /* detach low level driver from the tty device */

ポインタが特定のポイントでnullにならないことを知っている最も説得力のある引数は、そのポイントを「if(ptr!= NULL){」および対応する「}」でラップすることです。それを超えて、あなたは正式な検証領域にいます。
ジョンR.ストローム

4

フーガにもう1つ声を加えさせてください。

他の多くの答えのように、私は言う-この時点で確認することはありません。それは呼び出し側の責任です。しかし、単純な手段(およびCプログラミングのrog慢さ)ではなく、構築する基盤があります。

私は、プログラムを可能な限り脆弱にするというドナルド・クヌースの原則に従っています。何か問題が発生した場合は、クラッシュが大きくなります。通常は、nullポインターを参照するのが良い方法です。一般的な考えはクラッシュであるか、間違ったデータを作成するよりも無限ループがはるかに優れています。そして、プログラマーの注目を集めます!

ただし、nullポインターを参照すると(特に大きなデータ構造の場合)、常にクラッシュするわけではありません。はぁ。それは本当だ。Assertsが該当するのはここです。それらは単純で、プログラムを即座にクラッシュさせることができ(「nullに遭遇した場合、メソッドはどうすればよいでしょうか?」無効なデータを使用するよりも、顧客がクラッシュして不可解なメッセージを表示する方が良いため、それらをオフにしないでください。

それは私の2セントです。


1

私は通常、ポインターが割り当てられたときだけチェックします。これは一般に、実際にそれについて何かを行い、無効な場合は回復できる唯一の時間です。

たとえば、ウィンドウへのハンドルを取得した場合、その場でヌルであるかどうかをチェックし、ヌル条件について何かをしますが、毎回ヌルであるかどうかはチェックしませんポインターが渡されるすべての関数でポインターを使用します。そうしないと、重複するエラー処理コードが山になります。

のような関数graph_get_current_column_colorは、悪いポインタに遭遇した場合、おそらくあなたの状況に役立つものを完全に行うことができないので、呼び出し元にはNULLのチェックを任せておきます。


1

私はそれが以下に依存していると言うでしょう:

  1. CPU使用率は重要ですか?NULLをチェックするたびに時間がかかります。
  2. ポインターがNULLである確率はどのくらいですか?以前の機能で使用されただけですか。ポインターの値が変更された可能性があります。
  3. システムはプリエンプティブですか?つまり、タスクの変更が発生し、値が変更される可能性がありますか?ISRが入って値を変更できますか?
  4. コードはどの程度密結合されていますか?
  5. NULLポインターを自動的にチェックする何らかの自動メカニズムはありますか?

CPU使用率/オッズポインターがNULL NULL をチェックするたびに時間がかかります。このため、ポインターの値が変更された可能性がある場所にチェックを制限しようとします。

プリエンプティブシステム コードが実行されていて、別のタスクがそれを中断し、潜在的に値を変更する可能性がある場合、チェックを行うとよいでしょう。

密結合モジュール システムが密結合されている場合は、さらにチェックを行うことが理にかなっています。つまり、複数のモジュール間で共有されるデータ構造がある場合、1つのモジュールが別のモジュールの下から何かを変更する可能性があります。これらの状況では、より頻繁にチェックすることが理にかなっています。

自動チェック/ハードウェアアシスト 最後に考慮すべきことは、実行しているハードウェアにNULLをチェックできる何らかのメカニズムがあるかどうかです。具体的には、ページフォールト検出について言及しています。システムにページフォールト検出がある場合、CPU自体がNULLアクセスをチェックできます。個人的には、これは常に実行され、明示的なチェックを行うためにプログラマに依存しないため、これが最良のメカニズムであると思います。また、実質的にオーバーヘッドがゼロという利点もあります。これが利用可能な場合、私はそれをお勧めします、デバッグは少し難しくなりますが、過度にそうではありません。

使用可能かどうかをテストするには、ポインターを使用してプログラムを作成します。ポインターを0に設定してから、読み取り/書き込みを試みます。


セグメンテーション違反を自動NULLチェックの実行として分類するかどうかはわかりません。CPUメモリ保護があると、1つのプロセスがシステムの他の部分にそれほど大きなダメージを与えないようにするのに役立ちますが、自動保護とは呼びません。
ガブリエルサザン

1

私の意見では、入力の検証(事前/事後条件など)は、プログラミングエラーを検出するのに適していますが、無視できない種類の大規模で不快なショーストップエラーが発生する場合に限ります。assert通常、その効果があります。

これに欠けるものはすべて、非常に慎重に調整されたチームなしでは悪夢に変わる可能性があります。そして、理想的には、すべてのチームは非常に慎重に調整され、厳しい基準の下で統一されていますが、私が働いたほとんどの環境はそれをはるかに下回りました。

ちょうど例として、私は、nullポインターの存在を宗教的にチェックする必要があると信じている同僚と協力して、次のようなコードを大量に振りかけました。

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

...時には、エラーコードを返したり設定したりしなくても、そのようになります。そして、これは数十年前のコードベースにあり、多くのサードパーティのプラグインを取得していました。また、多くのバグに悩まされているコードベースであり、多くの場合、根本的な原因を突き止めるのが非常に難しいバグであり、問​​題の直接の原因から遠く離れたサイトでクラッシュする傾向がありました。

そして、この実践が理由の1つでした。move_vertexヌル頂点を渡すことは、上記の関数の確立された前提条件に違反していますが、そのような関数は黙ってそれを受け入れて、何も応答しませんでした。そのため、発生する傾向があったのは、プラグインにプログラマーのミスがあり、その関数にnullを渡して、それを検出せず、その後多くのことを行うだけで、最終的にシステムがフレークアウトまたはクラッシュする可能性があるということでした。

しかし、実際の問題は、この問題を簡単に検出できないことでした。そのassertため、上記の類推コードをに変えた場合にどうなるかを一度試してみました。

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

...そして、私の恐ろしいことに、アプリケーションを起動しても、アサーションが左右に失敗することがわかりました。最初のいくつかの呼び出しサイトを修正した後、さらにいくつかのことを行った後、さらに多くのアサーションエラーが発生しました。あまりにも多くのコードを修正するまで進み続けたため、変更が元に戻せなくなったため、変更が元に戻され、しぶしぶヌルポインターチェックを保持していたため、代わりに関数がヌル頂点を受け入れることができることを文書化しました。

しかし、それは最悪のシナリオではありますが、事前/事後条件の違反を簡単に検出できないという危険です。その後、長年にわたって、テストのレーダーの下で飛行中に、そのような事前/事後条件に違反する大量のコードを静かに蓄積することができます。私の意見では、このようなヌルポインターチェックは、露骨で不愉快なアサーションの失敗の外側で、実際にははるかに害をもたらす可能性があります。

NULLポインターをいつチェックするかという本質的な質問については、プログラマーのエラーを検出するように設計されている場合、寛大に主張し、それを沈黙させて検出するのを困難にしないと信じています。プログラミングエラーではなく、メモリ不足エラーなど、プログラマが制御できないものであれば、nullをチェックしてエラー処理を使用するのが理にかなっています。それを超えて、それは設計上の問題であり、あなたの関数が有効な事前/事後条件であると考えるものに基づいています。


0

1つの方法は、すでにチェックしていない限り、常にnullチェックを実行することです。入力が関数A()からB()に渡されており、A()がすでにポインター検証しており、B()が他のどこでも呼び出されないことが確実な場合、B()はA()を信頼することができますデータをサニタイズしました。


1
... 6か月後に誰かがやって来て、B()を呼び出すコードをさらに追加します(おそらく、B()を書いた人は必ず NULLを適切チェックしたと仮定します)。それから、あなたはねじ込まれていますよね?基本規則-関数への入力に無効な条件が存在する場合、入力は関数の制御外にあるため、チェックします。
マキシマスミニマス

@ mh01ランダムなコードを壊すだけの場合(つまり、ドキュメントを読まずに仮定を立てる場合)、余分なNULLチェックはあまり役に立たないと思います。考えてみてください:今B()NULL何をチェックし、何をしますか?戻る-1?発信者がをチェックしない場合、NULL彼らが対処しようとしている自信はどれくらいありますか-1場合戻り値のケースか?
確実に

1
それは呼び出し側の責任です。あなたはあなた自身の責任に対処します。これには、与えられたarbitrary意的/未知の/潜在的に未検証の入力を信頼しないことも含まれます。さもなければ、あなたは警官都市にいます。発信者がチェックしない場合、発信者は台無しになっています。あなたがチェックし、あなた自身のお尻が覆われている、あなたは少なくともあなたが正しいことをしたことを発信者に書いた人に伝えることができます。
マキシマスミニマス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.