>
>
Почему студентам нужен анализатор кода …

Андрей Карпов
Статей: 674

Почему студентам нужен анализатор кода CppCat

CppCat – это простой статический анализатор кода для поиска ошибок в программах на языке Си/Си++. Мы начали выдавать бесплатные академические лицензии всем желающим (студентам, преподавателям и так далее). Для большей популяризации CppCat среди студентов я решил написать эту заметку об ошибках, которые можно найти в лабораторных работах, встречающихся на сайте Pastebin.com.

К сожалению, мы больше не развиваем и не поддерживаем проект CppCat. Вы можете почитать здесь о причинах.

Чуть-чуть про 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. Многие из опубликованных примеров были с ограничением по времени и уже недоступны. Поэтому ссылки на удалённые страницы давать не буду.

Паттерн 1. Третье место по популярности. Путаница в однотипных условиях

Многие задачи по программированию подразумевают проверку многих условий. И, реализуя их, легко запутаться или опечататься. Показательный пример:

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), но при этом должны выводиться разные строки.

Кстати, в обоих примерах ошибка находится в последней строке. Опять нам встретился "эффект последней строки".

Паттерн 2. Второе место по популярности. Выход за границу массива на 1 элемент

То, что элементы массивов нумеруются в языке 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

Остальные ошибки аналогичны приведённым выше, так что закончим.

Паттерн 3. Первое место по популярности. Неинициализированные переменные

О! Я, кажется, понял, почему, кого не спроси, одной из наиболее частых и опасных ошибок в программировании на Си/Си++ люди называют "неинициализированные переменные". Но, при этом, анализируя проекты с помощью 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:

  • V614 Uninitialized variable 'n1' used. 9r9zdkp6.cpp 25
  • V614 Uninitialized variable 'n2' used. 9r9zdkp6.cpp 26

Далее приводить примеры, думаю, смысла нет. Но, поверьте, студенты отстреливают себе ноги неинициализированными переменными разнообразнейшими способами.

Прочие ошибки

Конечно, в лабораторных я увидел много других разнообразнейших ошибок. Таких же больших групп ошибок, как описано выше, я выделить не могу. Хотя я могу назвать ещё несколько заметных групп: неправильное вычисление размера массивов, точка с запятой, досрочное прерывание цикла, неправильная работа с массивами, 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:

  • V514 Dividing sizeof a pointer 'sizeof (mas)' by another value. There is a probability of logical error presence. 0mxbjwbg.cpp 2
  • V514 Dividing sizeof a pointer 'sizeof (mas2)' by another value. There is a probability of logical error presence. 0mxbjwbg.cpp 3

Не там поставлена точка с запятой ';'

Этих ошибки встречаются не так часто, как я ожидал. Они есть, но распространенной ошибкой в лабораторных работах я её назвать не могу.

Типовой пример:

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

Есть фрагменты кода, которые я, кроме как 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:

  • V674 The '0.8' literal of the 'double' type is assigned to a variable of the 'long' type. Consider inspecting the '= 0.8' expression. 2k2bmnpz.cpp 21
  • V674 The '1.2' literal of the 'double' type is assigned to a variable of the 'long' type. Consider inspecting the '= 1.2' expression. 2k2bmnpz.cpp 22

Тем не менее, надо заканчивать. Надеюсь, я развлек читателей и соблазнил кого-то попробовать CppCat.

Почему мы не планируем делать какой-либо online-анализатор

Я предвижу вопрос: "Почему бы вам не сделать какую-то систему для online-проверки кода?". Например, есть какая-то форма, куда можно вставить код и нажать кнопку "проверить". Или, раз вы мониторите сайт pastebin.com, так почему бы не выкладывать куда-то результаты проверки?

Я уверен, что делать этого нам не нужно. Для этого есть три причины, поэтому прошу не начинать дискуссию на эту тему.

Причины:

  • Это не нужно ни нам, ни пользователю. Нам это добавит работы. А пользователь не получит ничего нового. Он может просто скачать и установить PVS-Studio или CppCat и провести все эксперименты, которые пожелает. Демонстрационной версии будет более чем достаточно. Часто формы "вставь и проверь код" делают те, у кого просто так нельзя скачать пробную версию. У нас можно. Более того, она не имеет каких-либо функциональных ограничений. Ещё кто-то может сказать, что у него нет Windows, а он хотел что-то попробовать. Но раз у него нет Windows, то он всё равно не наш пользователь.
  • Такая система сильно искажает оценку возможностей статического анализатора. Статья на эту тему: Мифы о статическом анализе. Миф пятый – можно составить маленькую программу, чтобы оценить инструмент. Мы хотим, чтобы люди испытывали анализатор на своих реальных проектах, а не на синтетических примерах.
  • Как уже я сказал, синтетические примеры мы проверять не хотим. А проверить проект целиком сложно с инфраструктурной точки зрения. Подробнее про это написано в интервью. Кратко: придётся делать сложную систему, куда нужно закачивать исходники, библиотеки, настраивать параметры сборки и так далее. Иначе полноценный анализ невозможен. Получается, что проще скачать анализатор, установить и проверить.

Заключение

Уважаемые студенты и преподаватели, будем рады видеть вас среди наших пользователей. Желаю студентам стать квалифицированными специалистами и склонить в дальнейшем свой коллектив на приобретение PVS-Studio для командной работы.

Дополнительные ссылки: