Эта функция C всегда должна возвращать false, но не

310

Я давно наткнулся на интересный вопрос на форуме, и я хочу знать ответ.

Рассмотрим следующую 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.

  • 9
    Другие упоминали, что вам нужен прототип, потому что ваши функции находятся в отдельных файлах. Но даже если вы скопируете f1() в тот же файл, что и main() , вы получите некоторую странность: хотя в C ++ правильно использовать () для пустого списка параметров, в C, который используется для функции с еще не определенный список параметров (в основном он ожидает список параметров в стиле K & R после ) ). Чтобы быть правильным C, вы должны изменить свой код на bool f1(void) .
  • 1
    main() можно упростить до int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; } - это лучше показало бы несоответствие.
Показать ещё 4 комментария
Теги:
gcc

4 ответа

359
Лучший ответ

Как отмечено в других ответах, проблема заключается в том, что вы используете 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 означает дать мне несколько других дополнительных предупреждений, которые могут быть полезны.
  • 16
    Этот ответ в целом верен, но для более сложных программ -std=gnu11 гораздо вероятнее будет работать, как ожидается, чем -std=c11 , из-за какой-либо или всех из них: нуждающихся в функциональности библиотеки помимо C11 (POSIX, X / Open и т. Д.) это доступно в расширенных режимах "gnu", но подавлено в режиме строгого соответствия; ошибки в системных заголовках, которые скрыты в расширенных режимах, например, при предположении наличия нестандартных typedefs; непреднамеренное использование триграфов (эта стандартная ошибка в режиме «gnu» отключена).
  • 5
    По тем же причинам, хотя я обычно рекомендую использовать высокие уровни предупреждений, я не могу поддерживать использование режимов предупреждения-ошибки. -pedantic-errors меньше проблем, чем -Werror но и то и другое может привести к сбоям компиляции программ в операционных системах, которые не были включены в первоначальное авторское тестирование, даже когда проблем нет.
Показать ещё 19 комментариев
134

У вас нет прототипа, объявленного для 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 также должно быть изменено, чтобы отразить это.

  • 1
    Что-то, что я делал в своих проектах (когда я все еще использовал GCC), было добавление -Werror-implicit-function-declaration к опциям GCC, таким образом, этот больше не ускользает. Еще лучшим выбором является -Werror чтобы превратить все предупреждения в ошибки. Заставляет вас исправить все предупреждения, когда они появляются.
  • 2
    Вы также не должны использовать пустые скобки, потому что это устаревшая функция. Это означает, что они могут запретить такой код в следующей версии стандарта C.
Показать ещё 3 комментария
31

Я думаю, что интересно посмотреть, где на самом деле происходит несоответствие размера, упомянутое в 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

что мы хотим.

26

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню