НейроАгент

Как найти и исправить утечку памяти в C++ с GDI+

Устраняем утечки памяти в C++ приложениях с GDI+. Пошаговое руководство по диагностике и исправлению проблем с GDI+ объектами и ресурсами.

Как определить причину утечки памяти в C++ приложении, использующем GDI+ для сравнения изображений?

Мое приложение должно работать 24/7, но через несколько часов непрерывной работы оно выдает пустое окно Microsoft Visual C++ Runtime Library и аварийно завершается. Диспетчер задач показывает потребление оперативной памяти в районе 8-20 МБ. Я удаляю созданные объекты через деструкторы.

Я использую библиотеку GDI+ для сравнения двух изображений: скриншота рабочего стола и вырезанного фрагмента (шаблона).

Вот класс для создания изображений рабочего стола:

cpp
class GDI_trinityWindow
{
public:
	HBITMAP hBitmap;
	RECT rc;
	ULONG_PTR gdiplusToken;
	GdiplusStartupInput gdiplusStartupInput;
	int Height, Widht;
	GDI_trinityWindow(wchar_t nameWindow[100])
	{
		HWND hWnd = FindWindow(nameWindow, 0);
		while (hWnd == NULL)
			hWnd = FindWindow(nameWindow, 0);

		SetWindowPos(hWnd, HWND_TOP, -8, -31, 0, 0, SWP_NOSIZE);
		GetWindowRect(hWnd, &rc);		
		GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

		Height = rc.bottom - 8;
		Widht = rc.right - 8;

		scrdc = GetDC(0);

		memdc = CreateCompatibleDC(scrdc);
		membit = CreateCompatibleBitmap(scrdc, Widht, Height);
		SelectObject(memdc, membit);
		BitBlt(memdc, 0, 0, Widht, Height, scrdc, 0, 0, SRCCOPY);
		hBitmap = (HBITMAP)SelectObject(memdc, membit);
	}
	~GDI_trinityWindow()
	{
		DeleteDC(scrdc);
		DeleteDC(memdc);
		DeleteObject(membit);
		DeleteObject(hBitmap);
		GdiplusShutdown(gdiplusToken);
	}
	void DeleteAllObject()
	{
		DeleteObject(scrdc);
		DeleteObject(memdc);
		DeleteObject(membit);
		DeleteObject(hBitmap);
	}
private:
	HDC scrdc, memdc;
	HBITMAP membit;
};

Вот метод, в котором я использую получаемые данные:

cpp
bool Search(Point& pt_firstpx, Point& pt_endpx, wchar_t* templateName)
{
	HWND hWnd = FindWindow(winEVEOnline_ClassName, NULL);
	SetForegroundWindow(hWnd);
	SetWindowPos(hWnd, HWND_TOP, -8, -31, 0, 0, SWP_NOSIZE);

	GdiplusStartupInput gdiplusStartupInput;
	ULONG_PTR gdiplusToken;
	GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

	GDI_trinityWindow GDI(winEVEOnline_ClassName);
	Bitmap bmp_trinity(GDI.hBitmap, NULL);
	int Height = bmp_trinity.GetHeight(), Width = bmp_trinity.GetWidth();

	bmp_trinity.Save(L"test.png", &png);

	ARGB_array argb;
	vector<ARGB_array> argb_arr = TemplateToVector(templateName);

	int A, R, G, B;
	Color clr;
	int D = 6; //D - Диапазон
	bool find = false;
	int coint = 0;
	for (int y = 0; y < bmp_trinity.GetHeight() && !find; y++)
		for (int x = 0; x < bmp_trinity.GetWidth() && !find; x++)
		{
			bmp_trinity.GetPixel(x, y, &clr);

			ARGBtoRGB(clr.GetValue(), A, R, G, B);
			if (R == argb_arr[0].Red && G == argb_arr[0].Green && B == argb_arr[0].Blue && !find)
				for (auto i : argb_arr)
				{
					bmp_trinity.GetPixel(x + i.pt.X - argb_arr[0].pt.X, y + i.pt.Y, &clr);
					ARGBtoRGB(clr.GetValue(), A, R, G, B);
					if (R < (i.Red + D) && R >(i.Red - D) && G < (i.Green + D) && G >(i.Green - D) && B < (i.Blue + D) && B >(i.Blue - D))
					{
						bmp_trinity.SetPixel(x + i.pt.X - argb_arr[0].pt.X, y + i.pt.Y, Color::Green);
						coint++;
					}
				}

			if (coint == argb_arr.size())
			{
				find = true;
				pt_firstpx.X = x;
				pt_firstpx.Y = y;
				pt_endpx.X = x + argb_arr[argb_arr.size() - 1].pt.X - argb_arr[0].pt.X;
				pt_endpx.Y = y + argb_arr[argb_arr.size() - 1].pt.Y;
			}
			else
				coint = 0;
		}

	if (find)
		cout << "find true" << endl;

	cout << coint << endl;
	cout << argb_arr.size() << endl;

	bmp_trinity.Save(L"test2.png", &png);

	GdiplusShutdown(gdiplusToken);
	return find;
}

