Im letzten Artikel habe ich operator new und delete überladen. Damit war es möglich, Speicherlecks zu erkennen und einen ersten Hinweis auf den Bösewicht zu erhalten. Meine Lösung besaß aber noch zwei größere Unschönheiten. Mit diesem Artikel werde ich diese beseitigen.
Welche waren die zwei Unschönheiten meines letzten Artikels? Zum einen, bekam ich nur einen Hinweis darauf, welcher Speicher nicht mehr freigegeben wurde, zum anderen musste ich die ganze Buchführung zum Speichermanagement bereits zur Compilezeit bereits vorhalten. Wer die Einschränkungen genauer verstehen will, verweise ich gerne nochmals auf den ersten Artikel dieser Miniserie. Beide Unschönheiten lösen sich aber jetzt in Wohlgefallen auf.
Wer ist der Bösewicht?
Besondere Umstände fordern besondere Maßnahmen. In diesem Fall komme ich nicht umhin, ein kleines Makro zu debugging Zwecken einzusetzen.
Ich möchte es gerne explizit betonen. Ich bin kein Freund von Makros.
Wie schaut das Makro aus? #define new new(__FILE__, __LINE__)
Das kleine Makro bewirkt, dass jeder new-Aufruf auf einen überladenen new-Aufruf umgeschrieben wird. Dieser überladene new-Aufruf erhält zusätzlich den Namen der Datei und die Zeilennummer, der den new-Aufruf angestoßen hat. Das ist genau die Information, die ich benötige.
Was passiert, wenn ich das Makro in Zeile 6 verwende?
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
|
// overloadOperatorNewAndDelete2.cpp
#include "myNew4.hpp"
// #include "myNew5.hpp"
#define new new(__FILE__, __LINE__)
#include <iostream>
#include <new>
#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;
dummyFunction();
getInfo();
}
|
Der Präprozessor substituiert alle new-Aufrufe. Das zeigt insbesondere die veränderte main-Funktion.
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
|
class MyClass{
float* p= new("overloadNewAndDelete.cpp", 14) float[100];
};
class MyClass2{
int five= 5;
std::string s= "hello";
};
int main(){
int* myInt= new("overloadNewAndDelete.cpp", 24) int(1998);
double* myDouble= new("overloadNewAndDelete.cpp", 25) double(3.14);
double* myDoubleArray= new("overloadNewAndDelete.cpp", 26) double[2]{1.1,1.2};
MyClass* myClass= new("overloadNewAndDelete.cpp", 28) MyClass;
MyClass2* myClass2= new("overloadNewAndDelete.cpp", 29) MyClass2;
delete myDouble;
delete [] myDoubleArray;
delete myClass;
delete myClass2;
dummyFunction();
getInfo();
}
|
Die Zeilen 2 und 12 - 17 zeigen schön, wie der Präprozessor die Konstanten __FILE__ und __LINE__ im Makro ersetzt. Wie funktioniert das ganze? Das Rätsel löst die Header-Datei myNew4.hpp.
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// myNew4.hpp
#ifndef MY_NEW4
#define MY_NEW4
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <new>
#include <array>
int const MY_SIZE= 10;
int counter= 0;
std::array<void* ,MY_SIZE> myAlloc{nullptr,};
void* newImpl(std::size_t sz,char const* file, int line){
void* ptr= std::malloc(sz);
std::cerr << file << ": " << line << " " << ptr << std::endl;
myAlloc.at(counter++)= ptr;
return ptr;
}
void* operator new(std::size_t sz,char const* file, int line){
return newImpl(sz,file,line);
}
void* operator new [](std::size_t sz,char const* file, int line){
return newImpl(sz,file,line);
}
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);
}
#define new new(__FILE__, __LINE__)
void dummyFunction(){
int* dummy= new int;
}
void getInfo(){
std::cout << std::endl;
std::cout << "Allocation: " << std::endl;
for (auto i: myAlloc){
if (i != nullptr ) std::cout << " " << i << std::endl;
}
std::cout << std::endl;
}
#endif // MY_NEW4
|
In Zeile 25 und 29 implementiere ich meine speziellen Operatoren new und new [], die ihr Funktionalität an die Hilfsfunktion newImpl (Zeile 18 - 23) delegieren. Diese Funktion erledigt zwei wichtige Aufgaben. Zum einen gibt sie zu jedem new-Aufruf den Namen der Sourecodedatei und die Zeilennummer aus (Zeile 20), zum anderen führt sie in dem statischen Array myAlloc genau Buch über jede verwendete Speicheradresse (Zeile 21). Dies passt genau zum Verhalten des überladenen operator delete, der alle Speicheradressen auf einen Null-Zeiger nullptr setzt (Zeile 35). Die Speicheradresse repräsentiert den deallokierten Speicherbereich. Die Funktion getInfo gibt zum Abschluss alle Speicheradressen der Speicherbereiche aus, die nicht freigegeben wurde. Diese lassen sich nun direkt mit der Ausgabe in 20 vergleichen.
Natürlich kann ich das Makro auch direkt in der Datei myNew4.hpp anwenden. Nun aber genug der Theorie. Welche Ausgabe produziert das Programm?
Die Speicherbereiche zu den Speicheradressen 0x8c3010, 0x8c3090 und 0x8c3230 wurden nicht freigegeben. Die Bösewichte sind die new-Aufrufe in den Zeilen 24 und 14 (overloadNewAndDelete.hpp) und der new-Aufruf in der Zeile 42 (myNew4.hpp).
Beeindruckt? Ich hoffe doch. Zwei Nachteile besitzt die vorgestellte Technik aber und die will ich nicht verschweigen. Ein Kleinen und einen Großen.
- Ich muss sowohl den einfachen Operator new als auch den Operator new [] für Arrays überladen. Dies gilt, da der überladene operator new nicht als Fallback für die drei weiteren Operatoren new einspringt.
- Ich kann nicht die speziellen Operatoren new instrumentalisieren, die im Fehlerfall einen Null-Zeiger zurückgeben. Denn diese werden bereits explizit durch das Argument std::nothrow angestoßen: int* myInt= new (std::nothrow) int(1998);
Nun gilt es nur noch das erste Problem zu lösen. Ich möchte für meine Array myAlloc eine Datenstruktur verwenden, die ihren Speicher zur Laufzeit anfordert. Dadurch ist es nicht mehr notwendig, auf Verdacht Speicher für den statischen Container zu allokieren.
Alles zu Laufzeit
Was war der Grund, dass ich im letzten Artikel keine Speicheranforderungen im operator new verwenden konnte? Der operator new war global überladen. Somit führt jeder new Aufruf im operator new zu einer Endlosrekursion. Genau das ist das Problem, wenn Container wie std::vector verwendet werden, die ihren Speicher dynamisch anfordern.
Diese Randbedingungen gelten aber nicht mehr, denn ich habe nicht den globalen operator new überladen, der als Fallback für die drei weiteren Operatoren new verwendet wird. Durch das Makro werden meine eigenen Varianten des operator new verwendet. Damit kann ich natürlich jetzt auch einen std::vector in meinen überladenen operator new verwenden.
Genau das zeigt das Programm.
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
45
46
47
48
49
50
51
52
53
54
55
56
|
// myNew5.hpp
#ifndef MY_NEW5
#define MY_NEW5
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <new>
#include <string>
#include <vector>
std::vector<void*> myAlloc;
void* newImpl(std::size_t sz,char const* file, int line){
static int counter{};
void* ptr= std::malloc(sz);
std::cerr << file << ": " << line << " " << ptr << std::endl;
myAlloc.push_back(ptr);
return ptr;
}
void* operator new(std::size_t sz,char const* file, int line){
return newImpl(sz,file,line);
}
void* operator new [](std::size_t sz,char const* file, int line){
return newImpl(sz,file,line);
}
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);
}
#define new new(__FILE__, __LINE__)
void dummyFunction(){
int* dummy= new int;
}
void getInfo(){
std::cout << std::endl;
std::cout << "Allocation: " << std::endl;
for (auto i: myAlloc){
if (i != nullptr ) std::cout << " " << i << std::endl;
}
std::cout << std::endl;
}
#endif // MY_NEW5
|
In den Zeilen 13, 19 und 33 kommt der std::vector zum Einsatz.
Wie geht's weiter?
Im nächsten Artikel werde ich mir std::allocator genauer anschauen. Mich interessiert vor allem, wie Speicheranfragen auf spezielle Speicherbereiche abgebildet werden können.
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...