W lekcji 3.1 — Błędy składniowe i semantyczne, omówiliśmy syntax errors, które występują podczas pisania kodu, który nie jest poprawny zgodnie z gramatyką języka C++. Kompilator powiadomi Cię o takich błędach, więc są one łatwe do wyłapania i zazwyczaj łatwe do naprawienia.
Omówiliśmy także semantic errors, które pojawiają się, gdy piszesz kod, który nie robi tego, co zamierzałeś. Kompilator generalnie nie wyłapie błędów semantycznych (chociaż w niektórych przypadkach inteligentne kompilatory mogą być w stanie wygenerować ostrzeżenie).
Błędy semantyczne mogą powodować większość takich samych symptomów jak undefined behavior, takich jak spowodowanie, że program będzie generował błędne wyniki, błędne zachowanie, uszkodzenie danych programu, awarię programu - lub mogą nie mieć żadnego wpływu.
Podczas pisania programów prawie nieuniknione jest, że zrobisz to błędy semantyczne. Prawdopodobnie zauważysz niektóre z nich po prostu korzystając z programu: na przykład, jeśli piszesz grę w labirynty, a twoja postać potrafi przechodzić przez ściany. Testowanie programu (9.1 -- Wprowadzenie do testowania kodu) może również pomóc w wykryciu błędów semantycznych.
Ale jest jeszcze jedna rzecz, która może pomóc — wiedza o tym, jakie typy błędów semantycznych występują najczęściej, dzięki czemu możesz poświęcić trochę więcej czasu na upewnienie się, że w takich przypadkach wszystko działa prawidłowo.
W tej lekcji omówimy kilka najpopularniejszych typów błędów semantycznych występujących w C++ (z których większość musi wystarczyć z kontrolą przepływu).
Warunkowe błędy logiczne
Jednym z najczęstszych rodzajów błędów semantycznych są warunkowe błędy logiczne. błąd logiki warunkowej występuje, gdy programista błędnie koduje logikę instrukcji warunkowej lub warunku pętli. Oto prosty przykład:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
if (x >= 5) // oops, we used operator>= instead of operator>
std::cout << x << " is greater than 5\n";
return 0;
}Oto wykonanie programu, który wykazuje błąd logiki warunkowej:
Enter an integer: 5 5 is greater than 5
Gdy użytkownik wprowadzi 5 wyrażenie warunkowe x >= 5 wyniesie true, w związku z czym wykonywana jest powiązana instrukcja.
Oto kolejny przykład z wykorzystaniem pętli for:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
// oops, we used operator> instead of operator<
for (int count{ 1 }; count > x; ++count)
{
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}Ten program ma wypisać wszystkie liczby pomiędzy 1 i numer wprowadzony przez użytkownika. Ale oto, co faktycznie robi:
Enter an integer: 5
Nie drukuje niczego. Dzieje się tak, ponieważ przy wejściu do pętli for count > x Jest false pętla w ogóle nie wykonuje iteracji.
Nieskończone pętle
W lekcji 8.8 — Wprowadzenie do pętli i instrukcji while omówiliśmy nieskończone pętle i pokazaliśmy ten przykład:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // this condition will never be false
{
std::cout << count << ' '; // so this line will repeatedly execute
}
std::cout << '\n'; // this line will never execute
return 0; // this line will never execute
}W tym przypadku zapomnieliśmy o inkrementacji count, więc warunek pętli nigdy nie będzie fałszywe i pętla będzie drukowana:
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
… dopóki użytkownik nie zamknie programu.
Oto kolejny przykład, który nauczyciele uwielbiają zadawać w formie pytań quizowych. Co jest nie tak z następującym kodem?
#include <iostream>
int main()
{
for (unsigned int count{ 5 }; count >= 0; --count)
{
if (count == 0)
std::cout << "blastoff! ";
else
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}Ten program powinien wydrukować 5 4 3 2 1 blastoff! i to robi, ale na tym się nie kończy. W rzeczywistości wypisuje:
5 4 3 2 1 blastoff! 4294967295 4294967294 4294967293 4294967292 4294967291
, a następnie po prostu zmniejsza wartość. Program nigdy się nie zakończy, ponieważ count >= 0 nie może być false gdy count jest liczbą całkowitą bez znaku.
Błędy o jeden
An błąd o jeden to błąd pojawiający się, gdy pętla wykona się o jeden raz za dużo, raz za mało. Oto przykład, który omówiliśmy w lekcji 8.10 -- Dla instrukcji:
#include <iostream>
int main()
{
for (int count{ 1 }; count < 5; ++count)
{
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}Programista zaplanował wydruk tego kodu 1 2 3 4 5. Jednakże użyto błędnego operatora relacyjnego (< zamiast <=), więc pętla wykonuje się o jeden mniej razy niż zamierzono, wypisując 1 2 3 4.
Nieprawidłowe pierwszeństwo operatora
Z lekcji 6.8 -- Operatory logiczne następujący program popełnia błąd pierwszeństwa operatora:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!x > y) // oops: operator precedence issue
std::cout << x << " is not greater than " << y << '\n';
else
std::cout << x << " is greater than " << y << '\n';
return 0;
}Ponieważ logical NOT ma wyższy priorytet niż operator>, warunek ocenia tak, jakby był napisany (!x) > y, co nie jest zamierzone przez programistę.
W rezultacie ten program wypisuje:
5 is greater than 7
Może się to również zdarzyć podczas mieszania logicznego OR i logicznego AND w tym samym wyrażeniu (logiczne ORAZ ma pierwszeństwo przed logicznym OR). Używaj jawnych nawiasów, aby uniknąć tego rodzaju błędów.
Problemy z precyzją w typach zmiennoprzecinkowych
Następująca zmienna zmiennoprzecinkowa nie ma wystarczającej precyzji, aby zapisać całą liczbę:
#include <iostream>
int main()
{
float f{ 0.123456789f };
std::cout << f << '\n';
return 0;
}Z powodu tego braku precyzji liczba jest lekko zaokrąglana:
0.123457
W lekcji 6.7 -- Operatory relacyjne i zmiennoprzecinkowe porównania, rozmawialiśmy o tym, jak użycie operator== i operator!= może być problematyczne w przypadku liczb zmiennoprzecinkowych z powodu małych błędów zaokrągleń (również co z tym zrobić). Oto przykład:
#include <iostream>
int main()
{
double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should sum to 1.0
if (d == 1.0)
std::cout << "equal\n";
else
std::cout << "not equal\n";
return 0;
}Ten program wypisuje:
not equal
Im więcej arytmetyki wykonasz na liczbie zmiennoprzecinkowej, tym więcej będzie kumulować się małych błędów zaokrągleń.
Dzielenie na liczbach całkowitych
W poniższym przykładzie mamy zamiar wykonać dzielenie zmiennoprzecinkowe, ale ponieważ oba operandy są liczbami całkowitymi, zamiast tego wykonamy dzielenie całkowite:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 3 };
std::cout << x << " divided by " << y << " is: " << x / y << '\n'; // integer division
return 0;
}Wypisuje:
5 divided by 3 is: 1
W lekcji 6.2 — Operatory arytmetyczne pokazaliśmy, że możemy użyć static_cast do przekształcenia jednego z całkowitych operandów na wartość zmiennoprzecinkową w celu wykonania dzielenia zmiennoprzecinkowego.
Przypadkowe instrukcje zerowe
W lekcji 8.3 -- Wspólna instrukcja if problemy, omówiliśmy null statements, które są instrukcjami, które nic nie robią.
W poniższym programie chcemy wysadzić świat w powietrze tylko wtedy, gdy mamy pozwolenie użytkownika:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c == 'y'); // accidental null statement here
blowUpWorld(); // so this will always execute since it's not part of the if-statement
return 0;
}Jednakże ze względu na przypadkowo null statement, wywołanie funkcji blowUpWorld() jest zawsze wykonywane, więc wysadzamy to w powietrze niezależnie od:
Should we blow up the world again? (y/n): n Kaboom!
Nieużywanie instrukcji złożonej, gdy jest ona wymagana
Inny wariant powyższego programu, który zawsze wysadza w powietrze świat:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c == 'y')
std::cout << "Okay, here we go...\n";
blowUpWorld(); // Will always execute. Should be inside compound statement.
return 0;
}Ten program wypisuje:
Should we blow up the world again? (y/n): n Kaboom!
A dangling else (opisane w lekcji 8.3 -- Wspólna instrukcja if problemy) również należy do tej kategorii.
Używanie przypisania zamiast równości w warunku
Ponieważ operator przypisania (=) i operator równości (==) są podobne, możemy chcieć użyć równości, ale przypadkowo zamiast tego użyliśmy przypisania:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c = 'y') // uses assignment operator instead of equality operator
blowUpWorld();
return 0;
}Ten program wypisuje:
Should we blow up the world again? (y/n): n Kaboom!
Operator przypisania zwraca lewą stronę operand. c = 'y' wykonuje się jako pierwszy, który przypisuje y Do c i zwraca c. Następnie if (c) jest oceniany. Ponieważ wartość c jest teraz różna od zera, jest ona niejawnie konwertowana na bool wartości true i wykonywana jest instrukcja powiązana z instrukcją if.
Ponieważ przypisanie wewnątrz warunku prawie nigdy nie jest zamierzone, nowoczesne kompilatory często ostrzegają, gdy się z tym spotkają. Jeśli jednak nie masz w zwyczaju rozwiązywać wszystkich swoich ostrzeżeń, ostrzeżenia takie mogą łatwo zostać utracone.
Zapominanie o użyciu operatora wywołania funkcji podczas wywoływania funkcji
#include <iostream>
int getValue()
{
return 5;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}Chociaż możesz oczekiwać, że ten program wydrukuje 5, najprawdopodobniej wydrukuje 1 (w niektórych kompilatorach wydrukuje adres pamięci w hex).
Zamiast używać getValue() (które wywołałoby funkcję i wygenerowało int wartość zwracaną), użyliśmy getValue bez operatora wywołania funkcji. W wielu przypadkach spowoduje to, że wartość zostanie przekonwertowana na bool wartości true).
W powyższym przykładzie to właśnie bool wartości true jest wyprowadzana, a na wydruku 1.
Dla zaawansowanych czytelników
Użycie nazwy funkcji bez jej wywołania zazwyczaj daje wskaźnik funkcji przechowujący adres funkcji. Taki wskaźnik funkcji zostanie niejawnie przekonwertowany na wartość bool . A ponieważ ten wskaźnik nigdy nie powinien mieć adresu 0, ta bool wartość zawsze będzie true.
Wskaźniki funkcji omówimy w lekcji 20.1 -- Wskaźniki funkcji.
Co jeszcze?
Powyższe stanowi dobrą próbkę najczęstszego rodzaju błędów semantycznych, które popełniają nowi programiści C++, ale jest ich znacznie więcej. Czytelnicy, jeśli macie jakieś dodatkowe, które uważacie za typowe pułapki, zostawcie notatkę w komentarzach.

