Häufig kommt es in C++ Applikationen vor, dass Speicher zwar angefordert aber nicht mehr freigegeben wird. Hier schlägt die Stunde von operator new und delete. Dank den beiden Operatoren ist es möglich, das Speichermanagement der Applikation explizit zu verwalten.
Das ein oder andere Mal wurde ich von Kunden schon beauftragt, zu verifizieren, ob eine Applikation ihren Speicher wieder ordentlich freigibt. Gerade bei Anwendungen, die über einen längeren Zeitraum häufig Speicher anfordern und wieder freigeben, ist die richtige Speicherverwaltung eine der zentralen Herausforderungen. Die automatische Freigabe des Speichers beim Schließen der Applikation ist in diesem Fall natürlich keine Option.
Der Startpunkt
Als Startpunkt für meine Analyse verwende ich ein kleines Programm, das relativ häufig Speicher anfordert und wieder freigibt. In der Realität wird die Applikation natürlich deutlich größer sein. Das ändert aber nichts an meiner Strategie.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
// overloadOperatorNewAndDelete.cpp
// #include "myNew.hpp"
// #include "myNew2.hpp"
// #include "myNew3.hpp"
#include <iostream>
#include <string>
class MyClass{
float* p= new float[100];
};
class MyClass2{
int five= 5;
std::string s= "hello";
};
int main(){
int* myInt= new int(1998);
double* myDouble= new double(3.14);
double* myDoubleArray= new double[2]{1.1,1.2};
MyClass* myClass= new MyClass;
MyClass2* myClass2= new MyClass2;
delete myDouble;
delete [] myDoubleArray;
delete myClass;
delete myClass2;
// getInfo();
}
|
Die Frage stellt sich mir nun natürlich. Gibt es in dem Programm zu jedem new einen delete Aufruf?
Ein bisschen nachlässig
Diese Frage lässt sich einfach beantworten, indem ich den globalen operator new und delete überlade. Bei beiden Operatoren zähle ich mit, wie häufig sie verwendet wurden.
Operator new
Den globalen operator new gibt es in vier Variationen.
void* operator new ( std::size_t count );
void* operator new[]( std::size_t count );
void* operator new ( std::size_t count, const std::nothrow_t& tag);
void* operator new[]( std::size_t count, const std::nothrow_t& tag);
Die ersten zwei Varianten, werfen eine std::bad_alloc Ausnahme, wenn sie den Speicher nicht bereitstellen können. Die letzten zwei Varianten geben einen Null-Zeiger zurück. Das schöne ist, dass es ausreicht die Variante 1 zu überladen, da die 2-4 Variante implizit void* operator new ( std::size_t count ) aufrufen. Das gilt auch für die Variante 2 und 4, die für C-Arrays konzipiert sind. Die Details zum globalen operator new sind auf cppreference.com schön nachzulesen.
Die Aussagen lassen sich direkt auf operator delete übertragen.
Operator delete
Für den operator delete gibt es sogar sechs Variationen.
void operator delete ( void* ptr );
void operator delete[]( void* ptr );
void operator delete ( void* ptr, const std::nothrow_t& tag );
void operator delete[]( void* ptr, const std::nothrow_t& tag );
void operator delete ( void* ptr, std::size_t sz );
void operator delete[]( void* ptr, std::size_t sz );
Wie bei operator new ist es bei operator delete ausreichend, nur die Variante eins zu implementieren, da alle übrigen 5 Varianten implizit void operator delete ( void* ptr ) aufrufen. Noch ein Wort zu den letzten zwei operator delete Funktionen. In ihnen steht die Länge des korrespondierenden new Operators mit der Variable sz zu Verfügung. Die Details gibt es wieder auf cppreference.com.
In dem Programm overloadOperatorNewAndDelete.cpp kommentiere ich nun die Zeile 3 ein, in der die Header-Datei myNew.hpp eingelesen wird. Das gleiche gilt für die Zeile 34, in der die Funktion getInfo Information zum Speichermanagement ausgibt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// myNew.hpp
#ifndef MY_NEW
#define MY_NEW
#include <cstdlib>
#include <iostream>
#include <new>
static std::size_t alloc{0};
static std::size_t dealloc{0};
void* operator new(std::size_t sz){
alloc+= 1;
return std::malloc(sz);
}
void operator delete(void* ptr) noexcept{
dealloc+= 1;
std::free(ptr);
}
void getInfo(){
std::cout << std::endl;
std::cout << "Number of allocations: " << alloc << std::endl;
std::cout << "Number of deallocations: " << dealloc << std::endl;
std::cout << std::endl;
}
#endif // MY_NEW
|
In der Datei lege ich zwei statische Variablen alloc und dealloc (Zeile 10 und 11) an, die mit zählen, wie oft der überladene operator new (Zeile 13) und operator delete (Zeile 18) aufgerufen wird. In beiden Funktionen greife ich wiederum auf die Funktionen std::malloc bzw. std::free zu Speicherallokation als auch Speicherfreigabe zurück. Die Funktion getInfo (Zeile 23 - 31) fasst die Ergebnisse zusammen und gibt sie aus.
Die Frage ist nun, habe ich den Speicher sauber aufgeräumt?
Anscheinend war ich nicht so ordentlich. Jetzt weiß ich zumindesten, dass ich Speicherlecks besitze. Vielleicht hilft es mir ja weiter, wenn ich die Adresse der Objekte bestimme, die ich nicht aufgeräumt habe.
Adresse der Speicherlecks
In die Headerdatei myNew2.hpp muss ich jetzt mehr Intelligenz rein stecken.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// myNew2.hpp
#ifndef MY_NEW2
#define MY_NEW2
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <new>
#include <string>
#include <array>
int const MY_SIZE= 10;
std::array<void* ,MY_SIZE> myAlloc{nullptr,};
void* operator new(std::size_t sz){
static int counter{};
void* ptr= std::malloc(sz);
myAlloc.at(counter++)= ptr;
return ptr;
}
void operator delete(void* ptr) noexcept{
auto ind= std::distance(myAlloc.begin(),std::find(myAlloc.begin(),myAlloc.end(),ptr));
myAlloc[ind]= nullptr;
std::free(ptr);
}
void getInfo(){
std::cout << std::endl;
std::cout << "Not deallocated: " << std::endl;
for (auto i: myAlloc){
if (i != nullptr ) std::cout << " " << i << std::endl;
}
std::cout << std::endl;
}
#endif // MY_NEW2
|
Die zentrale Idee ist es, in dem statischen Array myAlloc (Zeile 15) genau Buch zu führen, welche Adresse ich von std::malloc (Zeile 19) erhalten habe und welche ich mit std::free wieder (Zeile 27) freigegeben habe. In der Funktion operator new kann ich natürlich keinen Container verwenden, der Speicher anfordert. Da dieser Container wiederum die Funktion operator new aufruft. Eine Rekursion, die zwangsläufig zum Absturz des Programms führt. Aus diesem Grunde kommt std::array in Zeile 15 zu Einsatz, da std::array seinen Speicher bereits zur Compilezeit anfordert. Da es natürlich passieren kann, dass das std::array für das neue Hinzufügen von Elementen zu klein ist, verwende ich in dem konkreten Fall den myAlloc.at(counter++). Dieser überprüft die Grenzen des Arrays.
Welche Speicheradressen habe ich vergessen freizugeben? Diese Frage beantwortet die Ausgabe des Programms.
Eine naive Suche nach Objekten im Code, die diese Adresse besitzen, ist keine gute Idee. Denn es kann durchaus sein, dass der Aufruf std::malloc eine bereits vergebene Adresse nochmals zurückgibt. Dies ist möglich, wenn das ursprünglich erzeugte Objekt zwischenzeitlich gelöscht wurde.
Warum sind die Adresse aber trotzdem Teil der Lösung? Wir müssen nur die Speicheradresse beim Erzeugen der Objekte mit den Adressen der nicht gelöschten Speicherbereichen vergleichen.
Vergleich der Speicheradressen
Neben der reservierten Speicherbereiche besitzen wir natürlich auch die Größe des reservierten Speichers. Die Information lasse ich zusätzlich in operator new einfließen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// myNew3.hpp
#ifndef MY_NEW3
#define MY_NEW3
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <new>
#include <string>
#include <array>
int const MY_SIZE= 10;
std::array<void* ,MY_SIZE> myAlloc{nullptr,};
void* operator new(std::size_t sz){
static int counter{};
void* ptr= std::malloc(sz);
myAlloc.at(counter++)= ptr;
std::cerr << "Addr.: " << ptr << " size: " << sz << std::endl;
return ptr;
}
void operator delete(void* ptr) noexcept{
auto ind= std::distance(myAlloc.begin(),std::find(myAlloc.begin(),myAlloc.end(),ptr));
myAlloc[ind]= nullptr;
std::free(ptr);
}
void getInfo(){
std::cout << std::endl;
std::cout << "Not deallocated: " << std::endl;
for (auto i: myAlloc){
if (i != nullptr ) std::cout << " " << i << std::endl;
}
std::cout << std::endl;
}
#endif // MY_NEW3
|
Damit wird die Allokation und Deallokation der Applikation deutlich transparenter.
Ein einfacher Vergleich zeigt: Zum einen wurde ein Objekt mit 4 Byte nicht freigegeben, zum anderen ein Objekt mit 400 Byte. Darüber hinaus entspricht die Reihenfolge der Allokation im Sourcecode der der Ausgabe des Programms. Nun sollte es recht einfach sein, die vergessenen Speicherfreigaben zu identifizieren.
Wie geht's weiter?
Das Programm besitzt noch zwei Unschönheiten. Zum einen wird der Speicher für das std::array statisch allokiert, zum andern möchte ich gerne direkt sehen, welcher Speicherbereich nicht freigegeben wurde. Im nächsten Artikel werde ich an den Unschönheiten arbeiten.
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...