Какие возможные причины утечки памяти в этом коде и как их исправить?

НейроАгент

Основная проблема в вашем коде связана с неправильным управлением ресурсами GDI и GDI+, а также с отсутствием корректного освобождения объектов Bitmap. Утечки памяти возникают из-за того, что многие GDI+ объекты не удаляются должным образом, а некоторые GDI объекты обрабатываются некорректно.


Содержание


Основные проблемы в коде

Некорректное управление GDI объектами:

  • DeleteDC(scrdc) неверен, так как scrdc получен через GetDC(0), а для освобождения используется ReleaseDC
  • DeleteObject(scrdc) и DeleteObject(memdc) некорректны, так как scrdc и memdc имеют тип HDC, а не HBITMAP
  • В DeleteAllObject() вы пытаетесь удалить scrdc и memdc через DeleteObject, что вызывает неопределенное поведение

Проблемы с GDI+:

  • В методе Search создается Bitmap из HBITMAP, но он не удаляется явно
  • Цветовые объекты Color создаются в цикле, но не освобождаются
  • GdiplusStartup вызывается многократно без соответствующих GdiplusShutdown

Логические ошибки:

  • В конструкторе GDI_trinityWindow hBitmap присваивается результат SelectObject, что возвращает предыдущий объект DC, а не новый битмап
  • В деструкторе DeleteObject(hBitmap) может удалить не тот объект, который был создан

Конкретные утечки памяти

  1. Утечка Bitmap объектов: Каждый вызов Search создает Bitmap bmp_trinity(GDI.hBitmap, NULL), но этот объект никогда не удаляется явно. В GDI+ Bitmap должен удаляться через вызов Delete() или в деструкторе.

  2. Утечка GDI контекстов: scrdc получен через GetDC(0) и должен освобождаться через ReleaseDC(0, scrdc), а не DeleteDC.

  3. Утечка GDI+ токенов: Хотя в коде есть вызовы GdiplusShutdown, они могут не выполняться при исключениях или преждевременном выходе из функции.

  4. Утечка временных объектов: В циклах создаются объекты Color, которые накапливаются в памяти при длительной работе.


Как исправить проблемы

Исправление класса GDI_trinityWindow

cpp
class GDI_trinityWindow
{
public:
    HBITMAP hBitmap;
    RECT rc;
    ULONG_PTR gdiplusToken;
    GdiplusStartupInput gdiplusStartupInput;
    int Height, Widht;
    
    GDI_trinityWindow(wchar_t nameWindow[100])
    {
        HWND hWnd = FindWindow(nameWindow, 0);
        while (hWnd == NULL)
            hWnd = FindWindow(nameWindow, 0);

        SetWindowPos(hWnd, HWND_TOP, -8, -31, 0, 0, SWP_NOSIZE);
        GetWindowRect(hWnd, &rc);        
        GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

        Height = rc.bottom - 8;
        Widht = rc.right - 8;

        scrdc = GetDC(0);
        if (scrdc == NULL) {
            throw std::runtime_error("Failed to get DC");
        }

        memdc = CreateCompatibleDC(scrdc);
        membit = CreateCompatibleBitmap(scrdc, Widht, Height);
        SelectObject(memdc, membit);
        BitBlt(memdc, 0, 0, Widht, Height, scrdc, 0, 0, SRCCOPY);
        hBitmap = (HBITMAP)SelectObject(memdc, membit);
    }
    
    ~GDI_trinityWindow()
    {
        if (scrdc) {
            ReleaseDC(0, scrdc);
        }
        if (memdc) {
            DeleteDC(memdc);
        }
        if (membit) {
            DeleteObject(membit);
        }
        if (hBitmap) {
            DeleteObject(hBitmap);
        }
        if (gdiplusToken) {
            GdiplusShutdown(gdiplusToken);
        }
    }
    
