web-dev-qa-db-ja.com

このC関数は常にfalseを返すべきですが、そうではありません

昔、フォーラムでおもしろい質問につまずいたので、その答えを知りたいです。

次のC関数を考えてください。

f1.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関数は次のようになります。

main.c

#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を付けてコンパイルしました。

311
incBrain

他の回答で述べたように、問題は、コンパイラオプションを設定せずにgccを使用することです。これを行うと、デフォルトで「gnu90」と呼ばれるものになります。これは、1990年以降に廃止された古いC90標準の非標準実装です。

古いC90標準には、C言語に重大な欠陥がありました。関数を使用する前にプロトタイプを宣言しなかった場合、デフォルトでint func ()になります(ここで、( )は「パラメータを受け入れる」ことを意味します) )。これにより、関数funcの呼び出し規則が変更されますが、実際の関数定義は変更されません。 boolintのサイズは異なるため、関数が呼び出されると、コードは未定義の動作を呼び出します。

この危険なナンセンスな動作は、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は、他にも警告が表示される場合があることを意味します。
389
Lundin

Main.cにf1()用に宣言されたプロトタイプがないので、暗黙的にint f1()として定義されています。つまり、未知の数の引数を取り、intを返す関数です。

intboolのサイズが異なる場合は、未定義の動作になります。たとえば、私のマシンでは、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も変更する必要があります。

138
dbush

Lundinの優れた答えの中で言及されているサイズの不一致が実際にどこで起こるのかを見るのは面白いと思います。

--save-tempsを付けてコンパイルすると、アセンブリファイルが手に入ります。これがf1()== 0の比較を行い、その値を返す部分です。

cmpl    $0, -4(%rbp)
sete    %al

返される部分はsete %alです。 Cのx86呼び出し規約では、4バイト以下(intboolを含む)の戻り値は、レジスタ%eaxを介して返されます。 %al%eaxの最下位バイトです。そのため、%eaxの上位3バイトは制御されていない状態のままになります。

main()で:

call    f1
testl   %eax, %eax
je  .L2

これは、intをテストしていると考えているため、%eaxwholeがゼロかどうかをチェックします。

明示的な関数宣言を追加すると、main()が次のように変更されます。

call    f1
testb   %al, %al
je  .L2

それが私たちが欲しいものです。

35
Owen

このようなコマンドでコンパイルしてください。

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を使うようにしてください、それはあなたのお尻の多くの苦痛を救うでしょう。

27
jdarthenay