7.14 – Распространенные семантические ошибки при программировании на C++
В уроке «3.1 – Синтаксические и семантические ошибки» мы рассмотрели синтаксические ошибки, которые возникают, когда вы пишете код, который не соответствует грамматике языка C++. Компилятор уведомит вас об ошибках этого типа, поэтому их легко обнаружить и обычно легко исправить.
Мы также рассмотрели семантические ошибки, которые возникают, когда вы пишете код, который выполняет не то, что вы планировали. Как правило, компилятор не обнаруживает семантических ошибок (хотя в некоторых случаях умные компиляторы могут генерировать предупреждения).
Семантические ошибки могут вызывать большинство из симптомов неопределенного поведения, например, приводить к тому, что программа выдает неправильные результаты, быть причиной неустойчивого поведения, искажать данные программы, вызывать сбой программы – или они могут вообще никак не влиять.
При написании программ семантические ошибки практически неизбежны. Некоторые из них вы, вероятно, заметите, просто используя программу: например, если вы писали игру лабиринт, а ваш персонаж может проходить сквозь стены. Тестирование вашей программы (7.12 – Введение в тестирование кода) также может помочь выявить семантические ошибки.
Но есть еще одна вещь, которая может помочь – это знание того, какой тип семантических ошибок наиболее распространен, чтобы вы могли потратить немного больше времени на то, чтобы убедиться, что в этих случаях всё правильно.
В этом уроке мы рассмотрим ряд наиболее распространенных типов семантических ошибок, возникающих в C++ (большинство из которых так или иначе связаны с управлением порядком выполнения программы).
Условные логические ошибки
Один из наиболее распространенных типов семантических ошибок – это условная логическая ошибка. Условная логическая ошибка возникает, когда программист неправильно пишет логику условного оператора или условия цикла. Вот простой пример:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
if (x >= 5) // упс, мы использовали operator>= вместо operator>
std::cout << x << " is greater than 5";
return 0;
}
А вот результат запуска программы, при котором была обнаружена эта условная логическая ошибка:
Enter an integer: 5
5 is greater than 5
Когда пользователь вводит 5, условное выражение x >= 5
принимает значение true
, поэтому выполняется соответствующая инструкция.
Вот еще один пример для цикла for
:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
// упс, мы использовали operator> вместо operator<
for (unsigned int count{ 1 }; count > x; ++count)
{
std::cout << count << ' ';
}
return 0;
}
Эта программа должна напечатать все числа от 1 до числа, введенного пользователем. Но вот что она на самом деле делает:
Enter an integer: 5
Она ничего не напечатала. Это происходит потому, что при входе в цикл for
условие count > x
равно false
, поэтому цикл вообще не повторяется.
Бесконечные циклы
В уроке «7.7 – Введение в циклы и инструкции while
» мы рассмотрели бесконечные циклы и показали этот пример:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // это условие никогда не будет ложным
{
std::cout << count << ' '; // поэтому эта строка выполняется многократно
}
return 0; // эта строка никогда не будет выполнена
}
В этом случае мы забыли увеличить count
, поэтому условие цикла никогда не будет ложным, и цикл продолжит печатать:
1 1 1 1 1 1 1 1 1 1
пока пользователь не закроет программу.
Вот еще один пример, который преподаватели любят задавать в тестах. Что не так со следующим кодом?
#include <iostream>
int main()
{
for (unsigned int count{ 5 }; count >= 0; --count)
{
if (count == 0)
std::cout << "blastoff! ";
else
std::cout << count << ' ';
}
return 0;
}
Эта программа должна напечатать "5 4 3 2 1 blastoff!", что она и делает, но не останавливается на достигнутом. На самом деле она печатает:
5 4 3 2 1 blastoff! 4294967295 4294967294 4294967293 4294967292 4294967291
а затем просто продолжает печатать уменьшающиеся числа. Программа никогда не завершится, потому что условие count >= 0
никогда не может быть ложным, если count
является целым числом без знака.
Ошибки на единицу
Ошибки «на единицу» возникают, когда цикл повторяется на один раз больше или на один раз меньше, чем это необходимо. Вот пример, который мы рассмотрели в уроке «7.9 – Инструкции for
»:
#include <iostream>
int main()
{
for (unsigned int count{ 1 }; count < 5; ++count)
{
std::cout << count << ' ';
}
return 0;
}
Этот код должен печатать "1 2 3 4 5", но он печатает только "1 2 3 4", потому что был использован неправильный оператор отношения.
Неправильный приоритет операторов
Следующая программа из урока «5.7 – Логические операторы» допускает ошибку приоритета операторов:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!x > y)
std::cout << x << " is not greater than " << y << '\n';
else
std::cout << x << " is greater than " << y << '\n';
return 0;
}
Поскольку логическое НЕ имеет более высокий приоритет, чем operator>
, условное выражение вычисляется так, как если бы оно было написано (!x) > y
, что не соответствует замыслу программиста.
В результате эта программа печатает:
5 is greater than 7
Это также может произойти при смешивании в одном выражении логического ИЛИ и логического И (логическое И имеет больший приоритет, чем логическое ИЛИ). Используйте явные скобки, чтобы избежать подобных ошибок.
Проблемы точности с типами с плавающей запятой
Следующая переменная с плавающей запятой не имеет достаточной точности для хранения всего числа:
#include <iostream>
int main()
{
float f{ 0.123456789f };
std::cout << f;
}
Как следствие, эта программа напечатает:
0.123457
В уроке «5.6 – Операторы отношения и сравнение чисел с плавающей запятой» мы говорили о том, что использование operator==
и operator!=
может вызывать проблемы с числами с плавающей запятой из-за небольших ошибок округления (а также о том, что с этим делать). Вот пример:
#include <iostream>
int main()
{
// сумма должна быть равна 1.0
double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
if (d == 1.0)
std::cout << "equal";
else
std::cout << "not equal";
}
Эта программа напечатает:
not equal
Чем больше вы выполняете арифметических действий с числом с плавающей запятой, тем больше в нем накапливаются небольшие ошибки округления.
Целочисленное деление
В следующем примере мы хотим выполнить деление с плавающей запятой, но поскольку оба операнда принадлежат целочисленному типу, вместо этого мы выполняем целочисленное деление:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 3 };
std::cout << x << " divided by " << y << " is: " << x / y; // целочисленное деление
return 0;
}
Этот код напечатает:
5 divided by 3 is: 1
В уроке «5.2 – Арифметические операторы» мы показали, что мы можем использовать static_cast
для преобразования одного из целочисленных операндов в значение с плавающей запятой, чтобы выполнять деление с плавающей запятой.
Случайные пустые инструкции
В уроке «7.3 – Распространенные проблемы при работе с операторами if
» мы рассмотрели пустые инструкции, которые ничего не делают.
В приведенной ниже программе мы хотим взорвать мир, только если у нас есть разрешение пользователя:
#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'); // здесь случайная пустая инструкция
blowUpWorld(); // поэтому это всегда будет выполняться, так как это не часть оператора if
return 0;
}
Однако из-за случайной пустой инструкции вызов функции blowUpWorld()
выполняется всегда, поэтому мы взрываем независимо от ввода:
Should we blow up the world again? (y/n): n
Kaboom!
Неиспользование составной инструкции, когда она требуется
Еще один вариант приведенной выше программы, которая всегда взрывает мир:
#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(); // упс, всегда будет выполняться. Должно быть внутри составной инструкции.
return 0;
}
Эта программа печатает:
Should we blow up the world again? (y/n): n
Kaboom!
Висячий else
(рассмотренный в уроке «7.3 – Распространенные проблемы при работе с операторами if
») также попадает в эту категорию.
Что еще?
Приведенные выше примеры представляют собой хороший образец наиболее распространенных типов семантических ошибок, которые склонны совершать на C++ начинающие программисты, но их гораздо больше. Читатели, если у вас есть дополнительные примеры, которые, по вашему мнению, являются распространенными ошибками, напишите об этом в комментариях.