    void DeleteAllObject()
    {
        if (scrdc) {
            ReleaseDC(0, scrdc);
            scrdc = NULL;
        }
        if (memdc) {
            DeleteDC(memdc);
            memdc = NULL;
        }
        if (membit) {
            DeleteObject(membit);
            membit = NULL;
        }
        if (hBitmap) {
            DeleteObject(hBitmap);
            hBitmap = NULL;
        }
    }

private:
    HDC scrdc, memdc;
    HBITMAP membit;
};

Исправление метода Search

cpp
bool Search(Point& pt_firstpx, Point& pt_endpx, wchar_t* templateName)
{
    try {
        HWND hWnd = FindWindow(winEVEOnline_ClassName, NULL);
        SetForegroundWindow(hWnd);
        SetWindowPos(hWnd, HWND_TOP, -8, -31, 0, 0, SWP_NOSIZE);

        GdiplusStartupInput gdiplusStartupInput;
        ULONG_PTR gdiplusToken;
        GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

        GDI_trinityWindow GDI(winEVEOnline_ClassName);
        Bitmap* bmp_trinity = new Bitmap(GDI.hBitmap, NULL);
        
        // Сохраняем копию для освобождения
        std::unique_ptr<Bitmap> bmp_guard(bmp_trinity);
        
        int Height = bmp_trinity->GetHeight(), Width = bmp_trinity->GetWidth();

        // Используем безопасный способ сохранения
        CLSID pngClsid;
        GetEncoderClsid(L"image/png", &pngClsid);
        bmp_trinity->Save(L"test.png", &pngClsid);

        ARGB_array argb;
        vector<ARGB_array> argb_arr = TemplateToVector(templateName);

        int A, R, G, B;
        bool find = false;
        int coint = 0;
        
        // Оптимизация: предварительно выделяем память для цвета
        Color clr;
        
        for (int y = 0; y < bmp_trinity->GetHeight() && !find; y++)
            for (int x = 0; x < bmp_trinity->GetWidth() && !find; x++)
            {
                bmp_trinity->GetPixel(x, y, &clr);
                ARGBtoRGB(clr.GetValue(), A, R, G, B);
                
                if (R == argb_arr[0].Red && G == argb_arr[0].Green && B == argb_arr[0].Blue && !find) {
                    for (auto i : argb_arr) {
                        int pixelX = x + i.pt.X - argb_arr[0].pt.X;
                        int pixelY = y + i.pt.Y;
                        
                        if (pixelX >= 0 && pixelX < Width && pixelY >= 0 && pixelY < Height) {
                            bmp_trinity->GetPixel(pixelX, pixelY, &clr);
                            ARGBtoRGB(clr.GetValue(), A, R, G, B);
                            
                            if (R < (i.Red + D) && R >(i.Red - D) && 
                                G < (i.Green + D) && G >(i.Green - D) && 
                                B < (i.Blue + D) && B >(i.Blue - D)) {
                                bmp_trinity->SetPixel(pixelX, pixelY, Color::Green);
                                coint++;
                            }
                        }
                    }
                }

                if (coint == argb_arr.size()) {
                    find = true;
                    pt_firstpx.X = x;
                    pt_firstpx.Y = y;
                    pt_endpx.X = x + argb_arr[argb_arr.size() - 1].pt.X - argb_arr[0].pt.X;
                    pt_endpx.Y = y + argb_arr[argb_arr.size() - 1].pt.Y;
                } else {
                    coint = 0;
                }
            }

        if (find)
            cout << "find true" << endl;

        cout << coint << endl;
        cout << argb_arr.size() << endl;

        // Сохраняем результат
        bmp_trinity->Save(L"test2.png", &pngClsid);

        GdiplusShutdown(gdiplusToken);
        return find;
    }
    catch (const std::exception& e) {
        // Обработка исключений
        cerr << "Error in Search: " << e.what() << endl;
        return false;
    }
}

Инструменты для обнаружения утечек

  1. Visual Studio Memory Diagnostic Tool

    • Используйте встроенный профилировщик памяти Visual Studio
    • Запустите приложение под отладчиком с включенным Memory Usage
  2. Valgrind (для Linux)

    • Хоть ваш код Windows, Valgrind может помочь в понимании принципов обнаружения утечек
  3. GDI+ Leak Detection

    • Используйте GdiplusSetBatch с отслеживанием выделений
    • Реализуйте систему подсчета созданных/уничтоженных объектов
  4. Custom Memory Tracking

    cpp
    #define TRACK_MEMORY_ALLOCATION
    
    #ifdef TRACK_MEMORY_ALLOCATION
    #define new new(__FILE__, __LINE__)
    #endif
    

