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


317

昔、フォーラムで興味深い質問を見つけました。その答えを知りたいのですが。

次のC関数を考えます。

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

これは常にfalseから戻るはずvar3 == 3000です。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、プログラムはfalseを1つだけ画面に出力すると予想します。しかし、コンパイルして実行すると、executedも表示されます。

$ gcc main.c f1.c -o test
$ ./test
false
executed

何故ですか?このコードには何らかの未定義の動作がありますか?

注:でコンパイルしましたgcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2


9
関数が別のファイルにあるため、プロトタイプが必要であるとする人もいます。しかしf1()、と同じファイルにコピーしたとしてもmain()、奇妙なことになります。C++では()空のパラメーターリストに使用するのが適切ですが、Cではまだ定義されていないパラメーターリストを持つ関数に使用されます(基本的には))の後にK&Rスタイルのパラメーターリストが必要です。正しいCにするには、コードをに変更する必要がありますbool f1(void)
uliwitness 2016

1
main()単純化することができint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }-これは、より良い不一致を示すだろう。
Palec

@uliwitness K&R 1st edについてはどうですか。(1978)なかったときvoid
Ho1

@uliwitnessはありませんでしたtrueし、falseK&R第1版インチなので、全てのこのような問題はありませんでした。trueとfalseは0であり、0以外でした。だよね?当時プロトタイプが入手可能だったかどうかはわかりません。
Ho1

1
K&R 1st Ednはプロトタイプ(およびC標準)に10年以上先行しました(本の1978年と標準の1989年)—実際、C&R1が公開されたとき、C ++(C with Classes)はまだ未来でした。また、C99より前は、_Boolタイプも<stdbool.h>ヘッダーもありませんでした。
ジョナサンレフラー

回答:


396

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

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

この危険なナンセンスな動作は、C99標準のリリースにより1999年に修正されました。暗黙的な関数宣言は禁止されました。

残念ながら、バージョン5.xxまでのGCCはデフォルトで古いC標準を使用しています。標準C以外のコードとしてコードをコンパイルする必要がある理由はおそらくないでしょう。そのため、25年以上前の非標準のGNUがらくたではなく、コードを最新のCコードとしてコンパイルするようにGCCに明示的に指示する必要があります。 。

プログラムを常に次のようにコンパイルして、問題を修正します。

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 (現在の)C標準(非公式にはC11として知られています)に従ってコンパイルする中途半端な試みを行うように指示します。
  • -pedantic-errors 上記のことを真剣に行うように指示し、C標準に違反する不正なコードを記述した場合はコンパイラエラーを生成します。
  • -Wall ということは私にいくつかの追加の警告を与えるのが良いかもしれないということです。
  • -Wextra つまり、他にも警告を表示しておくとよいかもしれません。

19
この答えは全体的に正しいですが、次のいずれかまたはすべてが原因-std=gnu11-std=c11、より複雑なプログラムの方が予想どおりに機能する可能性が高くなります。「gnu」で利用可能なC11(POSIX、X / Openなど)を超えるライブラリ機能が必要拡張モードですが、厳密な適合モードでは抑制されます。拡張モードでは非表示になっているシステムヘッダーのバグ。トリグラフの意図しない使用(この標準の誤機能は「gnu」モードでは無効になっています)。
zwol

5
同様の理由で、私は通常、高レベルの警告を使用することをお勧めしますが、警告-エラーモードの使用はサポートできません。 -pedantic-errorsはそれほど面倒-Werrorではありませんが、実際の問題がない場合でも、元の作成者のテストに含まれていないオペレーティングシステムでは、プログラムのコンパイルが失敗する可能性があります。
zwol

7
@Lundinそれどころか、私が言及した2番目の問題(厳密な適合モードによって公開されるシステムヘッダーのバグ)は至る所にあります。私は広範囲にわたる体系的なテストを実施しましたが、そのようなバグが少なくとも1つない広く使用されているオペレーティングシステムはありません(とにかく2年前の時点で)。C11の機能のみを必要とし、それ以上の追加を必要としないCプログラムも、私の経験では規則ではなく例外です。
zwol

6
@joop標準のC bool/ を使用している場合_Bool、Cコードを「C ++風」の方法で記述できます。この場合、歴史的な理由により、すべての比較と論理演算子boolはCでincを返しますが、Cで同様の結果を返すと想定します。int。これには、静的分析ツールを使用して、そのようなすべての式の型安全性をチェックし、コンパイル時にあらゆる種類のバグを公開できるという大きな利点があります。また、自己文書化コードの形で意図を表現する方法でもあります。そしてそれほど重要ではありませんが、RAMの数バイトも節約できます。
ランディン

7
C99の新しいもののほとんどは、25歳以上のGNUがらくたからのものであることに注意してください。
Shahbaz

141

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

場合intbool異なるサイズのものであり、これはになります未定義の動作。たとえば、私のマシンでintは、4バイトでbool1バイトです。関数はreturn として定義されているため、戻るboolときに1バイトをスタックに入れます。ただし、main.c から戻るように暗黙的に宣言されているためint、呼び出し元の関数はスタックから4バイトを読み取ろうとします。

gccのデフォルトのコンパイラオプションは、これを行っていることを通知しません。しかし、でコンパイルすると-Wall -Wextra、次のようになります。

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

これを修正するにはf1、main.cの前にの宣言を追加しますmain

bool f1(void);

引数リストが明示的にに設定されているためvoid、引数が不明であることを意味する空のパラメーターリストとは対照的に、関数は引数を取らないことをコンパイラーに伝えます。f1f1.c の定義もこれを反映するように変更する必要があります。


2
私が自分のプロジェクトで使用していたもの(まだGCCを使用していたとき)が-Werror-implicit-function-declarationGCCのオプションに追加されました。これにより、このオプションが過去のものになることはありません。さらに良い選択は-Werror、すべての警告をエラーに変えることです。警告が表示されたときにすべての警告を修正するように強制します。
uliwitness 2016

2
空の括弧は廃止予定の機能であるため、使用しないでください。つまり、C標準の次のバージョンでそのようなコードを禁止できるということです。
ランディン

1
@uliwitnessああ。Cで手を
出す

通常、戻り値はスタックではなく、レジスターに入れられます。オーエンの答えを見てください。また、通常はスタックに1バイトを入れるのではなく、ワードサイズの倍数にします。
rsanchez

GCCの新しいバージョン(5.xx)では、追加のフラグなしで警告が出されます。
Overv

36

Lundinの優れた回答で言及されているサイズの不一致が実際にどこで発生するかを確認することは興味深いと思います。

でコンパイルすると--save-temps、表示できるアセンブリファイルが作成されます。ここでは一部だf1()== 0、比較し、その値を返しますが:

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

返却部分はsete %alです。Cのx86呼び出し規約では、4バイト以下の戻り値(intおよびを含むbool)がregisterを介して返され%eaxます。%alの最下位バイトです%eax。したがって、の上位3バイトは%eax制御されない状態のままになります。

現在main()

call    f1
testl   %eax, %eax
je  .L2

どうか、このチェック全体のは%eax、それがint型のテストだと考えているので、ゼロです。

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

call    f1
testb   %al, %al
je  .L2

これが私たちの望みです。


27

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

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ください、それはあなたのお尻の多くの苦痛を節約します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.