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ć false
od var3 == 3000
. main
Funkcja 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
.
f1()
do tego samego plikumain()
, 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 nabool f1(void)
.main()
Można uprościć doint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }
- byłoby to pokazać różnicę lepiej.void
?true
iwfalse
K&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._Bool
typu ani<stdbool.h>
nagłówka.Odpowiedzi:
Jak zauważono w innych odpowiedziach, problemem jest to, że używasz
gcc
go 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 funkcjifunc
, ale nie zmienia faktycznej definicji funkcji. Ponieważ wielkośćbool
iint
są 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:
-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ć.źródło
-std=gnu11
jest 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”).-pedantic-errors
jest mniej kłopotliwy niż-Werror
oba, 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.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ą znakbool
podobny do C ++, nawet jeśli zwracają znakint
C 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.Nie masz zadeklarowanego prototypu
f1()
w main.c, więc jest domyślnie zdefiniowany jakoint f1()
, co oznacza, że jest to funkcja, która pobiera nieznaną liczbę argumentów i zwraca anint
.Jeśli
int
ibool
mają różne rozmiary, spowoduje to niezdefiniowane zachowanie . Na przykład na moim komputerzeint
jest 4 bajty ibool
jeden bajt. Ponieważ funkcja jest zdefiniowana , aby powrócićbool
, stawia jeden bajt na stosie, kiedy wraca. Ponieważ jednak niejawnie zadeklarowano powrótint
z 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:Aby to naprawić, dodaj deklarację dla
f1
w main.c przedmain
: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.f1
Należy również zmienić definicję w f1.c, aby to odzwierciedlić.źródło
-Werror-implicit-function-declaration
do opcji GCC, w ten sposób ten już się nie wymyka. Jeszcze lepszym wyborem jest-Werror
przekształcenie wszystkich ostrzeżeń w błędy. Wymusza naprawienie wszystkich ostrzeżeń, gdy się pojawią.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órejf1()
dokonuje== 0
porównania i zwraca jego wartość: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ąint
ibool
) są zwracane przez rejestr%eax
.%al
jest najniższym bajtem z%eax
. Tak więc górne 3 bajty%eax
pozostają w niekontrolowanym stanie.Teraz w
main()
:To, czy kontrole cały of
%eax
wynosi zero, ponieważ uważa, że to badanie int.Dodanie jawnej deklaracji funkcji zmienia
main()
się w:czego chcemy.
źródło
Skompiluj za pomocą polecenia takiego jak to:
Wynik:
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-gnu99
nie jest to konieczne, spróbuj zawsze użyć przynajmniej-Wall -Werror
to pozwoli ci zaoszczędzić wiele bólu w dupie.źródło