Ловим утечки памяти в С/С++

от автора

Приветствую вас, Хабровчане!

Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.

На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.

Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.

Для понимания, что происходит, прикладываю реальный пример:

Main.c

struct Student create_student(); void ControlMenu();  int main() {     ControlMenu();     return 0; }  void ShowListMenu(int kX) {     char listMenu[COUNT_LIST_MENU][55] = { {"Read students from file"}, {"Input student and push"},     {"Input student and push it back"}, {"Input student and push it after student"},     {"Delete last student"}, {"Write students to file"}, {"Find student"}, {"Sort students"},     {"Show list of students"}, {"Exit"} };     for (int i = 0; i < COUNT_LIST_MENU; i++)     {         if (i == kX)         {             printf("%s", listMenu[i]);             printf(" <=\n");         }         else             printf("%s\n", listMenu[i]);     } }  void ControlMenu() {     struct ListOfStudents* list = NULL;     int kX = 0, key;     int exit = FALSE;     ShowListMenu(kX);     do     {         key = _getch();         switch (key)         {         case 72: //up         {             if (kX == 0)                 kX = COUNT_LIST_MENU-1;             else                 kX--;         }break;         case 80: //down         {             if (kX == COUNT_LIST_MENU-1)                 kX = 0;             else                 kX++;         }break;         case 13:         {             if (kX == 0)             {                 int sizeStudents = 0;                 struct Student* students = (struct Student*)malloc(1 * sizeof(struct Student));                 char* path = (char*)malloc(255 * sizeof(char));                 printf("Put the path to file with students: ");                 scanf("%s", path);                 int size = 0;                 students = read_students(path, &size);                 if (students == NULL)                 {                     printf("Can't open this file.\n");                 }                 else                 {                     for (int i = 0; i < size; i++)                     {                         if (i == 0)                         {                             list = init(students[i]);                         }                         else                         {                             list = add_new_elem_to_start(list, students[i]);                         }                     }                 }                                 free(students);                 printf("\nPress any key to continue...");                 getchar();                 getchar();                 free(path);             }             else if (kX == 1 || kX == 2 || kX == 3 || kX == 6)             {                 struct Student student = create_student();                 if (kX == 1)                 {                     if (list == NULL)                     {                         list = init(student);                     }                     else                     {                         list = add_new_elem_to_start(list, student);                     }                     printf("\nPress any key to continue...");                     getchar();                     getchar();                 }                 else if (kX == 2)                 {                     if (list == NULL)                     {                         list = init(student);                     }                     else                     {                         list = add_new_elem_to_end(list, student);                     }                     printf("\nPress any key to continue...");                     getchar();                     getchar();                 }                 else if (kX == 3)                 {                     if (list == NULL)                     {                         list = init(student);                         printf("The list was empty, so, list have been created.\n");                     }                     else                     {                         int position;                         printf("Put the position: ");                         scanf("%d", &position);                         list = add_new_elem_after_pos(list, student, position);                     }                     printf("\nPress any key to continue...");                     getchar();                     getchar();                 }                 else                 {                     if (find_elem(list, student))                         printf("Student exist");                     else                         printf("Student doesn't exist");                     printf("\nPress any key to continue...");                     getchar();                     getchar();                 }             }             else if (kX == 4)             {                 if (list == NULL)                 {                     printf("List is empty.\n");                 }                 else                 {                     list = delete_elem(list);                 }                 printf("\nPress any key to continue...");                 getchar();                 getchar();             }             else if (kX == 5)             {                 char* path = (char*)malloc(255 * sizeof(char));                 printf("Put the path to file with students: ");                 scanf("%s", path);                 if (write_students(list, path) == 0)                 {                     printf("Can't write");                     printf("\nPress any key to continue...");                     getchar();                     getchar();                 }                 free(path);             }             else if (kX == 7)             {                 if (list == NULL)                 {                     printf("List is empty.\n");                 }                 else                 {                     list = sort_list(list);                 }                 printf("\nThe list was successfully sorted");                 printf("\nPress any key to continue...");                 getchar();                 getchar();             }             else if (kX == 8)             {                 system("cls");                 show_list(list);                 printf("\nPress any key to continue...");                 getchar();                 getchar();             }             else                 exit = TRUE;         }break;         case 27:         {             exit = TRUE;         }break;         }         system("cls");         ShowListMenu(kX);     } while (exit == FALSE);     while (list != NULL)     {         list = delete_elem(list);     } }  struct Student create_student() {     struct Student new_student;     do     {         printf("Write the name of student\n");         scanf("%s", new_student.first_name);     } while (strlen(new_student.first_name) == 0);     do     {         printf("Write the last name of student\n");         scanf("%s", new_student.last_name);     } while (strlen(new_student.last_name) == 0);     do     {         printf("Write the patronyminc of student\n");         scanf("%s", new_student.patronyminc);     } while (strlen(new_student.patronyminc) == 0);     do     {         printf("Write the city of student\n");         scanf("%s", new_student.city);     } while (strlen(new_student.city) == 0);     do     {         printf("Write the district of student\n");         scanf("%s", new_student.disctrict);     } while (strlen(new_student.disctrict) == 0);     do     {         printf("Write the country of student\n");         scanf("%s", new_student.country);     } while (strlen(new_student.country) == 0);     do     {         printf("Write the phone number of student\n");         scanf("%s", new_student.phoneNumber);     } while (strlen(new_student.phoneNumber) != 13);     char* choose = (char*)malloc(255 * sizeof(char));     while (TRUE)     {         printf("Does student live in hostel? Y - yes, N - no\n");         scanf("%s", choose);         if (strcmp(choose, "y") == 0 || strcmp(choose, "Y") == 0)         {             new_student.is_live_in_hostel = TRUE;             break;         }         if (strcmp(choose, "n") == 0 || strcmp(choose, "n") == 0)         {             new_student.is_live_in_hostel = FALSE;             break;         }     }     while (TRUE)     {         printf("Does student get scholarship? Y - yes, N - no\n");         scanf("%s", choose);         if (strcmp(choose, "y") == 0 || strcmp(choose, "Y") == 0)         {             new_student.is_live_in_hostel = TRUE;             break;         }         if (strcmp(choose, "n") == 0 || strcmp(choose, "n") == 0)         {             new_student.is_live_in_hostel = FALSE;             break;         }     }     free(choose);     for (int i = 0; i < 3; i++)     {         char temp[10];         printf("Write the %d mark of ZNO\n", i + 1);         scanf("%s", temp);         new_student.mark_zno[i] = atof(temp);         if (new_student.mark_zno[i] == 0)         {             i--;         }     }      return new_student; }

А также есть Student.h и Student.c в котором объявлены структуры и функции.

Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.

В начало файла, где находится main, необходимо добавить этот кусок кода:

#define __CRTDBG_MAP_ALLOC #include <crtdbg.h> #define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) #define new DEBUG_NEW

А перед return 0 нужно прописать это: _CrtDumpMemoryLeaks();.

В итоге, в режиме Debug, студия будет выводить это:

Detected memory leaks!  Dumping objects -> {79} normal block at 0x00A04410, 376 bytes long.  Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  Object dump complete.

Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?

После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:

if (kX == 0)             {                 int sizeStudents = 0;                 struct Student* students = (struct Student*)malloc(1 * sizeof(struct Student));                 char* path = (char*)malloc(255 * sizeof(char));                 printf("Put the path to file with students: ");                 scanf("%s", path);                 int size = 0;                 students = read_students(path, &size);                 if (students == NULL)                 {                     printf("Can't open this file.\n");                 }                 else                 {                     for (int i = 0; i < size; i++)                     {                         if (i == 0)                         {                             list = init(students[i]);                         }                         else                         {                             list = add_new_elem_to_start(list, students[i]);                         }                     }                 }                 free(students);                 printf("\nPress any key to continue...");                 getchar();                 getchar();                 free(path);             }

Но как так — то? Я же все освобождаю? Или нет?

И тут мне сильно не хватало Valgrind, с его трассировкой вызовов…

В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.

Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe

Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.

Чтобы подключить VLD, необходимо прописать #include <vld.h>.

Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:

Visual Leak Detector read settings from: C:\Program Files (x86)\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed.

И вот, что будет выдавать при утечке памяти:

WARNING: Visual Leak Detector detected memory leaks! ---------- Block 1 at 0x01405FD0: 376 bytes ----------   Leak Hash: 0x555D2B67, Count: 1, Total 376 bytes   Call Stack (TID 8908):     ucrtbased.dll!malloc()     test.exe!0x00F41946()     test.exe!0x00F42E1D()     test.exe!0x00F44723()     test.exe!0x00F44577()     test.exe!0x00F4440D()     test.exe!0x00F447A8()     KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes     ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xED bytes     ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xBD bytes   Data:     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........     CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........  Visual Leak Detector detected 1 memory leak (412 bytes). Largest number used: 3115 bytes. Total allocations: 3563 bytes. Visual Leak Detector is now exiting.

Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?

Ладно, обещание сдержали, однако это не тот результат, который я хотел.

Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку «Использование памяти» и нажать на «Сделать снимок». Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.

Первый скриншот

Второй скриншот

После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку «Экземпляры представления объекта Foo».

Третий скриншот

Четвертый скриншот

Да! Это победа! Полная трассировка с местоположением вызовов! Это то, что было необходимо изначально.

Linux — разработка
Теперь, посмотрим, что творится в Linux.

В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).

Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:

main.c

#include <stdlib.h> #include <stdio.h> #define N 10  int main() {     int * mas = (int *)malloc(N * sizeof(int));     for(int i = 0; i < N; i++)     {         *(mas+i) = i;         printf("%d\t", *(mas+i));     }     printf("\n");     return 0; } 

Скомпилировав программу с помощью CLang, мы получаем .out файл, который мы подкидываем valgrind’у.

С помощью команды valgrind ./a.out. Как работает valgrind, думаю, есть смысл описать в отдельной статье, а сейчас, как выполнится программа, valgrind выведет это:

==2342== HEAP SUMMARY: ==2342==     in use at exit: 40 bytes in 1 blocks ==2342==   total heap usage: 2 allocs, 1 frees, 1,064 bytes allocated ==2342==  ==2342== Searching for pointers to 1 not-freed blocks ==2342== Checked 68,984 bytes ==2342==  ==2342== LEAK SUMMARY: ==2342==    definitely lost: 40 bytes in 1 blocks ==2342==    indirectly lost: 0 bytes in 0 blocks ==2342==      possibly lost: 0 bytes in 0 blocks ==2342==    still reachable: 0 bytes in 0 blocks ==2342==         suppressed: 0 bytes in 0 blocks ==2342== Rerun with --leak-check=full to see details of leaked memory ==2342==  ==2342== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ==2342== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Таким образом, valgrind пока показывает, сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо прописать --leak-check=full, и тогда, valgrind, помимо выше описанного, выведет это:

==2348== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==2348==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==2348==    by 0x40053A: main (in /home/hunterlan/Habr/a.out)

Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.

Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.

Выводы

Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.

Спасибо вам за внимания, удачного написания кода вам!


ссылка на оригинал статьи https://habr.com/ru/post/480368/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *