Najczęstsze bugi
Poniższa lista, stałe work in progress, zbiera błędy popełniane przez użytkowników, które pojawiają się na forum często i regularnie. By wykryć wiele z nich, wystarczy mieć włączone ostrzeżenia kompilatora i zwracać na nie uwagę od czasu do czasu. Zwłaszcza wtedy, gdy coś nie działa..
Średnik po if, while
Problem szczególnie popularny wśród początkujących, którzy nie wiedzą jeszcze co znaczy średnik w C++, więc wstawiają go odruchowo i jak popadnie. Średnik sam z siebie oznacza instrukcję pustą. Dlatego coś takiego działa poprawnie:
std::cout << "Hello";;;;;;;
To jest wypisanie tekstu i 6 instrukcji pustych. Można dość do wniosku, że niepotrzebne średniki są ignorowane, ale jeśli postawimy średnik po
if,
while itp, to warunkowi, czy pętli, będzie podlegać właśnie instrukcja pusta, a nie to, co chcieliśmy.
if( wiek >= 18 );
std::cout << "Mozesz kupic alkohol";
Zmienne przechowują wartości. Nie wyrażenia!
int a, b;
int wynik = a + b;
std::cin >> a >> b;
"Wynik to suma a i b, więc wczytam a i b i wynik będzie zawierać sumę.." NIE.
wynik przechowuje
wartość, wynik sumowania wartości
a i
b, jakie by one w tym momencie nie były. Te zmienne są w tym jednym momencie użyte do zsumowania
i to jest koniec. Wynik się magicznie nie zaktualizuje, gdy zmienne użyte kiedyś do obliczenia go przypadkiem zmieniły wartości. Jak jakaś operacja na zmiennych ma mieć sens, wartości zmiennych muszą być w tym momencie już obliczone.
Zły operator
if( i = 5 )
std::cout << "jest równe 5";
Czy ten warunek porównuje
i do 5? Nie, ten warunek przypisuje 5 do
i i przez to jest zawsze prawdziwy. Do
if nie podaje się porównania, tylko wyrażenie - porównanie, ale nie tylko. Przejdzie wszystko, co ma wartość konwertowalną do wartości logicznej. Wartością przypisania jest wartość zmiennej po przypisaniu, więc
i = 1 jest warunkiem zawsze prawdziwym,
i = 0 jest warunkiem zawsze fałszywym.
warning: suggest parentheses around assignment used as truth value [-Wparentheses]
if(i = 5)
^
Brak nawiasów klamrowych
if(a > 100)
std::cout << "a > 100, ustawiam na 0\n";
a = 0;
// coś z `a`
Wcięcia mają znaczenie dla czytelności kodu, ale C++ to nie Python, gdzie mają także wpływ na znaczenie programu. Aby wiele instrukcji było podporządkowanych warunkowi, pętli, etc, trzeba je zamknąć w blok kodu
{}, a nie tylko postawić obok siebie. Po sformatowaniu kodu na forum, przypisanie do
a jest już wcięte w bardziej odpowiedni sposób.
if( a > 100 )
std::cout << "a > 100, ustawiam na 0\n";
a = 0;
Dzielenie bez ułamków
Weźmy początkującego programistę z zamiłowaniem do ułamków zwykłych i o taki błąd nietrudno. Tworzymy zmienną
float, a więc zdolną przechowywać wartości "z ułamkiem" i przypisujemy do niej ułamek. Problem w tym, że po takim zabiegu,
half ma wartość zero, bo w C++ nie ma ułamków zwykłych. Jest za to dzielenie 1 przez 2, obie wartości są typu
int, a dzielenie między liczbami o typach całkowitych, daje całkowity wynik. Część ułamkowa liczby jest więc tracona, bez zaokrąglania (9/10 to też 0).
Żeby taki zapis zadziałał zgodnie z intencjami, przynajmniej jedna z tych liczb musi być typu zmiennoprzecinkowego. Można to osiągnąć rzutowaniem, ale znacznie krócej jest użyć literałów zmiennoprzecinkowych:
Porównywanie liczb zmiennoprzecinkowych
float f = 0;
for( int i = 0; i < 10; ++i )
f += 0.1f;
std::cout << "Wynik: " << f << " czy rowne 1? " <<( f == 1.f ? "tak": "nie" );
Wynik: 1 czy rowne 1? nie
Wielu błędnie zakłada, że odpowiedź brzmi "tak". Tak mówi matematyka. Matematyka mówi wiele ciekawych rzeczy. Na przykład, że jest nieskończona ilość liczb rzeczywistych, ale bity w zmiennej
float mają tylko 2
32 możliwych kombinacji. Blado to wypada w porównaniu do konceptu nieskończoności. Liczba
0.1 jest dobra do tego przykładu, bo wygląda niewinnie. Ma krótkie rozwinięcie dziesiętne, ale w systemie dwójkowym, ma nieskończone rozwinięcie. Każde dodawanie w tej pętli jest obarczone błędem obliczeń, więc uzyskamy liczbę bliską 1 (na tyle bliską, że przy wyświetlaniu będzie zaokrąglona), ale jednak nie równą 1.
1.19209e-007
Jeśli naprawdę musisz porównać liczby w ten sposób, musisz określić tolerancję na błędy. Na przykład sprawdzając, czy wartość bezwzględna z różnicy dwóch liczb jest mniejsza od jakiegoś
Epsilon. W tym wypadku tą wartością może być
1e-6 (10
-6), ale czasem potrzeba większej, lub mniejszej tolerancji, zwłaszcza przy zmianie typu na taki o większej, lub mniejszej, precyzji.
std::cout << "Czy mniej wiecej rowne 1? " <<( fabs( f - 1 ) < Epsilon ? "tak"
: "nie" );
Czy mniej wiecej rowne 1? tak
Niezwracanie wartości z funkcji
int signum( int arg )
{
if( arg > 0 )
return 1;
if( arg < 0 )
return - 1;
}
Funkcja ma zwracać wartość typu
int i faktycznie zwraca. Gdyby nie zwracała, pojawiło by się takie ostrzeżenie
warning: no return statement in function returning non-void [-Wreturn-type]
}
^
Tu problemem jest to, że istnieją takie przypadki, w których funkcja nic nie zwraca. Kompilatory są na tyle inteligentne, że wykrywają takie błędy. Tutaj to łatwo zauważyć, ale kiedy funkcja jest długa i problem jest dla jednej wartości z N możliwych, da się to przeoczyć, zapomnieć o tym, czy zostawić na później i do tego nie wrócić.
warning: control reaches end of non-void function [-Wreturn-type]
Zwracanie referencji/wskaźnika na zmienną lokalną
int & f()
{
int a = 2;
return a;
}
W momencie zwrócenia wartości, zmienna
a przestaje istnieć, więc odwołanie przez zwróconą referencję, lub wskaźnik, będzie błędne.
In function 'int& f()':
warning: reference to local variable 'a' returned [-Wreturn-local-addr]
In function 'int* f()':
warning: address of local variable 'a' returned [-Wreturn-local-addr]