Ta funkcja C powinna zawsze zwracać wartość false, ale tak nie jest


317

Dawno temu natknąłem się na interesujące pytanie na forum i chcę poznać odpowiedź.

Rozważ następującą funkcję C:

f1.c

#include <stdbool.h>

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

Powinno to zawsze powrócić falseod var3 == 3000. mainFunkcja wygląda tak:

main.c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

Ponieważ f1()zawsze powinien wracać false, można by oczekiwać, że program wydrukuje tylko jeden fałsz na ekranie. Ale po skompilowaniu i uruchomieniu, wykonane jest również wyświetlane:

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

Dlaczego? Czy ten kod ma jakieś niezdefiniowane zachowanie?

Uwaga: skompilowałem to gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.


9
Inni wspominali, że potrzebujesz prototypu, ponieważ twoje funkcje są w osobnych plikach. Ale nawet jeśli skopiowałeś f1()do tego samego pliku main(), dostałbyś pewnej dziwności: Chociaż w C ++ poprawne jest używanie ()pustej listy parametrów, w C, która jest używana dla funkcji z nieokreśloną jeszcze listą parametrów ( w zasadzie oczekuje listy parametrów w stylu K&R po )). Aby być poprawnym C, powinieneś zmienić swój kod na bool f1(void).
uliwitness

1
main()Można uprościć do int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- byłoby to pokazać różnicę lepiej.
Palec

@uliwitness Co z K&R 1st ed. (1978) kiedy nie było void?
Ho1

@uliwitness Nie było trueiw falseK&R 1st ed., więc nie było takich problemów. To było tylko 0 i niezerowe dla prawdy i fałszu. Czyż nie Nie wiem, czy prototypy były w tym czasie dostępne.
Ho1 11.04.16

1
K&R 1st Edn poprzedza prototypy (i standard C) o ponad dekadę (1978 dla książki w porównaniu do 1989 dla standardu) - w rzeczywistości C ++ (C z klasami) był jeszcze w przyszłości, kiedy K & R1 został opublikowany. Ponadto przed C99 nie było _Booltypu ani <stdbool.h>nagłówka.
Jonathan Leffler,

Odpowiedzi:


396

Jak zauważono w innych odpowiedziach, problemem jest to, że używasz gccgo bez ustawionych opcji kompilatora. Jeśli to zrobisz, domyślnie będzie to tak zwane „gnu90”, które jest niestandardową implementacją starego, wycofanego standardu C90 z 1990 roku.

W starym standardzie C90 występowała poważna wada w języku C: jeśli nie zadeklarowałeś prototypu przed użyciem funkcji, domyślnie byłby to int func ()(gdzie ( )oznacza „zaakceptuj dowolny parametr”). Zmienia to konwencję wywoływania funkcji func, ale nie zmienia faktycznej definicji funkcji. Ponieważ wielkość booli intsą różne, kod wywołuje niezdefiniowane zachowanie, gdy funkcja jest wywoływana.

To niebezpieczne nonsensowne zachowanie zostało naprawione w 1999 roku wraz z wydaniem standardu C99. Deklaracje funkcji niejawnych zostały zakazane.

Niestety, GCC do wersji 5.xx nadal domyślnie używa starego standardu C. Prawdopodobnie nie ma powodu, dla którego powinieneś chcieć skompilować swój kod jako coś innego niż standardowy C. Więc musisz wyraźnie powiedzieć GCC, że powinien skompilować twój kod jako nowoczesny kod C, zamiast około 25-letniej, niestandardowej bzdury GNU .

Rozwiąż problem, zawsze kompilując program jako:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 nakazuje mu podjąć bezmyślną próbę kompilacji zgodnie z (obecnym) standardem C (nieoficjalnie znanym jako C11).
  • -pedantic-errors każe mu z całego serca robić powyższe i dawać błędy kompilatora, gdy piszesz niepoprawny kod, który narusza standard C.
  • -Wall oznacza, że ​​podam kilka dodatkowych ostrzeżeń, które warto mieć.
  • -Wextra oznacza, że ​​podam kilka innych ostrzeżeń, które warto mieć.

19
Ta odpowiedź jest ogólnie poprawna, ale w przypadku bardziej skomplikowanych programów -std=gnu11jest o wiele bardziej prawdopodobne, że zadziała zgodnie z oczekiwaniami -std=c11, z powodu jednego lub wszystkich: potrzebujących funkcji biblioteki wykraczającej poza C11 (POSIX, X / Open itp.), Która jest dostępna w „GNU” tryby rozszerzone, ale pomijane w trybie ścisłej zgodności; błędy w nagłówkach systemowych, które są ukryte w trybach rozszerzonych, np. przy założeniu dostępności niestandardowych typów czcionek; niezamierzone użycie kaligrafii (ta standardowa pomyłka jest wyłączona w trybie „GNU”).
zwolnij

5
Z podobnych powodów, chociaż ogólnie zachęcam do korzystania z wysokich poziomów ostrzeżeń, nie mogę obsługiwać trybów ostrzeżenia-są-błędami. -pedantic-errorsjest mniej kłopotliwy niż -Werroroba, ale oba mogą powodować kompilację programów w systemach operacyjnych, które nie są objęte oryginalnymi testami autorskimi, i powodują ich, nawet jeśli nie ma rzeczywistego problemu.
zwolnij

