Einführung
Memory Leaks sind immer noch eines der schwierigsten Probleme, selbst für erfahrene Programmierer. Selbst wenn sie für eine gewisse Zeit inaktiv sind, kann ein Memory Leak weiterhin auftreten. Neben dem grundlegenden Leak-Phänomen von zugewiesenen Speicherbereichen gibt es viele verschiedene Arten von Memory Leaks, wie z. B. Leaks in Ausnahmefeldern usw. Dieses Projekt führt Sie durch die Implementierung eines Memory Leak Detektors.
Dinge, die Sie lernen können
- Überladen des Operators
new - Vordefinierte Makros
__FILE__und__LINE__ - Statische Variablen in der Kopfdatei
- Smart Pointer
std::shared_ptr
Code-Test
Memory Leaks hängen normalerweise mit versehentlich zugewiesenen Speicherbereichen zusammen, die nicht freigegeben werden. In modernen Betriebssystemen wird solch ein nicht freigegebener Speicher von dem Betriebssystem nach Beendigung einer Anwendung, die den regulären Speicher verwendet hat, wieder freigegeben. Ein Memory Leak, das durch eine vorübergehende Anwendung verursacht wird, hat daher keine schwerwiegenden Folgen.
Wenn wir jedoch eine Anwendung wie einen Server schreiben, wird diese ständig im Betrieb sein. Wenn es logische Fehler gibt, die zu einem Memory Leak führen, besteht die Möglichkeit, dass das Speicherleck sich ständig erhöht. Schließlich wird die Leistung des Systems reduziert und es kann sogar zu einem Betriebsausfall kommen.
Ein laufendes Programm, das ständig Speicher zuweist, ist ziemlich einfach:
int main() {
// weist ständig Speicher zu, gibt ihn nie frei
while(1) int *a = new int;
return 0;
}
Um Memory Leaks in einem laufenden Programm zu erkennen, legen wir in der Anwendung üblicherweise Prüfpunkte fest, um zu analysieren, ob sich der Speicher gegenüber anderen Prüfpunkten geändert hat. Im Wesentlichen ist dies ähnlich wie eine Prüfung auf kurzfristiges Memory Leaking in einem Programm. Daher wird der Memory Leak Detektor in diesem Projekt für kurzfristige Memory Leaks implementiert.
Wir hoffen, dass der folgende Testcode einen Memory Leak erkennen kann. In diesem Code geben wir den zugewiesenen Speicher nicht frei und erzeugen so einen Memory Leak aufgrund eines Ausnahmefeldes:
//
// main.cpp
// LeakDetector
//
#include <iostream>
// Hier wird die Memory Leak Prüfung implementiert
// #include "LeakDetector.hpp"
// testet das Verhalten eines Ausnahmefeldes, das zu einem Memory Leak führt
class Err {
public:
Err(int n) {
if(n == 0) throw 1000;
data = new int[n];
}
~Err() {
delete[] data;
}
private:
int *data;
};
int main() {
// Memory Leak: Vergessen, Zeiger b freizugeben
int *a = new int;
int *b = new int[12];
delete a;
// Memory Leak: Der Konstruktorparameter 0 verursacht ein Ausnahmefeld, das zu einem Memory Leak führt
try {
Err* e = new Err(0);
delete e;
} catch (int &ex) {
std::cout << "Exception: " << ex << std::endl;
};
return 0;
}
Wenn wir den Codeausschnitt #include "LeakDetector.hpp" auskommentieren, kann dieser Code wie folgt normal ausgeführt werden. Natürlich wissen wir jedoch, dass er tatsächlich zu einem Memory Leak führt:
Das Ergebnis:

Den Memory Leak Detektor entwerfen
Um die Erkennung von Memory Leaks zu erreichen, betrachten wir die folgenden Aspekte:
- Memory Leaks werden erzeugt, wenn nach einem
new-Vorgang keindeleteausgeführt wird. - Der Destruktor des zuerst erstellten Objekts wird immer am Ende ausgeführt.
Anschließend können wir die folgenden Aktionen entsprechend den beiden oben genannten Punkten ausführen:
- Überladen des
new-Operators. - Erstellen eines statischen Objekts und Aufrufen des Destruktors am Ende des ursprünglichen Programms.
Der Vorteil dieser beiden Schritte besteht darin, dass die Memory Leak-Erkennung ohne Änderung des ursprünglichen Codes durchgeführt werden kann. Daher können wir LeakDetector.hpp schreiben:
//
// /home/labex/Code/LeakDetector.hpp
//
#ifndef __LEAK_DETECTOR__
#define __LEAK_DETECTOR__
void* operator new(size_t _size, char *_file, unsigned int _line);
void* operator new[](size_t _size, char *_file, unsigned int _line);
// Dieser Makro wird in LeakDetector.cpp beschrieben
#ifndef __NEW_OVERLOAD_IMPLEMENTATION__
#define new new(__FILE__, __LINE__)
#endif
class _leak_detector
{
public:
static unsigned int callCount;
_leak_detector() noexcept {
++callCount;
}
~_leak_detector() noexcept {
if (--callCount == 0)
LeakDetector();
}
private:
static unsigned int LeakDetector() noexcept;
};
static _leak_detector _exit_counter;
#endif
Warum wird ein callCount entworfen? Der callCount gewährleistet, dass unser LeakDetector nur einmal aufgerufen werden kann. Betrachten Sie den folgenden vereinfachten Codeausschnitt:
// main.cpp
#include <iostream>
#include "test.h"
int main () {
return 0;
}
// test.hpp
#include <iostream>
class Test {
public:
static unsigned int count;
Test () {
++count;
std::cout << count << ",";
}
};
static Test test;
// test.cpp
#include "test.hpp"
unsigned int Test::count = 0;
Die endgültige Ausgabe ist 1, 2,.
Dies liegt daran, dass die statischen Variablen in der Kopfdatei mehrfach definiert werden und jede Mal, wenn sie in einer .cpp-Datei enthalten sind, erneut definiert werden. (Im Falle, dass sie in main.cpp und test.cpp enthalten sind.) Im Grunde genommen sind es jedoch dieselben Objekte.
Nun bleibt die verbleibende Frage: Wie implementiert man einen Memory Leak Detektor?
Ein Memory Leak Detektor zu implementieren
Lassen Sie uns unseren Memory Leak Detektor implementieren.
Da wir bereits den Operator new überladen haben, fällt es leicht, die Speicherzuweisung manuell zu verwalten und den Speicher natürlich freizugeben. Wenn unser delete nicht alle zugewiesenen Speicherbereiche freigibt, wird ein Memory Leak auftreten. Jetzt ist die Frage: Welche Struktur sollten wir verwenden, um den Speicher manuell zu verwalten?
Eine doppelt verkettete Liste ist eine gute Option. Für den tatsächlichen Code wissen wir nicht, wann wir Speicher zuweisen müssen. Daher ist eine einseitige lineare Liste nicht ausreichend, aber eine dynamische Struktur (z. B. eine Liste) ist viel bequemer. Außerdem ist eine einseitige lineare Liste auch nicht bequem, wenn es darum geht, das Objekt im Memory Leak Detektor zu löschen.
Somit ist die doppelt verkettete Liste momentan unsere beste Option:
#include <iostream>
#include <cstring>
// Definiert die __NEW_OVERLOAD_IMPLEMENTATION__ Makros,
// verhindert das Überschreiben des Operators `new`
#define __NEW_OVERLOAD_IMPLEMENTATION__
#include "LeakDetector.hpp"
typedef struct _MemoryList {
struct _MemoryList *next, *prev;
size_t size; // Größe des zugewiesenen Speichers
bool isArray; // Ob es sich um ein zugewiesenes Array handelt oder nicht
char *file; // Datei, in der der Leak auftritt
unsigned int line; // Zeile, in der der Leak auftritt
} _MemoryList;
static unsigned long _memory_allocated = 0; // Speichert die Größe des nicht freigegebenen Speichers
static _MemoryList _root = {
&_root, &_root, // Zeiger auf das erste Element zeigen alle auf sich selbst
0, false, // 0 zugewiesene Speichergröße, kein Array
NULL, 0 // Keine Datei, Zeile 0
};
unsigned int _leak_detector::callCount = 0;
Lassen Sie uns jetzt den zugewiesenen Speicher manuell verwalten:
// Speicherzuweisung beginnt am Anfang der _MemoryList
void* AllocateMemory(size_t _size, bool _array, char *_file, unsigned _line) {
// Berechne die neue Größe
size_t newSize = sizeof(_MemoryList) + _size;
// Wir können nur malloc verwenden, um die Zuweisung durchzuführen, da new bereits überschrieben wurde.
_MemoryList *newElem = (_MemoryList*)malloc(newSize);
newElem->next = _root.next;
newElem->prev = &_root;
newElem->size = _size;
newElem->isArray = _array;
newElem->file = NULL;
// Speichere die Dateiinformation, wenn vorhanden
if (_file) {
newElem->file = (char *)malloc(strlen(_file)+1);
strcpy(newElem->file, _file);
}
// Speichere die Zeilennummer
newElem->line = _line;
// Aktualisiere die Liste
_root.next->prev = newElem;
_root.next = newElem;
// Speichere die Größe des nicht freigegebenen Speichers
_memory_allocated += _size;
// Gebe den zugewiesenen Speicher zurück
return (char*)newElem + sizeof(_MemoryList);
}
und auch den Operator delete:
void DeleteMemory(void* _ptr, bool _array) {
// Gehe zurück zum Anfang der MemoryList
_MemoryList *currentElem = (_MemoryList *)((char *)_ptr - sizeof(_MemoryList));
if (currentElem->isArray!= _array) return;
// Aktualisiere die Liste
currentElem->prev->next = currentElem->next;
currentElem->next->prev = currentElem->prev;
_memory_allocated -= currentElem->size;
// Gebe den zugewiesenen Speicher für die Dateiinformation frei
if (currentElem->file) free(currentElem->file);
free(currentElem);
}
Beachten Sie, dass wir zwei verschiedene Wege haben, new und delete zu verwenden:
void* operator new(size_t _size) {
return AllocateMemory(_size, false, NULL, 0);
}
void* operator new[](size_t _size) {
return AllocateMemory(_size, true, NULL, 0);
}
void* operator new(size_t _size, char *_file, unsigned int _line) {
return AllocateMemory(_size, false, _file, _line);
}
void* operator new[](size_t _size, char *_file, unsigned int _line) {
return AllocateMemory(_size, true, _file, _line);
}
// Überschreibe den Operator delete
void operator delete(void *_ptr) noexcept {
DeleteMemory(_ptr, false);
}
void operator delete[](void *_ptr) noexcept {
DeleteMemory(_ptr, true);
}
Schließlich müssen wir _leak_detector::LeakDetector implementieren und es wird im Destruktor von static _leak_detector _exit_counter aufgerufen. Dann werden alle zugewiesenen Objekte freigegeben. Ein Memory Leak tritt auf, wenn static _MemoryList _root nicht leer ist, und wir müssen nur durch _root iterieren, um zu finden, wo der Speicherleak ist:
unsigned int _leak_detector::LeakDetector(void) noexcept {
unsigned int count = 0;
// Iteriere die gesamte Liste, wenn es einen Memory Leak gibt,
// dann zeigt _LeakRoot.next immer nicht auf sich selbst
_MemoryList *ptr = _root.next;
while (ptr && ptr!= &_root)
{
// Gib alle Leak-Informationen aus
if(ptr->isArray)
std::cout << "Leaking[] ";
else
std::cout << "Leaking ";
std::cout << ptr << ", Größe von " << ptr->size;
if (ptr->file)
std::cout << " (lokalisiert in " << ptr->file << ", Zeile " << ptr->line << ")";
else
std::cout << " (keine Dateiinformation)";
std::cout << std::endl;
++count;
ptr = ptr->next;
}
if (count)
std::cout << "Es gibt " << count << " Memory Leak-Verhaltensweisen, insgesamt " << _memory_allocated << " Byte." << std::endl;
return count;
}
Lassen Sie uns alles kompilieren:
g++ main.cpp LeakDetector.cpp -std=c++11 -Wno-write-strings
In main.cpp kommentieren Sie die Zeile include "LeakDetector.hpp" aus. Hier ist die erwartete Ausgabe:

Zusammenfassung
In diesem Lab überschreiben wir den Operator new in C++ und implementieren einen Memory Leak Detektor.
Tatsächlich kann der Code auch für das Leaking von zirkulären Referenzen von Smart Pointern verwendet werden. Wir müssen nur main.cpp wie folgt ändern:
#include <memory> // Verwende Smart Pointer
// Keine Änderungen am Anfang wie zuvor in main.cpp
// Füge zwei Testklassen hinzu
class A;
class B;
class A {
public:
std::shared_ptr<B> p;
};
class B {
public:
std::shared_ptr<A> p;
};
int main() {
// Änderungen hier wie zuvor in main.cpp
// Zusätzliche Fälle, Verwende Smart Pointer
auto smartA = std::make_shared<A>();
auto smartB = std::make_shared<B>();
smartA->p = smartB;
smartB->p = smartA;
return 0;
}
Unsere endgültigen Ausgaben sind:

Für einige von Ihnen kann das Ergebnis sein:

Ihr Ergebnis kann unterschiedlich sein. Beide der obigen sind richtig, da sich Ihre Umgebung in Bezug auf Maschine und Compiler unterscheiden kann.