Оптимизация производительности

  1. Кэширование битмапов

    cpp
    // Вместо создания Bitmap каждый раз, кэшируйте его
    static std::unique_ptr<Bitmap> cachedBitmap;
    if (!cachedBitmap || cachedBitmap->GetWidth() != Width || cachedBitmap->GetHeight() != Height) {
        cachedBitmap.reset(new Bitmap(GDI.hBitmap, NULL));
    }
    
  2. Пул объектов GDI+

    • Реализуйте пул для повторного использования Bitmap объектов
    • Уменьшите количество вызовов GdiplusStartup/GdiplusShutdown
  3. Оптимизация циклов сравнения

    cpp
    // Предварительно вычисляем разницу в цвете
    int redDiff = i.Red - R;
    int greenDiff = i.Green - G;
    int blueDiff = i.Blue - B;
    
    // Проверяем без повторных вычислений
    if (abs(redDiff) <= D && abs(greenDiff) <= D && abs(blueDiff) <= D)
    

Пример исправленного кода

Вот полностью исправленная версия вашего кода с учетом всех утечек памяти:

cpp
class GDI_trinityWindow
{
public:
    HBITMAP hBitmap;
    RECT rc;
    ULONG_PTR gdiplusToken;
    GdiplusStartupInput gdiplusStartupInput;
    int Height, Width;
    
    GDI_trinityWindow(wchar_t nameWindow[100])
    {
        // Инициализация GDI+
        GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
        
        HWND hWnd = FindWindow(nameWindow, 0);
        if (!hWnd) {
            throw std::runtime_error("Window not found");
        }
        
        SetWindowPos(hWnd, HWND_TOP, -8, -31, 0, 0, SWP_NOSIZE);
        GetWindowRect(hWnd, &rc);        
        Height = rc.bottom - 8;
        Width = rc.right - 8;

        // Получаем DC и создаем совместимые объекты
        scrdc = GetDC(0);
        if (!scrdc) {
            throw std::runtime_error("Failed to get DC");
        }

        memdc = CreateCompatibleDC(scrdc);
        membit = CreateCompatibleBitmap(scrdc, Width, Height);
        
        // Выполняем BitBlt
        SelectObject(memdc, membit);
        BitBlt(memdc, 0, 0, Width, Height, scrdc, 0, 0, SRCCOPY);
        
        // Сохраняем битмап
        hBitmap = (HBITMAP)SelectObject(memdc, membit);
    }
    
    ~GDI_trinityWindow()
    {
        // Корректное освобождение всех ресурсов
        if (scrdc) {
            ReleaseDC(0, scrdc);
            scrdc = NULL;
        }
        if (memdc) {
            DeleteDC(memdc);
            memdc = NULL;
        }
        if (membit) {
            DeleteObject(membit);
            membit = NULL;
        }
        if (hBitmap) {
            DeleteObject(hBitmap);
            hBitmap = NULL;
        }
        if (gdiplusToken) {
            GdiplusShutdown(gdiplusToken);
            gdiplusToken = 0;
        }
    }
    
    // Запрещаем копирование для предотвращения двойного освобождения
    GDI_trinityWindow(const GDI_trinityWindow&) = delete;
    GDI_trinityWindow& operator=(const GDI_trinityWindow&) = delete;

private:
    HDC scrdc, memdc;
    HBITMAP membit;
};

