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

Для понимания, что происходит, прикладываю реальный пример:
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-семейства).
Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:
#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/
Добавить комментарий