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

$ 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
1
Можно main()было бы упростить до int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- это лучше показало бы несоответствие.
Palec
@uliwitness Как насчет K & R 1-е изд. (1978), когда не было void?
Ho1
@uliwitness Не было trueи falseв K & R 1-е изд., поэтому таких проблем не было вообще. Это был просто 0 и ненулевой для истины и ложи. Не так ли? Я не знаю, были ли доступны прототипы в то время.
Ho1
1
K & R 1st Edn предшествовал прототипам (и стандарту C) более чем на десятилетие (1978 г. для книги против 1989 г. для стандарта) - действительно, C ++ (C с классами) был еще в будущем, когда K & R1 была опубликована. Также до C99 не было ни _Boolтипа, ни <stdbool.h>заголовка.
Джонатан Леффлер

Ответы:

396

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

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

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

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

Исправьте проблему, всегда компилируя вашу программу как:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 говорит ему сделать нерешительную попытку компилирования в соответствии с (текущим) стандартом C (неофициально известным как C11).
  • -pedantic-errors говорит ему от всего сердца сделать вышеизложенное и выдавать ошибки компилятора, когда вы пишете неправильный код, который нарушает стандарт Си.
  • -Wall означает дать мне несколько дополнительных предупреждений, которые могут быть полезны.
  • -Wextra означает дать мне несколько дополнительных предупреждений, которые могут быть полезны.
Лундин
источник
19
Этот ответ в целом правильный, но для более сложных программ -std=gnu11гораздо более вероятно, что они будут работать -std=c11должным образом, чем из-за какой-либо или всех из них: необходимая функциональность библиотеки за пределами C11 (POSIX, X / Open и т. Д.), Доступная в «gnu» расширенные режимы, но подавленные в режиме строгого соответствия; ошибки в системных заголовках, которые скрыты в расширенных режимах, например, при допущении наличия нестандартных typedefs; непреднамеренное использование триграфов (этот стандартный сбой отключен в режиме "gnu").
zwol
5
По тем же причинам, хотя я обычно поощряю использование высоких уровней предупреждений, я не могу поддерживать использование режимов предупреждений-ошибок. -pedantic-errorsэто менее хлопотно, чем то, -Werrorно оба могут и не приводить к тому, что программы не могут компилироваться в операционных системах, которые не включены в первоначальное авторское тестирование, даже когда нет реальной проблемы.
zwol
7
@Lundin Напротив, вторая проблема, которую я упомянул (ошибки в системных заголовках, которые обнаруживаются в режимах строгого соответствия), повсеместна ; Я провел обширное систематическое тестирование, и нет широко используемых операционных систем, в которых бы не было хотя бы одной такой ошибки (по крайней мере, два года назад). Программы на C, которые требуют только функциональности C11, без каких-либо дополнительных дополнений, также являются скорее исключением, чем правилом из моего опыта.
zwol
6
@joop Если вы используете стандартный C bool/, _Boolто вы можете написать свой код C "C ++ - esque", где вы предполагаете, что все сравнения и логические операторы возвращают boolаналог в C ++, даже если они возвращают intв C по историческим причинам. , Это имеет большое преимущество, заключающееся в том, что вы можете использовать инструменты статического анализа для проверки безопасности типов всех таких выражений и выявлять все виды ошибок во время компиляции. Это также способ выразить намерение в виде самодокументируемого кода. И что не менее важно, он также экономит несколько байтов оперативной памяти.
Лундин
7
Обратите внимание, что большая часть нового материала в C99 пришла из этого более чем 25-летнего дерьма GNU.
Шахбаз
141

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

Если intи boolимеют разные размеры, это приведет к неопределенному поведению . Например, на моей машине 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 также должно быть изменено, чтобы отразить это.

dbush
источник
2
Что-то, что я делал в своих проектах (когда я все еще использовал GCC), было добавлено -Werror-implicit-function-declarationк настройкам GCC, так что этот больше не ускользает. Еще лучший выбор - -Werrorпревратить все предупреждения в ошибки. Заставляет вас исправить все предупреждения, когда они появляются.
Uliwitness
2
Вы также не должны использовать пустые скобки, потому что это устаревшая функция. Это означает, что они могут запретить такой код в следующей версии стандарта C.
Лундин
1
@ Aliwitness Ах. Хорошая информация для тех, кто пришел из C ++, кто только балуется на C.
SeldomNeedy
Возвращаемое значение обычно помещается не в стек, а в регистр. Смотрите ответ Оуэна. Кроме того, вы обычно никогда не помещаете в стек один байт, но кратный размеру слова.
rsanchez
Более новые версии GCC (5.xx) выдают это предупреждение без дополнительных флагов.
Overv
36

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

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

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

Возвращающая часть есть sete %al. В соглашениях о вызовах x86 в C возвращаемые значения размером 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

что мы и хотим

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

jdarthenay
источник