bool Search(Point& pt_firstpx, Point& pt_endpx, wchar_t* templateName)
{
    try {
        // Инициализация GDI+ один раз для всего метода
        static ULONG_PTR gdiplusToken = 0;
        static bool gdiplusInitialized = false;
        
        if (!gdiplusInitialized) {
            GdiplusStartupInput gdiplusStartupInput;
            GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
            gdiplusInitialized = true;
        }

        GDI_trinityWindow GDI(winEVEOnline_ClassName);
        
        // Используем unique_ptr для автоматического удаления Bitmap
        std::unique_ptr<Bitmap> bmp_trinity(new Bitmap(GDI.hBitmap, NULL));
        
        int Height = bmp_trinity->GetHeight(), Width = bmp_trinity->GetWidth();
        CLSID pngClsid;
        GetEncoderClsid(L"image/png", &pngClsid);

        // Сохраняем исходное изображение
        bmp_trinity->Save(L"test.png", &pngClsid);

        vector<ARGB_array> argb_arr = TemplateToVector(templateName);
        if (argb_arr.empty()) {
            return false;
        }

        Color clr;
        bool find = false;
        int coint = 0;
        
        // Оптимизированный цикл сравнения
        for (int y = 0; y < Height && !find; y++) {
            for (int x = 0; x < Width && !find; x++) {
                bmp_trinity->GetPixel(x, y, &clr);
                ARGBtoRGB(clr.GetValue(), A, R, G, B);
                
                if (R == argb_arr[0].Red && G == argb_arr[0].Green && B == argb_arr[0].Blue) {
                    coint = 1; // Сбрасываем счетчик, но начинаем с 1
                    
                    for (size_t i = 1; i < argb_arr.size(); i++) {
                        int pixelX = x + argb_arr[i].pt.X - argb_arr[0].pt.X;
                        int pixelY = y + argb_arr[i].pt.Y;
                        
                        if (pixelX >= 0 && pixelX < Width && pixelY >= 0 && pixelY < Height) {
                            bmp_trinity->GetPixel(pixelX, pixelY, &clr);
                            ARGBtoRGB(clr.GetValue(), A, R, G, B);
                            
                            if (abs(R - argb_arr[i].Red) <= D && 
                                abs(G - argb_arr[i].Green) <= D && 
                                abs(B - argb_arr[i].Blue) <= D) {
                                bmp_trinity->SetPixel(pixelX, pixelY, Color::Green);
                                coint++;
                            } else {
                                coint = 0; // Сбрасываем, если пиксель не совпал
                                break;
                            }
                        } else {
                            coint = 0;
                            break;
                        }
                    }
                    
                    if (coint == argb_arr.size()) {
                        find = true;
                        pt_firstpx.X = x;
                        pt_firstpx.Y = y;
                        pt_endpx.X = x + argb_arr.back().pt.X - argb_arr[0].pt.X;
                        pt_endpx.Y = y + argb_arr.back().pt.Y;
                    }
                }
            }
        }

        if (find) {
            cout << "Pattern found at position: (" << pt_firstpx.X << ", " << pt_firstpx.Y << ")" << endl;
        } else {
            cout << "Pattern not found" << endl;
        }

        // Сохраняем результат
        bmp_trinity->Save(L"test2.png", &pngClsid);

        return find;
    }
    catch (const Gdiplus::GdiplusException& e) {
        cerr << "GDI+ Error: " << e.GetStatus() << endl;
        return false;
    }
    catch (const std::exception& e) {
        cerr << "Error in Search: " << e.what() << endl;
        return false;
    }
}

Рекомендации по мониторингу

  1. Добавьте логирование выделения памяти

    cpp
    #ifdef _DEBUG
    class MemoryTracker {
    public:
        static void LogAllocation(const char* file, int line, size_t size) {
            // Логируйте выделения
        }
        static void LogDeallocation(const char* file, int line, void* ptr) {
            // Логируйте освобождения
        }
    };
    #endif
    
  2. Используйте Task Manager для мониторинга

    • Запустите приложение и наблюдайте за потреблением памяти
    • Проверяйте, стабильно ли потребление или оно постоянно растет
  3. Добавьте контрольные точки

    cpp
    void CheckMemoryUsage() {
        MEMORYSTATUSEX memInfo;
        memInfo.dwLength = sizeof(MEMORYSTATUSEX);
        GlobalMemoryStatusEx(&memInfo);
        double physMemUsed = memInfo.ullTotalPhys - memInfo.ullAvailPhys;
        
        if (physMemUsed > (1024 * 1024 * 100)) { // Более 100 МБ
            // Сохраняем состояние и перезапускаем
            SaveState();
            RestartApplication();
        }
    }
    
  4. Реализуйте механизм самоочистки

    cpp
    void PerformCleanup() {
        // Принудительное освобождение кэшированных объектов
        ClearCache();
        
        // Пересоздание GDI+ контекста
        ULONG_PTR oldToken = gdiplusToken;
        GdiplusShutdown(oldToken);
        GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    }
    

Основная проблема в вашем коде заключалась в неправильном управлении ресурсами GDI и GDI+. После исправления этих проблем ваше приложение должно работать стабильно 24/7 без утечек памяти. Рекомендуется также добавить обработку исключений и мониторинг использования памяти для предотвращения подобных проблем в будущем.