Эта функция 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;
}

Это всегда должно возвращать 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. Но после компиляции и запуска его также выполняется:

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

Почему? Этот код имеет своего рода поведение undefined?

Примечание. Я скомпилировал его с помощью gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

Ответ 1

Как отмечено в других ответах, проблема заключается в том, что вы используете gcc без каких-либо параметров компилятора. Если вы это сделаете, по умолчанию используется так называемое "gnu90", что является нестандартной реализацией старого, снятого стандарта C90 с 1990 года.

В старом стандарте C90 был серьезный недостаток на языке C: если вы не объявили прототип перед использованием функции, по умолчанию он будет int func () (где ( ) означает "принять любой параметр" ), Это изменяет вызывающее соглашение функции func, но не изменяет фактическое определение функции. Поскольку размер bool и int различен, ваш код вызывает поведение undefined при вызове функции.

Это опасное бессмысленное поведение было зафиксировано в 1999 году с выпуском стандарта C99. Неявные декларации функций были запрещены.

К сожалению, GCC до версии 5.x.x по-прежнему использует старый стандарт C по умолчанию. Вероятно, нет причин, по которым вам нужно будет скомпилировать ваш код как ничего, кроме стандартного C. Таким образом, вы должны явно указать GCC, что он должен скомпилировать ваш код как современный C-код вместо 25-летнего нестандартного GNU дерьма.

Устраните проблему, всегда компилируя свою программу как:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 говорит, что он делает половинчатую попытку скомпилировать согласно (текущему) стандарту C (неофициально известному как C11).
  • -pedantic-errors рассказывает об этом полностью и делает ошибки компилятора, когда вы пишете неправильный код, который нарушает стандарт C.
  • -Wall означает дать мне несколько дополнительных предупреждений, которые могут быть хорошими.
  • -Wextra означает дать мне несколько других дополнительных предупреждений, которые могут быть полезны.

Ответ 2

У вас нет прототипа, объявленного для f1() в main.c, поэтому он неявно определяется как int f1(), что означает, что это функция, которая принимает неизвестное количество аргументов и возвращает int.

Если int и bool имеют разные размеры, это приведет к undefined поведению. Например, на моей машине int - 4 байта, а bool - один байт. Поскольку функция определена для возврата bool, она возвращает один байт в стек, когда он возвращается. Однако, поскольку он неявно объявлен для возврата int из main.c, вызывающая функция попытается прочитать 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, который сообщает компилятору, что функция не принимает никаких аргументов, в отличие от пустого списка параметров, что означает неизвестное количество аргументов. Определение f1 в f1.c также должно быть изменено, чтобы отразить это.

Ответ 3

Я думаю, что интересно посмотреть, где на самом деле происходит несоответствие размера, упомянутое в Lundin, на самом деле происходит.

Если вы скомпилируете с помощью --save-temps, вы получите сборные файлы, на которые вы можете посмотреть. Здесь часть, где f1() выполняет сравнение == 0 и возвращает ее значение:

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

Возвращаемая часть sete %al. В условных вызовах C x86 возвращаемые значения 4 байта или меньше (включая int и bool) возвращаются через регистр %eax. %al - младший байт %eax. Таким образом, верхние 3 байта %eax остаются в неконтролируемом состоянии.

Теперь в main():

call    f1
testl   %eax, %eax
je  .L2

Это проверяет, является ли все %eax равным нулю, поскольку он считает, что он тестирует int.

Добавление явного объявления функции изменяет main() на:

call    f1
testb   %al, %al
je  .L2

что мы хотим.

Ответ 4

Скомпилируйте с помощью такой команды:

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, это сэкономит вам много боли в заднице.