昔、フォーラムでおもしろい質問につまずいたので、その答えを知りたいです。
次のC関数を考えてください。
#include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}
var3 == 3000
以降、これは常にfalse
を返すはずです。 main
関数は次のようになります。
#include <stdio.h>
#include <stdbool.h>
int main()
{
printf( f1() == true ? "true\n" : "false\n");
if( f1() )
{
printf("executed\n");
}
return 0;
}
f1()
は常にfalse
を返すはずなので、プログラムは1つだけfalseを画面に表示します。しかし、コンパイルして実行すると、実行済みも表示されます。
$ gcc main.c f1.c -o test
$ ./test
false
executed
何故ですか?このコードには未定義の動作がありますか?
注:gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2
を付けてコンパイルしました。
他の回答で述べたように、問題は、コンパイラオプションを設定せずにgcc
を使用することです。これを行うと、デフォルトで「gnu90」と呼ばれるものになります。これは、1990年以降に廃止された古いC90標準の非標準実装です。
古いC90標準には、C言語に重大な欠陥がありました。関数を使用する前にプロトタイプを宣言しなかった場合、デフォルトでint func ()
になります(ここで、( )
は「パラメータを受け入れる」ことを意味します) )。これにより、関数func
の呼び出し規則が変更されますが、実際の関数定義は変更されません。 bool
とint
のサイズは異なるため、関数が呼び出されると、コードは未定義の動作を呼び出します。
この危険なナンセンスな動作は、1999年にC99標準のリリースにより修正されました。暗黙的な関数宣言は禁止されました。
残念ながら、バージョン5.x.xまでのGCCは、引き続きデフォルトで古いC標準を使用します。コードを標準C以外のものとしてコンパイルする必要がある理由はおそらくないでしょう。したがって、GCCに、25歳以上の非標準GNUがらくた。
プログラムを常に次のようにコンパイルして、問題を修正します。
gcc -std=c11 -pedantic-errors -Wall -Wextra
-std=c11
は、(現在の)C標準(非公式にはC11として知られている)に従ってコンパイルしようと中途半端な試みを行うように指示します。-pedantic-errors
は、上記を心から行い、C標準に違反する不正なコードを記述するとコンパイラエラーを発生させるように指示します。-Wall
は、持っていると良いかもしれない追加の警告を私に与えることを意味します。-Wextra
は、他にも警告が表示される場合があることを意味します。Main.cにf1()
用に宣言されたプロトタイプがないので、暗黙的にint f1()
として定義されています。つまり、未知の数の引数を取り、int
を返す関数です。
int
とbool
のサイズが異なる場合は、未定義の動作になります。たとえば、私のマシンでは、int
は4バイト、bool
は1バイトです。関数はbool
を返すためのdefinedなので、戻るときにスタックに1バイトを置きます。しかし、main.cからint
を返すのは暗黙的に宣言されますしているので、呼び出し側の関数はスタックから4バイトを読み込もうとします。
Gccのデフォルトのコンパイラオプションでは、これが行われていることがわかりません。 -Wall -Wextra
を付けてコンパイルした場合は、次のようになります。
main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’
これを修正するには、main.cのmain
の前にf1
の宣言を追加します。
bool f1(void);
引数リストが明示的にvoid
に設定されていることに注意してください。空のパラメータリストは未知数の引数を意味するのに対して、コンパイラは関数に引数を取らないように指示します。これを反映するように、f1.cの定義f1
も変更する必要があります。
Lundinの優れた答えの中で言及されているサイズの不一致が実際にどこで起こるのかを見るのは面白いと思います。
--save-temps
を付けてコンパイルすると、アセンブリファイルが手に入ります。これがf1()
が== 0
の比較を行い、その値を返す部分です。
cmpl $0, -4(%rbp)
sete %al
返される部分はsete %al
です。 Cのx86呼び出し規約では、4バイト以下(int
とbool
を含む)の戻り値は、レジスタ%eax
を介して返されます。 %al
は%eax
の最下位バイトです。そのため、%eax
の上位3バイトは制御されていない状態のままになります。
今main()
で:
call f1
testl %eax, %eax
je .L2
これは、intをテストしていると考えているため、%eax
のwholeがゼロかどうかをチェックします。
明示的な関数宣言を追加すると、main()
が次のように変更されます。
call f1
testb %al, %al
je .L2
それが私たちが欲しいものです。
このようなコマンドでコンパイルしてください。
gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c
出力:
main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
printf( f1() == true ? "true\n" : "false\n");
^
cc1.exe: all warnings being treated as errors
そのようなメッセージで、あなたはそれを修正するために何をすべきか知っているべきです。
編集:(今削除された)コメントを読んだ後、私はフラグなしであなたのコードをコンパイルしようとしました。まあ、これは私にコンパイラエラーの代わりにコンパイラ警告なしのリンカエラーをもたらしました。そして、それらのリンカエラーは理解するのがより難しいので、-std-gnu99
が必要でないとしても、少なくとも-Wall -Werror
を使うようにしてください、それはあなたのお尻の多くの苦痛を救うでしょう。