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