Martin Sulzmann
Manuelle Speicherverwaltung in C++ ist trickreich
Zu wenige “deletes” (Speicherleck)
Zu viele “deletes (es kracht)
Abhilfen
Resource Acquisition Is Initialization (RAII)
Konstruktor legt Speicher an
Destruktor gibt Speicher frei
Copy/Move Semantik (von Hand selbstgebasteltet)
“smart pointers” (clevere Bibliothek welche die Copy/Move Semantik umsetzt)
Wir verwenden ab jetzt C++11
Umdrehen (“reverse”) eines Strings in C
Anlegen von Speicher via “malloc”
Fehlende Speicherfreigabe (“free”)
Zu jedem “malloc” Bedarfs es eines “free” zur richtigen Zeit!
#include <stdlib.h>
#include <iostream>
#include <string>
using namespace std;
int length(char *s) {
int n = 0;
while(*s != '\0') {
n++;
s++;
}
return n;
}
char* reverse(char* s) {
const int n = length(s);
char* t = (char*)malloc(n + 1);
int i;
for(i = 0; i < n; i++) {
t[i] = s[n-1-i];
}
t[n] = '\0';
return t;
}
void testReverse() {
char s[] = "Hallo";
cout << "\n" << s;
cout << "\n" << reverse(s);
cout << "\n" << reverse(reverse(s));
}
void testReverse2() {
char s[] = "Hallo";
cout << "\n" << s;
char* s1 = reverse(s);
cout << "\n" << s1;
free(s1);
char* s2 = reverse(s);
char* s3 = reverse(s2);
cout << "\n" << s3;
free(s2);
free(s3);
}
int main() {
testReverse();
testReverse2();
return 0;
}
Konstruktor legt Speicher an
Destruktor gibt Speicher frei
Konstruktor legt Objekt an auf dem “Stack”.
Destruktor wird aufgerufen sobald das Objekt freigegeben wird. Annahme ist, dass das Objekt auf dem “Stack” liegt.
Dadurch wird garantiert, dass es zu jedem “new” ein passendes “delete” gibt.
In C++, wird “malloc” ersetzt durch “new” und “free” wird ersetzt durch “delete”.
Beachte:
Wir verwenden (unten) delete[]
(anstatt delete).
Wieso?
Via new
liegen wir ein Array von char
Werten an.
Der Aufruf delete
gibt nur das erste Zeichen
frei
Deshalb verwenden wir delete[]
um das gesamte Array
freizugeben.
#include <stdlib.h>
#include <iostream>
#include <string>
using namespace std;
// Resource Acquisition Is Initialization (RAII)
class String {
public:
String() {
s = new char[1]; // (char*)malloc(1);
s[0] = '\0';
}
String(char* t) {
int n = length_(t);
s = new char[n+1];
for(int i=0; i<n; i++) {
s[i] = t[i];
}
s[n] = '\0';
}
~String() {
delete[] s; // free the sequence of chars allocated
}
int length() {
return(length_(this->s));
}
void reverse() {
int n = this->length();
char* t = new char[n+1];
for(int i = 0; i < n; i++) {
t[i] = s[n-1-i];
}
t[n] = '\0';
delete[] s;
s = t;
}
private:
char* s;
int length_(char *s) {
int n = 0;
while(*s != '\0') {
n++;
s++;
}
return n;
}
// Friends can access private attributes.
friend ostream& operator<< (ostream &out, String &s);
};
ostream& operator<< (ostream &out, String &str) {
for(int i=0; i<str.length(); i++) {
out << str.s[i];
}
return out;
}
void testReverse() {
char s[] = "Hallo";
String str(s);
cout << "\n" << str;
str.reverse();
cout << "\n" << str;;
}
int main() {
testReverse();
return 0;
}
Objekte werden Elementweise kopiert
In unserem Beispiel verweisen daher zwei verschiedene String Objekte auf den gleichen Speicher
Der Destruktor wird für jedes Objekt aufgefufen
Zweifacher Aufruf von “delete” auf der gleiche Speicheradresse
Führt zum Programmabsturz
#include <stdlib.h>
#include <iostream>
#include <string>
using namespace std;
// Kopieren mit Zeiger
// Zwei-faches delete!
class String {
public:
String() {
s = new char[1];
s[0] = '\0';
}
String(char* t) {
int n = length_(t);
s = new char[n+1];
for(int i=0; i<n; i++) {
s[i] = t[i];
}
s[n] = '\0';
}
~String() {
delete[] s;
}
private:
char* s;
int length_(char *s) {
int n = 0;
while(*s != '\0') {
n++;
s++;
}
return n;
}
};
void f(String str2) {
}
// Destruktoraufruf fuer str2
// => delete[] str2.s
int main() {
char s[] = "Hallo";
String str(s);
f(str); // str2.s = str.s
return 0;
}
// Destruktoraufruf fuer str
// => delete[] str.s
// Zweifaches delete!
Jedesmal wenn ein Objekt (mit einem Zeiger) kopiert wird
Wie und wann werden Objekte kopiert?
Via Kopierkonstruktur
Via Zuweisungsoperator
Beides kann überladen werden.
#include <stdlib.h>
#include <iostream>
#include <string>
using namespace std;
// Copy Semantik
class String {
public:
String() {
s = new char[1];
s[0] = '\0';
}
String(char* t) {
int n = length_(t);
s = new char[n+1];
copy_(t,n,s);
s[n] = '\0';
}
// Copy Konstruktor
// Standard Definition ist: this->s = src.s
// Gilt auch fuer Zuweisung.
String(const String& src) {
int n = length_(src.s);
this->s = new char[n+1];
copy_(src.s,n,this->s);
this->s[n] = '\0';
}
// Copy Zuweisung
String& operator=(const String& src) {
if(this != &src) {
delete[] this->s;
int n = length_(src.s);
this->s = new char[n+1];
copy_(src.s,n,this->s);
this->s[n] = '\0';
}
return *this;
}
~String() {
delete[] s;
}
private:
char* s;
int length_(char *s) {
int n = 0;
while(*s != '\0') {
n++;
s++;
}
return n;
}
void copy_(char* s, int n, char* t) {
int i = 0;
while(i < n) {
t[i] = s[i];
i++;
}
}
};
void f(String str2) {
}
int main() {
char s[] = "Hallo";
String str(s);
f(str); // Copy str
String str2 = str; // Copy str
f(str2);
return 0;
}
Jedes mal eine (deep)copy durchzuführen ist nicht sehr effizient.
Man betrachte
Temporäre Objekte
Objekte welche nicht mehr verwendet werden sollen
In solchen Fällen führen einen “move” durch
#include <stdlib.h>
#include <iostream>
#include <string>
using namespace std;
// Copy/Move Semantik
class String {
public:
String() {
s = new char[1];
s[0] = '\0';
}
String(char* t) {
int n = length_(t);
s = new char[n+1];
copy_(t,n,s);
s[n] = '\0';
}
// Copy Konstruktor
// Standard Definition ist: this->s = src.s
// Gilt auch fuer Move und Zuweisung.
String(const String& src) {
int n = length_(src.s);
this->s = new char[n+1];
copy_(src.s,n,this->s);
this->s[n] = '\0';
}
// Copy Zuweisung
String& operator=(const String& src) {
if(this != &src) {
delete[] this->s;
int n = length_(src.s);
this->s = new char[n+1];
copy_(src.s,n,this->s);
this->s[n] = '\0';
}
return *this;
}
// Move Konstruktor
String(String&& src) {
this->s = src.s;
src.s = nullptr;
}
// Move Zuweisung
String& operator=(String&& src) {
if(this != &src) {
delete[] this->s;
this->s = src.s;
src.s = nullptr;
}
return *this;
}
~String() {
delete[] s;
}
void print() {
cout << this->s;
}
private:
char* s;
int length_(char *s) {
int n = 0;
while(*s != '\0') {
n++;
s++;
}
return n;
}
void copy_(char* s, int n, char* t) {
int i = 0;
while(i < n) {
t[i] = s[i];
i++;
}
}
};
void f(String str2) {
}
int main() {
char s[] = "Hallo";
String str(s);
f(str); // Copy str
String str3 = str; // Copy str
f(str3);
f(String()); // Move eines temporaeren String Objekts
String str4 = move(str);
f(str4);
// str.print();
// Liefert segmentation fault
// wegen "move"
return 0;
}
Destruktor
“copy” Konstruktor (für lvalue refs)
“copy” Zuweisung (für lvalue refs)
Definiere (geeignet)
Destruktor
“copy” Konstruktor (für lvalue refs)
“copy” Zuweisung (für lvalue refs)
“move” Konstruktor (für rvalue refs)
“move” Zuweisung (für rvalue refs)