Ця функція C завжди повинна повертати помилкове значення, але це не так


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; }- це краще показало б розбіжність.
Палець

@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 повідомляє це відверто робити вищезазначене, і видавати помилки компілятора, коли ви пишете неправильний код, який порушує стандарт C.
  • -Wall означає, дайте мені додаткові попередження, які, можливо, добре мати.
  • -Wextra означає, дайте мені ще кілька додаткових попереджень, які, можливо, добре мати.

19
Ця відповідь в цілому правильна, але для складніших програм -std=gnu11набагато більше шансів працювати, як очікувалося, ніж -std=c11через будь-яку або всі: необхідність функціонування бібліотеки за межами C11 (POSIX, X / Open тощо), яка доступна в "gnu" розширені режими, але пригнічені в режимі суворого відповідності; помилки в заголовках системи, які приховані в розширених режимах, наприклад, припускаючи наявність нестандартних типівdedefs; ненавмисне використання триграфа (ця стандартна неправильна функція вимкнена в режимі "гну").
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 надійшла від того глупого GNU у віці 25 років.
Шахбаз

141

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

Якщо intі boolмають різний розмір, це призведе до невизначеної поведінки . Наприклад, на моїй машині int4 байти і boolє один байт. Оскільки функція визначена для повернення bool, вона кладе один байт у стек при поверненні. Однак, оскільки неявно оголошено повернення intз main.c, функція виклику спробує прочитати 4 байти зі стека.

Параметри компіляторів за замовчуванням у gcc не скажуть вам, що це робиться. Але якщо ви компілюєте -Wall -Wextra, ви отримаєте це:

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

Щоб виправити це, додайте декларацію для f1main.c перед main:

bool f1(void);

Зауважте, що в списку аргументів явно встановлено void, що повідомляє компілятору, що функція не бере аргументів, на відміну від порожнього списку параметрів, що означає невідому кількість аргументів. Визначення f1у f1.c також слід змінити, щоб відобразити це.


2
Щось, що я робив у своїх проектах (коли я ще використовував GCC), було додано -Werror-implicit-function-declarationдо варіантів GCC, таким чином ця вже не проходить повз. Ще кращим вибором є -Werrorперетворення всіх попереджень на помилки. Вимушує виправляти всі попередження, коли вони з’являються.
uliwitness

2
Ви також не повинні використовувати порожні дужки, оскільки це є застарілою функцією. Це означає, що вони можуть заборонити такий код у наступній версії стандарту C.
Лундін

1
@uliwitness Ага. Хороша інформація для тих, хто приїжджає з C ++, які лише
балакають

Повернене значення зазвичай не вкладається в стек, а в регістр. Дивіться відповідь Оуена. Крім того, зазвичай ви ніколи не ставите один байт у стек, а кратний розміру слова.
rsanchez

Новіші версії GCC (5.xx) дають це попередження без зайвих прапорів.
Перебіг

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це позбавить вас від болю в дупі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.