67

興味深いインタビューの質問に出くわしました:

test 1:
printf("test %s\n", NULL);
printf("test %s\n", NULL);

prints:
test (null)
test (null)

test 2:
printf("%s\n", NULL);
printf("%s\n", NULL);
prints
Segmentation fault (core dumped)

これは一部のシステムでは問題なく動作する可能性がありますが、少なくとも私のシステムではセグメンテーション エラーが発生しています。この動作の最良の説明は何ですか? 上記のコードは C です。

以下は私のgcc情報です:

deep@deep:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
4

4 に答える 4

77

まず最初に:printfは %s 引数に有効な (つまり、非 NULL) ポインターを期待しているため、NULL を渡すことは公式には定義されていません。「(null)」と表示されるか、ハード ドライブ上のすべてのファイルが削除される可能性があります。ANSI に関する限り、どちらも正しい動作です (少なくとも、Harbison と Steele はそう言っています)。

そうは言っても、これは本当に奇妙な行動です。printf次のような単純なことをすると、何が起こっているかがわかります。

printf("%s\n", NULL);

gcc は (エヘム) これを への呼び出しに分解するのに十分スマート putsです。最初のprintf、これ:

printf("test %s\n", NULL);

gcc が代わりに real への呼び出しを発行するほど複雑 printfです。

(gcc は、コンパイル時に無効な引数に関する警告を発することに注意してくださいprintf。これは、*printfフォーマット文字列を解析する機能がずっと前に開発されたためです。)

-save-tempsこれは、オプションを指定してコンパイルし、結果の.sファイルを調べることで確認できます。

最初の例をコンパイルすると、次のようになりました。

movl    $.LC0, %eax
movl    $0, %esi
movq    %rax, %rdi
movl    $0, %eax
call    printf      ; <-- Actually calls printf!

(コメントは私が追加しました。)

しかし、2番目のものはこのコードを生成しました:

movl    $0, %edi    ; Stores NULL in the puts argument list
call    puts        ; Calls puts

奇妙なことは、次の改行を出力しないことです。これがセグメンテーション違反を引き起こすことがわかっているかのように、気にしません。(これは、コンパイル時に警告されました。)

于 2012-07-21T04:31:38.847 に答える
34

C言語に関する限り、その理由は、未定義の動作を呼び出しており、何でも起こり得るからです。

なぜこれが起こっているのかというメカニズムについては、最新の gcc は に最適化printf("%s\n", x)puts(x)、null ポインターを検出したときにputs出力するばかげたコードを持っていませんが、 の一般的な実装にはこの特別なケースがあります。gcc は (一般に) このような重要なフォーマット文字列を最適化できないため、実際には、フォーマット文字列に他のテキストが存在する場合に呼び出されます。(null)printfprintf

于 2012-07-21T04:23:46.467 に答える
18

セクション 7.1.4 (C99 または C11 の) は次のように述べています。

§7.1.4 ライブラリ関数の使用

¶1 以下の詳細な説明で特に明記されていない限り、次の各ステートメントが適用されます。プログラム、または null ポインター、または対応するパラメーターが const 修飾されていない場合の変更不可能なストレージへのポインター)、または可変数の引数を持つ関数によって予期されない型 (昇格後) の場合、動作は未定義です。

の仕様でprintf()は、指定子にヌル ポインターを渡したときに何が起こるかについて何も述べていないため%s、動作は明示的に未定義です。(指定子によって出力される null ポインターを渡すこと%pは、未定義の動作ではないことに注意してください。)

家族の行動の「章と節」はfprintf()次のとおりです (C2011 — C1999 ではセクション番号が異なります)。

§7.21.6.1 fprintf 関数

s     長さ修飾子が存在しない場合l、引数は文字型の配列の最初の要素へのポインターになります。[...]

     長さ修飾子が存在する場合l、引数は wchar_t 型の配列の最初の要素へのポインターでなければなりません。

p     引数は void へのポインタでなければなりません。ポインターの値は、実装定義の方法で一連の印刷文字に変換されます。

変換指定子の仕様はs、ヌル ポインターが適切な型の配列の最初の要素を指していないため、ヌル ポインターが有効である可能性を排除します。変換指定子のp指定では、void ポインターが何かを指す必要は特にないため、NULL が有効です。

null ポインターが渡された場合など、多くの実装が文字列を出力するという事実(null)は、頼りにするのは危険な優しさです。未定義の動作の利点は、そのような応答が許可されていることですが、必須ではありません。同様に、クラッシュは許可されていますが、必須ではありません (さらに残念なことに、寛容なシステムで作業してから他の寛容性の低いシステムに移植すると、噛まれてしまいます)。

于 2012-07-21T04:36:21.847 に答える
7

ポインタはどのアドレスも指していません。NULLポインタを出力しようとすると、未定義の動作が発生します。未定義とは、NULLを出力しようとしたときに何をするかを決めるのはコンパイラまたはCライブラリ次第であることを意味します。

于 2012-07-21T04:13:10.423 に答える