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



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


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


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


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




К сожалению, не доступен сервер mySQL