興味深いインタビューの質問に出くわしました:
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
まず最初に:printf
は%s引数に有効な(つまりNULL以外の)ポインターを期待しているため、NULLを渡すことは公式には未定義です。 「(null)」と表示されるか、ハードドライブ上のすべてのファイルが削除される場合があります。ANSIに関する限り、どちらも正しい動作です(少なくとも、それはHarbisonとSteeleが言っていることです)。
そうは言っても、これは本当に奇妙な振る舞いです。何が起こっているのかは、次のような単純なprintf
を実行したときに発生することがわかります。
printf("%s\n", NULL);
gccは(ahem)賢く、これを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
奇妙なことは、次の改行を印刷しないことです。これはセグメンテーション違反を引き起こすことがわかっているので、気にしないようです。 (これは、コンパイル時に警告されました。)
C言語に関する限り、その理由は未定義の動作を呼び出しているためであり、何でも起こり得ます。
なぜこれが起こっているかのメカニズムについては、現代のgccはprintf("%s\n", x)
をputs(x)
に最適化し、puts
には(null)
を出力する愚かなコードがありませんヌルポインターが表示されますが、printf
の一般的な実装にはこの特殊なケースがあります。 gccは(一般に)このような重要なフォーマット文字列を最適化できないため、フォーマット文字列に他のテキストが存在する場合、printf
は実際に呼び出されます。
セクション7.1.4(C99またはC11):
§7.1.4ライブラリ関数の使用
¶1以下の各ステートメントは、以下の詳細な説明で明示的に述べられていない限り適用されます:関数の引数に無効な値がある場合(関数のドメイン外の値、またはアドレス空間外のポインターなど)プログラム、ヌルポインター、または対応するパラメーターがconst修飾されていない場合の変更不可能な記憶域へのポインター、または可変数の引数を持つ関数で予期されない型(昇格後)の場合、動作は未定義です。
printf()
の仕様では、_%s
_指定子にNULLポインターを渡すと何が起こるかについて何も述べていないため、動作は明示的に定義されていません。 (_%p
_指定子によって印刷されるNULLポインターを渡すことは、未定義の動作ではないことに注意してください。)
fprintf()
ファミリの動作の「章と詩」を次に示します(C2011 — C1999では別のセクション番号です)。
§7.21.6.1fprintf関数
s
l
長さ修飾子が存在しない場合、引数は文字型の配列の初期要素へのポインタでなければなりません。 [...]
l
長さ修飾子が存在する場合、引数はwchar_t型の配列の初期要素へのポインターでなければなりません。
p
argument引数はvoidへのポインタでなければなりません。ポインターの値は、実装定義の方法で、印刷文字のシーケンスに変換されます。
s
変換指定子の仕様は、nullポインターが適切な型の配列の初期要素を指していないため、nullポインターが有効である可能性を排除しています。 p
変換指定子の仕様では、特にポインターを指すためにvoidポインターを必要としないため、NULLが有効です。
多くの実装がNULLポインターを渡されたときに_(null)
_などの文字列を出力するという事実は、依存するのが危険な優しさです。未定義の動作の利点は、そのような応答が許可されることですが、必須ではありません。同様に、クラッシュは許可されますが、必須ではありません(さらに残念なことに、寛容なシステムで作業してから他の寛容でないシステムに移植すると、人は噛まれます)。
NULL
ポインターはどのアドレスも指し示しておらず、印刷しようとすると未定義の動作が発生します。未定義の意味は、NULLを出力しようとしたときに何をするかはコンパイラーまたはCライブラリーが決めることです。