Вебинар: Парсим С++ - 25.10
CppCat – это простой статический анализатор кода для поиска ошибок в программах на языке Си/Си++. Мы начали выдавать бесплатные академические лицензии всем желающим (студентам, преподавателям и так далее). Для большей популяризации CppCat среди студентов я решил написать эту заметку об ошибках, которые можно найти в лабораторных работах, встречающихся на сайте Pastebin.com.
К сожалению, мы больше не развиваем и не поддерживаем проект CppCat. Вы можете почитать здесь о причинах.
CppCat – статический анализатор кода, интегрирующийся в среду Visual Studio и позволяющий найти множество опечаток и прочих ошибок ещё на этапе кодирования. Анализатор умеет запускаться автоматически после компиляции и проверять только что написанный код. Анализатор поддерживает C, C++, C++/CLI, C++/CX.
О том, как получить бесплатную лицензию на CppCat, описано в статье: Бесплатный CppCat для студентов. Хочу добавить, что мы даём лицензию не только студентам, но и аспирантам, преподавателям и так далее.
Многих расстраивает, что CppCat не интегрируется в бесплатную среду Visual Studio Express. К сожалению, мы не можем ничего с этим поделать. Express-версии Visual Studio не поддерживают модули расширений (plugins). Однако, это не беда. Хочется напомнить, что студенты имеют доступ к Microsoft DreamSpark и могут получить доступ к Visual Studio Professional.
В начале хотел написать что-то в духе:
Даже студент может получить пользу от статического анализа. Зачем долго и мучительно искать ошибку в своей программе, если о ней может подсказать CppCat? Таким образом можно не только быстрее найти ошибку, но ещё узнать подробности о том, как делать не надо. Документация к CppCat подробно объясняет каждую диагностику и приводит различные примеры ошибок, подсказывает, как их исправлять.
Потом решил, что как-то это натянуто. Ну какие у студентов серьезные ошибки? Можно и поотлаживать цикл из 10 итераций. Это будет даже полезно. Поэтому я переформулирую рекомендацию использовать CppCat следующим образом:
Работодатель будет ценить не только ваше умение программировать и писать хитрые алгоритмы. Не менее важно уметь пользоваться базовым инструментарием.
Грош цена алгоритму, если он будет потерян из-за того, что вы не знаете, что такое система контроля версий и максимум, что предпринимали – делали копию исходных кодов на флешку.
Поэтому важно изучать не только языки и среды разработки, но и вспомогательный инструментарий.
Я не берусь составлять список наиболее важного инструментария. Однако важно знать хотя бы одну систему контроля версий, зачем может быть нужен WinMerge, что такое профайлер, как сделать дистрибутив и так далее.
Одной из технологий, которую хорошо знать и можно упомянуть в резюме, является статический анализ кода. CppCat отлично подходит для знакомства с методологией статического анализа. Это будет хорошим плюсом к вашим знаниям и намекнёт работодателю, что вы что-то слышали о качестве кода.
Скучная часть закончилась. Я думаю, вы догадываетесь, что сейчас мы будем искать ошибки в лабораторных работах.
Говорить о том, что код часто плох, в этот раз я не буду. И так понятно, что код лабораторных содержит в себе тьму разнообразнейших ошибок. Поэтому я поставил задачу не найти как можно больше ошибок. Это неинтересно. Я попробовал выделить какие-то паттерны ошибок, которые проявляются у студентов чаще всего. Правда, пока я смог четко заметить преобладание ошибок только трех типов. Но про это чуть ниже.
У Вас может возникнуть вопрос: откуда я взял лабораторные работы? Отвечаю.
Есть сайт Pastebin.com, где разработчики могут удобно обмениваться фрагментами кода. Студенты активно используют этот сайт. Почти весь код, имеющий тег C++, представляет собой какую-то лабораторную работу или её часть.
Мы написали программу, которая следит за сайтом Pastebin.com и выкачивает оттуда свежие файлы, помеченные как "код C++". Насобирав более двух тысяч файлов, я сделал из них проект в Visual Studio и проверил его. Больше половины, конечно, проверить не удалось. Во многих файлах только фрагменты кода, вписан текст, не являющийся комментарием, не хватает каких-то библиотек и так далее. Однако я и не ставил целью проверить как можно больше кода, опубликованного на Pastebin.com. То, что проверилось, мне вполне хватит для этой статьи. Сбор файлов продолжается и, возможно, я потом напишу ещё что-то на эту тему.
Я просмотрел не так уж много ошибок в лабораторных работах. Так что пока могу выделить только 3 паттерна.
Приводить все подряд примеры ошибок я не буду. Они однотипны и неинтересны. Ограничусь только несколькими примерами. Но, поверьте, если я говорю, что такая ошибка распространена, это действительно так.
P.S. Многие из опубликованных примеров были с ограничением по времени и уже недоступны. Поэтому ссылки на удалённые страницы давать не буду.
Многие задачи по программированию подразумевают проверку многих условий. И, реализуя их, легко запутаться или опечататься. Показательный пример:
int main()
{
int n,a,b,c;
cin >> n;
for(int i=0;i<n;i++)
{
cin >> a >> b >> c;
if((a % 2==0 && b % 2 ==0 && c % 2!=0)||
(a % 2==0 && b % 2!=0 && c % 2==0)||
(a % 2!=0 && b % 2==0 && c % 2==0)||
(a % 2!=0 && b % 2 !=0 && c % 2==0)||
(a % 2==0 && b % 2!=0 && c % 2!=0)|| // <=
(a % 2==0 && b % 2!=0 && c % 2!=0)) // <=
{
cout << "1";
}
else
cout << "2";
}
cout << endl;
return 0;
}
Предупреждение CppCat: V501 There are identical sub-expressions '(a % 2 == 0 && b % 2 != 0 && c % 2 != 0)' to the left and to the right of the '||' operator. jtzrihcg.cpp 14
Надо было как-то хитро проверить значения трёх введенных переменных. Скорее всего, код копировался и не везде был правильно поправлен. В результате предпоследняя и последняя строка в условии совпадают.
Ещё пример:
int main() {
....
} else if(gesucht < geraten) {
puts("Ein bisschen zu klein");
} else if (gesucht < geraten) {
puts("Ein bisschen zu gross");
}
....
}
V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 41, 43. wrgkuuzr.cpp 40
Два раза выполняется проверка (gesucht < geraten), но при этом должны выводиться разные строки.
Кстати, в обоих примерах ошибка находится в последней строке. Опять нам встретился "эффект последней строки".
То, что элементы массивов нумеруются в языке C++ от нуля, является большим затруднением при его изучении. Т.е. вроде понять это просто, но вот научиться не выходить за границу массива очень сложно. Если нужен 10 элемент массива, то так и хочется написать A[10]. Пример:
int main()
{
....
int rodnecs[10];
....
VelPol1 = rodnecs[1] + rodnecs[3] + rodnecs[5] +
rodnecs[8] + rodnecs[10];
....
}
Предупреждение CppCat: V557 Array overrun is possible. The '10' index is pointing beyond array bound. 0z3x9b3i.cpp 38
Ещё:
void main()
{
....
double pop[3][3];
....
for (int i = 0; i<3; i++)
{
calc_y[i] = F(pop[i][1], pop[i][2], pop[i][3], x[i]);
}
....
}
Предупреждение CppCat: V557 Array overrun is possible. The '3' index is pointing beyond array bound. 1uj9v9xs.cpp 48
Много неправильных сравнений в условиях циклов:
int main()
{
int i,pinakas[20],temp,temp2,max,min,sum=0;
for (i=1;i<=20;i++)
{
pinakas[i]=rand();
......
}
Предупреждение CppCat: V557 Array overrun is possible. The value of 'i' index could reach 20. 287ep6c0.cpp 20
Очень много:
int main()
{
const int arraySize = 10;
int a[arraySize];
int key,index,to_do = arraySize - 1;
bool did_swap = true;
srand(time(NULL));
for (int i = 0; i <= arraySize; i++)
{
//generating random number between 1 - 100
a[i] = rand() % 100 + 1;
}
....
}
Предупреждение CppCat: V557 Array overrun is possible. The value of 'i' index could reach 10. wgk1lx3u.cpp 18
Остальные ошибки аналогичны приведённым выше, так что закончим.
О! Я, кажется, понял, почему, кого не спроси, одной из наиболее частых и опасных ошибок в программировании на Си/Си++ люди называют "неинициализированные переменные". Но, при этом, анализируя проекты с помощью PVS-Studio, я редко встречаю эту ошибку.
Почему? Видимо, при изучении языка все очень сильно набивают себе шишки на этом. Таким образом, программисты потом почти не допускают такие ошибки. Но воспоминания остались. И на вопрос, чего надо бояться, часто ответят "неинициализированные переменные".
Есть совсем простое:
int main()
{
....
int n,k=0, liczba=n, i=1;
....
}
Предупреждение CppCat: V614 Uninitialized variable 'n' used. 1hvefw6r.cpp 92
Можно неправильно работать со списком:
void erase(List * Lista){
List* pom;
pom->next = Lista->next;
Lista->next= pom;
delete pom;
}
Предупреждение CppCat: V614 Uninitialized pointer 'pom' used. 6gpsgjuy.cpp 54
Можно сделать цикл со случайным количеством итераций:
void main()
{
int i,n;
imie* ime[20];
string nazwa;
string kobieta="Kobiece imina: ";
wpr_dane();
for (i = 1; i < n; i++)
{
....
}
Предупреждение CppCat: V614 Uninitialized variable 'n' used. 8kns8hyn.cpp 63
Можно вначале использовать, а потом вводить значение переменной:
int main() {
int n1;
int n2;
std::vector<int> vec1(n1);
std::vector<int> vec2(n2);
std::cin >> n1;
for (int i = 0; i < n1; i++) {
std::cin >> vec1[i];
}
std::cin >> n2;
for (int j = 0; j < n2; j++) {
std::cin >> vec2[j];
}
....
}
Предупреждения CppCat:
Далее приводить примеры, думаю, смысла нет. Но, поверьте, студенты отстреливают себе ноги неинициализированными переменными разнообразнейшими способами.
Конечно, в лабораторных я увидел много других разнообразнейших ошибок. Таких же больших групп ошибок, как описано выше, я выделить не могу. Хотя я могу назвать ещё несколько заметных групп: неправильное вычисление размера массивов, точка с запятой, досрочное прерывание цикла, неправильная работа с массивами, WTF.
Многим начинающим тяжело даётся понимание, что в Си/Си++ указатель и массив – это разные сущности. В результате нередко попадается код подобный этому:
int arrayLen(int p[])
{
return(sizeof(p)/sizeof(*p));
}
Предупреждение CppCat: V511 The sizeof() operator returns size of the pointer, and not of the array, in 'sizeof (p)' expression. seprcjvw.cpp 147
Правда, функция arrayLen() нигде не используется. Видимо, из-за того, что не работает. :)
Ещё один пример:
bool compare_mas(int * mas, int * mas2){
//вычисляем кол-во элементов первого массива
const auto mas_size = sizeof(mas) / sizeof(mas[0]);
//вычисляем кол-во элементов второго массива
const auto mas2_size = sizeof(mas2) / sizeof(mas2[0]);
....
}
Предупреждения CppCat:
Этих ошибки встречаются не так часто, как я ожидал. Они есть, но распространенной ошибкой в лабораторных работах я её назвать не могу.
Типовой пример:
vector sum(vector m[],int N){
vector sum,tmp;
for (int i=0;i<N;i++);
{
tmp.a=m[i].a;
tmp.b=m[i].b;
tmp.c=m[i].c;
sum.a+=tmp.a;
sum.b+=tmp.b;
sum.c+=tmp.c;
}
return sum.a,sum.b,sum.c;
}
Предупреждение CppCat: V529 Odd semicolon ';' after 'for' operator. knadcqde.cpp 122
Есть ряд примеров, где цикл случайно прерывается раньше времени:
int main()
{
....
for (long long j = sled.size()-1; j > i; j --)
{
sled[j] = '0';
des = 1;
break;
}
....
}
Предупреждение CppCat: V612 An unconditional 'break' within a loop. XHPquVXs.cpp 31
В нескольких лабораторных встретилась работа с массивами в стиле Pascal. То есть используется запятая, что хоть и компилируется, но работает, конечно, неправильно:
void build_maze(){
// tablica przechowujaca informacje o odwiedzonych polach
bool ** tablica = new bool *[n];
....
if (tablica[aktualny.x - 1, aktualny.y] == false){
....
}
Предупреждение CppCat: V520 The comma operator ',' in array index expression '[aktualny.x - 1, aktualny.y]'. qqxjufye.cpp 125
Или забывают, что память под возвращаемые массивы надо выделять специальным образом:
int *mul3(int *a)
{
int mem = 0;
int b[1001];
for (int i = 100; i >= 0; i--)
{
int x = a[i] * 3 + mem;
mem = x / 10;
b[i] = x % 10;
}
return b;
}
Предупреждение CppCat: V558 Function returns the pointer to temporary local object: b. hqvgtwvr.cpp 89
Есть фрагменты кода, которые я, кроме как WTF, назвать не могу. Возможно, кто-то попросил одногруппника объяснить, где в программе ошибка. Хотя, скорее, эта лабораторная как раз на изучение переполнения массивов. К сожалению, я не знаю, что написано в комментарии.
Приведу целиком один такой пример:
#include <iostream>
using namespace std;
int main()
{
int a[10];
for(int i=0; i<50; i++)
cout << a[i] << endl;
//ovoj loop ili kje krashne ili kje ti nedefinirani vrednost
//(ne mora da bidat 0)
//ako namesto 50 stavis 500000, skoro sigurno kje krashne
int b[10];
for(int i=0; i<50; i++)
{
b[i] = i;
cout << b[i] << endl;
}
//ovoj loop nekogas kje raboti, nekogas ne. problemot so
//out-of-bounds index errori e sto nekogas rabotat kako
//sto treba, pa greskata tesko se naogja
}
Много что не вошло! Например, встречается неправильное использование функций printf(). Но это настолько банально, что даже писать про это не хочется.
Впрочем, встречались и достаточно экзотические разновидности ошибок:
void zmienne1()
{
....
int a,b,c,d;
cin >> a >> b >> c >> d;
if(a == b == c == d)
....
}
Предупреждение CppCat: V709 Suspicious comparison found: 'a == b == c'. Remember that 'a == b == c' is not e qual to 'a == b && b == c'. b5lt64hj.cpp 284
Тоже из редкого (если, конечно, не смотреть на предупреждения компилятора):
const long AVG_PSYCHO = 0.8;
const long AVG_GRAD = 1.2;
Предупреждения CppCat:
Тем не менее, надо заканчивать. Надеюсь, я развлек читателей и соблазнил кого-то попробовать CppCat.
Я предвижу вопрос: "Почему бы вам не сделать какую-то систему для online-проверки кода?". Например, есть какая-то форма, куда можно вставить код и нажать кнопку "проверить". Или, раз вы мониторите сайт pastebin.com, так почему бы не выкладывать куда-то результаты проверки?
Я уверен, что делать этого нам не нужно. Для этого есть три причины, поэтому прошу не начинать дискуссию на эту тему.
Причины:
Уважаемые студенты и преподаватели, будем рады видеть вас среди наших пользователей. Желаю студентам стать квалифицированными специалистами и склонить в дальнейшем свой коллектив на приобретение PVS-Studio для командной работы.
0