7
@Lundin Przeciwnie, drugi problem, o którym wspomniałem (błędy w nagłówkach systemu, które są ujawniane przez tryby ścisłej zgodności) jest wszechobecny ; Przeprowadziłem obszerne, systematyczne testy i nie ma powszechnie używanych systemów operacyjnych, które nie mają co najmniej jednego takiego błędu (przynajmniej dwa lata temu). Programy C, które wymagają tylko funkcjonalności C11, bez dalszych dodatków, są również wyjątkiem, a nie regułą z mojego doświadczenia.
zwolnij

6
@joop Jeśli używasz standardowego C bool/ _Bool, możesz napisać swój kod C w sposób „C ++ - esque”, przy założeniu, że wszystkie porównania i operatory logiczne zwracają znak boolpodobny do C ++, nawet jeśli zwracają znak intC z przyczyn historycznych . Ma to tę wielką zaletę, że można użyć narzędzi analizy statycznej do sprawdzenia bezpieczeństwa typu wszystkich takich wyrażeń i ujawnienia różnego rodzaju błędów w czasie kompilacji. Jest to także sposób wyrażenia intencji w postaci kodu samokontrującego. I, co mniej ważne, oszczędza również kilka bajtów pamięci RAM.
Lundin,

7
Zauważ, że większość nowych rzeczy w C99 pochodzi z tego badziewnego GNU w wieku 25+.
Shahbaz

141

Nie masz zadeklarowanego prototypu f1()w main.c, więc jest domyślnie zdefiniowany jako int f1(), co oznacza, że ​​jest to funkcja, która pobiera nieznaną liczbę argumentów i zwraca an int.

Jeśli inti boolmają różne rozmiary, spowoduje to niezdefiniowane zachowanie . Na przykład na moim komputerze intjest 4 bajty i booljeden bajt. Ponieważ funkcja jest zdefiniowana , aby powrócić bool, stawia jeden bajt na stosie, kiedy wraca. Ponieważ jednak niejawnie zadeklarowano powrót intz main.c, funkcja wywołująca spróbuje odczytać 4 bajty ze stosu.

Domyślne opcje kompilatorów w gcc nie mówią, że to robi. Ale jeśli się skompilujesz -Wall -Wextra, otrzymasz:

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

Aby to naprawić, dodaj deklarację dla f1w main.c przed main:

bool f1(void);

Zauważ, że lista argumentów jest jawnie ustawiona na void, co informuje kompilator, że funkcja nie przyjmuje żadnych argumentów, w przeciwieństwie do pustej listy parametrów, co oznacza nieznaną liczbę argumentów. f1Należy również zmienić definicję w f1.c, aby to odzwierciedlić.


2
Coś, co robiłem w swoich projektach (kiedy jeszcze korzystałem z GCC), było dodatkiem -Werror-implicit-function-declarationdo opcji GCC, w ten sposób ten już się nie wymyka. Jeszcze lepszym wyborem jest -Werrorprzekształcenie wszystkich ostrzeżeń w błędy. Wymusza naprawienie wszystkich ostrzeżeń, gdy się pojawią.
uliwitness

2
Nie należy również używać pustych nawiasów, ponieważ jest to przestarzała funkcja. Oznacza to, że mogą zablokować taki kod w następnej wersji standardu C.
Lundin,

1
@uliwitness Ah. Dobra informacja dla tych, którzy pochodzą z C ++, którzy
bawią się

Zwracana wartość zwykle nie jest umieszczana w stosie, ale w rejestrze. Zobacz odpowiedź Owena. Ponadto zwykle nigdy nie umieszcza się jednego stosu na stosie, ale wielokrotności rozmiaru słowa.
rsanchez

Nowsze wersje GCC (5.xx) dają to ostrzeżenie bez dodatkowych flag.
Overv

36

Myślę, że ciekawie jest zobaczyć, gdzie faktycznie występuje niedopasowanie wielkości wspomniane w doskonałej odpowiedzi Lundina.

Jeśli się skompilujesz --save-temps, otrzymasz pliki asemblera, które możesz obejrzeć. Oto część, w której f1()dokonuje == 0porównania i zwraca jego wartość:

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

Powracającą częścią jest sete %al. W konwencjach wywoływania x86 C, zwracane są wartości 4 bajtów lub mniejsze (które zawierają inti bool) są zwracane przez rejestr %eax. %aljest najniższym bajtem z %eax. Tak więc górne 3 bajty %eaxpozostają w niekontrolowanym stanie.

Teraz w main():

call    f1
testl   %eax, %eax
je  .L2

To, czy kontrole cały of %eaxwynosi zero, ponieważ uważa, że to badanie int.

Dodanie jawnej deklaracji funkcji zmienia main()się w:

call    f1
testb   %al, %al
je  .L2

czego chcemy.


27

Skompiluj za pomocą polecenia takiego jak to:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Wynik:

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

Dzięki takiej wiadomości powinieneś wiedzieć, co zrobić, aby to poprawić.

Edycja: Po przeczytaniu (teraz usuniętego) komentarza próbowałem skompilować kod bez flag. To doprowadziło mnie do błędów linkera bez ostrzeżeń kompilatora zamiast błędów kompilatora. Te błędy linkera są trudniejsze do zrozumienia, więc nawet jeśli -std-gnu99nie jest to konieczne, spróbuj zawsze użyć przynajmniej -Wall -Werrorto pozwoli ci zaoszczędzić wiele bólu w dupie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.