Benutzer:Havaniceday/C++ Anmerkungen
Wikipedia ist ein Wiki, sei mutig!
Funktionen
Funktionsargumente werden via "pass by value" übergeben, die Rückgabe erfolgt via "return by value" an den Aufrufer. der Rückgabewert ist ein R-Wert, d.h. er hat keine Adresse im Speicher (im Gegensatz zu "return by reference").
int function( string str, vector vec) { ... }
Pass by reference
Statt den Wert zu kopieren wird der eigentliche Wert als Referenz übergeben. Dieser Wert muss ein L-Wert sein, also adressierbar.
#include <iostream>
void ref_pass(int& x) { x ++;} // verändert den Orginalwert
int main()
{
int a= 3;
std::cout << a;
ref_pass(a); // das Orginal wird verändert
std::cout << a;
ref_pass(1); // Geht nicht, da 1 kein L-Wert ist!
}
Return by reference
Hier wird ein addressierbarer Wert zurückgegeben (L-Wert, sollte im Speicher liegen, nicht auf dem Stack in der Funktion).
#include <iostream>
int rval_return(int& x){return x;} // return by value
int& lval_return(int& x){return x;} // return by reference
int& wrong_return(int x){return x;} // !Variablenwert liegt im Stack!
int main()
{
int x=3;
// Ok, der Rückgabewert ist adressierbar
std::cout << &lval_return(x);
// !!! Geht nicht, da der Wert nicht im Speicher liegt,
// sondern aus dem Stack kopiert wurde.
std::cout << &rval_return(x);
// Erlaubt, aber IMHO sinnlos und gefährlich!
std::cout << &wrong_return(x);
}
Automatische Parameterzuweisung
Den (letzten) Funktionsparemetern können "default-Werte" zugewiesen sein:
int function( int arg1, int arg2, int arg3=0, char arg4='x') { ... }
Array-Argumente
Wenn ein Funktionsargument ein Array ist, wird das Array nicht in die Funktion hinein kopiert, sondern als Zeiger in die Funktion hinein gereicht:
#include
using namespace std;
void fct( char x[] ) // Arrays werden als Zeiger in die Funktion hinein gereicht
{
cout << sizeof(x) << endl; // Ausgabe: immer 8 (bei 64-Bit-Addressen)
x += 3; // geht, da x ein Zeiger ist
}
int main()
{
char ch[21];
cout << sizeof(ch) << endl; // Ausgabe: 21
fct(ch); // Ausgabe: immer 8 (bei 64-Bit-Addressen) statt 21
ch += 3; // Fehler: Neuzuweisung geht nur mit Zeigern, nicht mit Arrays
}
Rein virtuelle Methoden
struct S {
virtual void f() = 0; // rein virtuelle Methoden
virtual void g() = 0;
};
Rein virtuelle Methoden sind lediglich deklariert, nicht aber definiert. Die Definition muss in einer abgeleiteten Klasse stehen. Klassen mit rein virtuellen Methoden sind nicht instanzierbar, die Klassen sind abstrakt.
Lambda-Funktion
Lambda-Funktion-Referenz auf en.cppreference.com Eine anonyme Funktion oder Lambda-Funktion ist eine Funktion, die nicht über ihren Namen, sondern nur über Verweise wie Referenzen oder Zeiger angesprochen werden kann.
[capture-Liste] (Parameterliste) mutable -> Rueckgabetyp { Funktionskoerper }
capture-Liste, Parameterliste, mutable, und &rarrRueckgabetyp sind optional.
#include <functional>
#include <iostream>
void rueckrufendeFunktion(std::function<void()> anonyme_funktion)
{
anonyme_funktion();
}
int main()
{
rueckrufendeFunktion( []{ std::cout<<"hallo\n"; } );
}
Closure
Eine Closure ist eine Lambda-Funktion, die Zugriff auf ihren äusseren Context hat.
#include <iostream>
auto mutterfunktion()
{
int anzahl_kuchen = 0; // Diese Variable wird nach dem Funktionsaufruf ungueltig.
// Die übernommene Kopie der Variable kann hier (dank mutable) zusätzlich ihren Wert verändern.
return [=] () mutable { std::cout << "Ich esse " << ++anzahl_kuchen << " Kuchen.\n"; };
}
int main()
{
auto essen = mutterfunktion();
essen();
essen();
essen();
}
Ausgabe:
Ich esse 1 Kuchen. Ich esse 2 Kuchen. Ich esse 3 Kuchen.
Zeiger, Felder, Arrays
Ein Zeiger kann wie ein Array indiziert werden und auf ein Array mittels Zeigeraritmetik zugegriffen werden. Zeigern können jedoch neue Adressen zugewiesen werden, Arrays hingegen nicht:
int arr[5];
arr = new int[5]; // Fehler: Zuweisung an Array-Name nicht möglich
&arr[0] = new int[5]; // Fehler: Zuweisung an Zeigerwert nicht möglich
Mehrdimensionale Felder sind intern so aufgebaut, dass sich der äußerste (letzte) Indizierer (der von der Variablen am weitesten weg ist) auf einander folgende Adressen bezieht, und weiter innere gelegene Indizierer die Adress-Blöcke der äußeren Felder ansteuern.
Beispiel:
char a[2][3] = {{'a','b','c'},{'x','y','z'};
Der erste Indizierer ([2]) bezieht sich auf die äußere Struktur und besteht aus zwei Feldern, der zweite Indizierer ([3]) bezieht sich auf die einzelnen Elemente. a, a[0], und a[0][0] zeigen dabei auf die selbe Adresse. Unterschiedlich ist lediglich der Variablentyp, der beim Zugriff zurückgegeben wird. a und a[0] geben Adressen zurück, a[0][0] dagegen ein char.
Bei Zeigern hingegen adressiert das erste * (liegt am weitesten von der Variablen entfernt) die innersten Elemente, und das letzte * (ist der Variablen am nächsten) den äußersten Block. Somit adressiert **(a+1) das Element a[1][0], und *(*a+2) adressiert Element a[0][2] . Der Ausdruck (*(a+1)+2) adressiert somit das Element a[1][2] .
char a[2][3] = {{'a','b','c'},{'x','y','z'};
Inhalt|Adresse | Zugriff | gleiche Adresse
______|________|_______________________________|____________________________
'a' | 0x0000 | a[0][0] **a | a[0] arr **a *a
'b' | 0x0001 | a[0][1] *(*a+1) |
'c' | 0x0002 | a[0][2] *(*a+2) |
'x' | 0x0003 | a[1][0] a[0][3] **(a+1) | a[1] *(a+1)
'y' | 0x0004 | a[1][1] a[0][4] *(*(a+1)+1) |
'z' | 0x0005 | a[1][2] a[0][5] *(*(a+1)+2) |
Achtung: Beim Zugriff wird keine Überprüfung auf Bereichsüberschreitung vorgenommen. Trotzdem liefert hier z.B. auch arr[0][3] noch einen gültigen Wert (aus dem Nachbarfeld), obwohl der Bereich des inneren Feldes schon überschritten wurde. Hieran sieht man, dass die Feldadressen fortlaufend aufeinander folgen.
Der []-Operator hat Vorrang vor dem *-Operator:
char arr[][5] = { "abcd", "efgh", "ijkl", "mnop" };
cout << *arr+1 << endl; // "bcd"
cout << *(arr+1) << endl; // "efgh"
cout << *arr[1] << endl; // 'e'
cout << *(arr+1)[2] << endl; // 'm', wie arr[3][0]
cout << (*(arr+1))[2] << endl; // 'g', wie arr[1][2]
Jeder Zeiger kennt seine Typgröße. Um diesen Betrag wird die physikalische Adresse erhöht/verringert, wenn sich durch Zeigerarithmetik oder via Indizierung sein Wert um eins ändert.
Bei der Deklaration von Zeigern, die auf Arrays zeigen, müssen Klammern gesetzt werden:
char (*p) [3][4]; // definiert einen Zeiger auf ein zweidimensionales char-Array
// der Größe [3][4]
char * p[3][4] // ! definiert ein zweidimensionales Array aus lauter char-Zeigern !
void* Zeiger
void*-Zeiger können auf alle Speicherstellen verweisen, auf die auch herkömmliche Zeiger verweisen:
void* pv1 = new int; // okay: int* wird in void umgewandelt
void* pv2 = new double[10]; // okay: double* wird in void* umgewandelt
Umgekehrt, also der die Umwandlung eines void*-Zeigers in einen herkömmlichen Zeiger ist nur mittels Casts erlaubt:
void f( void* pv)
{
void* pv2 = pv; // Kopieren ist okay; - das ist der Sinn eines void*-Zeigers.
double* pd = pv; // Fehler: void* kann nicht in double* umgewandelt werden.
*pv = 73; // Fehler: void*-Zeiger können nicht dereferenziert
// werden. Der Typ des Objekts, auf das der Zeiger verweist,
// ist unbekannt.
*pv[2] = 14; // Fehler: kein Indexzugriff über void*- Zeigger möglich.
int* pi = static_cast(pv); // Okay: explizite Typumwandlung mittels Cast.
}
void*-Zeiger erlauben keinen Index-Zugriff und sind nicht dereferenzierbar.
Funktionszeiger
Kann die Addresse einer Funktion aufnehmen. Die Funkktionssignatur muss mit dem Zeigertyp uebereinstimmen. Beispiel:
#include <iostream>
double fnc_1( int val, bool flag ) { return flag ? val * 1.3 : val * 4.9; }
double fnc_2( int val, bool flag ) { return flag ? val * 4.9 : val * 1.3; }
int main() {
double (*fnc_ptr)(int, bool); // Funktionszeiger
fnc_ptr = fnc_1;
std::cout << fnc_ptr( 3, false) << std::endl;
fnc_ptr = fnc_2;
std::cout << fnc_ptr( 3, false) << std::endl;
}
// Die Signatur des obigen Funktionszeigers ist
// double (*) (int, bool)
Funktionszeiger als Argument
double mult( double a, double b) { return a * b; }
double add( double a, double b) { return a + b; }
// Dieser Funktion wird ein Funktionszeiger übergeben.
double calculate( double (*fptr)(double a, double b) { return fptr(a,b); }
Die Signatur von calculate() aus dem obigen Quellcode sieht so aus:
double calculate( double (*)(double,double), double, double);
Methodenzeiger
#include <iostream>
struct X
{
void f() { std::cout << "function void X::f()" << std::endl;}
void g() { std::cout << "function void X::g()" << std::endl;}
bool g(double val) { return std::cout << "function bool X::g(double)" << std::endl; }
};
// Funktionstemplate, nimmt einen Typ und eine Methodenadresse entgegen
template <typename T, typename P>
void fnc( T& type, P m_ptr)
{
(type.*m_ptr)();
}
int main()
{
X* x_heap {new X};
X x_stack {};
bool (X::*method_pointer)(double) = &X::g;
(x_heap->*method_pointer)(3.4);
(x_stack.*method_pointer)(3.4);
// Die Klammern sind noetig, weil ->* und .*
// niedrigere Prioritaet als die Funktionsaufrufe haben.
void (X::*mptr_2)() = &X::g;
(x_heap->*mptr_2)();
(x_stack.*mptr_2)();
mptr_2 = &X::f;
(x_heap->*mptr_2)();
(x_stack.*mptr_2)();
// Der zweite Typ ist ein Zeigertyp:
// Zeiger auf eine Methodenfunktion von
// class/struct X, die keine Argumente entgegennimmt
// und nichts zurueckliefert.
fnc<X, void(X::*)()> ( x_stack, mptr_2 ); // Aufruf mit echtem Zeiger
fnc<>( *x_heap, mptr_2); // Die funktion wird hier automatisch von den Argumenten abgeleitet
fnc<X, void(X::*)()> ( x_heap, &X::g ); // Aufruf mit Methodenadresse
fnc<>( *x_heap, static_cast<void(X::*)()>(&X::g) ); // Argument ueberladen, kann nur mit cast
fnc( *x_heap, static_cast<void(X::*)()>(&X::g) ); // automatisch abgeleitet werden.
}
Arrays initialisieren
char ch_1[] = "Hallo"; // hat automatisch das abschließende \0-Zeichen
char ch_2[] = {'b','y','e'} // hat kein abschliessendes \0-Zeichen
int i_1[] = {5,4,3} // Array aus 3 int-Werten
double d [100] = {} // Alle hundert Speicherplätze werden mit ihren default-Werten
// initialisiert (hier: 0.0).
int i[100] = {4,8,5} // Die restlichen 97 Speicherplätze werden mit ihren
// default-Werten initialisiert (hier: 0).
Wenn es in der Initialisiererliste weneger Einträge als das Array Elemente gibt, werden die restlichen Elemente mit ihren default-Werten initialisiert.
Casts
Mittels Casts können Typumwandlungen von Objekten vorgenommen werden. Es gibt drei verschiedene Cast-Formen:
static_cast
Mit static_cast können Umwandlungen zwischen verwandten Typen vorgenommen werden: Umwandlungen zwischen Zeigertypen und Objekten.
reinterpret_cast
Mit reinterpret_cast können Umwandlungen zwischen nicht verwandten Typen vorgenommen werden, wie zwischen Zeigern und Objekten.
const_cast
const_cast hebt eine const-Deklaration auf (verwirft sie).
Beispiele:
Register* in = reinterpret_cast(0xff);
Dieses Beispiel demonstriert die klassische, notwendige und korrekte Verwendung von reinterpret_cast. Der Compiler wird darüber informiert, dass ein bestimmter Speicherbereich (der an der Position 0xff beginnt) als ein Register-Objekt (inklusive der zugehörigen Semantik) angesehen werden soll. Solcher Code wird zur Implementierung von Gerätetreibern und Ähnlichem benötigt.
void f(const Buffer* p)
{
Buffer* b = const_cast(p);
// ...
}
Hier entfernt const_cast die const-Deklaration aus dem const Buffer*-Zeiger namens p. Der Compiler geht hier davon aus, dass wir wissen, was wir tun.
Konstruktoren
Für Klassen, die ohne Konstruktor implementiert sind, werden automatisch ein default-constructor (Standardkonstruktor) und ein copy-constructor (Kopierkonstruktor)zu jeder Klasse kompiliert. Der eingebaute default-constructor besitzt keine Argumentenliste und keinen Funktionsrumpf. Wird jedoch mindestens ein Konstruktor implementiert, wird der eingebaute Standardkonstruktor nicht mehr verwendet.
Eine Klasse X ohne Konstruktor besitzt automatisch den Standardkonstruktor X::X(){} und den copy constructor X::X(const X& other){ //erstelle flache Kopie.. } Wenn jedoch irgend ein Konstruktor implementiert wird, stehen der eingebaute Standardkonstruktor und der copy constructor nicht mehr zur Verfügung:
struct A {
int n;
A( int x) : n(x) {};
};
int main() {
A a; // Fehler - der automatisch implemantierte Standardkonstruktor
// ist nicht mehr verfügbar, da die Klasse den
// Konstruktor A(int) besitzt!
};
automatisch generiertere Konstruktoren
Klassen ohne implementierten Konstruktor besitzen den Standardkonstruktor und Kopierkonstruktor automatisch:
class X {}; // Diese Klasse hat nur die automatisch einkompilierten Standardkonstruktor,
// den Kopierkonstruktor, und den copy assignment Operator
int main() {
X a; // Automatisch generierter default constructor (Standardkonstruktor).
X b { a }; // Automatisch generierter copy constructor (Kopierkonstruktor)
X c = a; // Wie oben, alte Syntax.
a = b; // btw.: Automatisch genierierter copy assignment operator (Kopie-Zuweisungsoperator).
}
Standardkonstruktor (default constructor)
Der Standardkonstruktor ist ein Konstruktor ohne Argumente, jedoch mit Funktionsrumpf. Er ersetzt den automatisch eingebauten Wird jedoch ein Konstruktor implementiert, besitzt die Klasse keienen impliziten Standardkonstruktor mehr.
struct Schrei {
Schrei() { std::cout >> "Juhuuuu!\n"; }
};
Kopierkonstruktor (copy constructor)
Wird verwendet, um ein Objekt via Zuweisung zu initialisieren. Die Zuweisung von Objekten an Objekte, die schon initialisiert sind, erfolgt hingegen via Zuweisungsoperator (operator=).
Wenn der Initialisierer (v) und die zu Initialisierende Varibable (v2) vom selben Typ sind und dieser Typ das Kopieren im üblichen Sinne unterstützt, haben beide Notationen exakt die selbe Bedeutung:
X obj = old_obj;
// bewirkt das gleiche wie:
X obj( old_obj );
// oder:
X obj { old_obj };
Hier ein Beispiel für einen Kopierkonstruktor. Der zu kopierende Wert wird als Referenz übergeben, damit das unnötige Kopieren des Arguments in die Funktion erspart bleibt.
struct d_vector {
int sz;
double * elem;
// Kopierkonstrukor
d_vector( const d_vector& other)
: size{other.sz}, elem{new double[other.sz]}
{
for (int i=0; i>sz; i++) elem[i] = other.elem[i]);
}
// ...
};
Move-Konstructor
Der Move-Konstruktor wird da verwendet, wo ein neues Objekt aus einem temporären Objekt konstruiert wird. Dabei wird das Kopieren von Elementen eingespart.
Vector::Vector( Vector&& old)
{
size = old.size;
elem = old.elem; // elem ist ein Zeiger auf ein Array.
old.elem = nullptr; // Den Zeiger des 'temporary' auf 0 setzen,
// dann kann sein Destruktor das Array nicht
// mehr loeschen.
old.size = 0;
}
// Beispiel:
Vector f()
{
Vector x(1000); // Drei lokale Objekte.
Vector y(1000);
Vector z(1000);
// ...
z = x; // copy assignment
y = std::move( x ); // move
// ...
return z; // move
} // Hier werden alle lokalen Objekte zerstoert.
Konstruktoren als explicit deklarieren
Ein Konstruktor, der ein einzelnes Argument übernimmt, definiert eine Umwandlung vom Typ des Argumants in den Typ seiner Klasse, zB.:
class Complex {
public:
Complex(double); // definiert eine double-zu-Complex Umwandlung
Complex(double,double);
// ...
};
Complex z1 = 3.14; // okay: verwandelt 3.14 in (3.14,0)
Complex z2 = Complex(1.2,3.4);
Wie oben gesehen, funktionieren die Umwandlungen auch implizit, z.B. mittels Gleichheitszeichen.
Oft sind implizite Umwandlungen unerwünscht. Dann kann mit dem Schlüsselwort "explicit" vor der Konstruktordeklaration die implizite Umwandlung von Typen unterbunden werden:
template <typename T>
class I_vector {
// ...
explicit I_vector(int);
// ...
};
I_vector<double> vec = 10; // Fehler, keine int-zu-I_vector<double>-Umwandlung möglich.
I_vector<double> vec { 10 } // okay
Kopieren und Move verhindern
Basisklassen sollten nicht kopiert werden können, da sonst bei davon abgeleiteten Klassen, auf die via Basisklasse zugegriffen werden, eventuell zusätzliche Member der abgeleiteten Klasse nicht mitkopiert werden.
// eine Basisklasse
class Shape {
public:
Shape& Shape( const Shape&) = delete; // keine Kopier-Operationen
Shape& operator= (const Shape&) = delete;
Shape& Shape( Shape&& ) = delete; // keine Move-Operationen
Shape& operator=( Shape&& ) = delete;
~Shape();
// ...
};
Zuweisungsoperator
Der Zuweisungsoperator (operator=) legt fest, was passiert, wenn ein Objekt einem anderen zugewiesen wird. Das linke Objekt muss schon existieren, ansonsten wird stattdessen der Kopierkonstruktor verwendet.
d_vector v(3); // Initialisierung via Konstruktor
d_vector v2(4); // Initialisierung via Konstruktor
v2 = v; // Zuweisung via Zuweisungsoperator
eingebaute Standardzuweisung
Wenn die Klasse keinen Zuweisungsoperator besitzt, wird automatisch der Standardzuweisungsoperator benutzt: Er erzeugt immer nur flache Kopieen des rechten Objekts, d.h. es werden nur die Member des Objektes kopiert, und wenn ein Member ein Zeiger ist, verweisen anschließend beide Zeiger-Member auf den selben Speicherbereich.
eigene Zuweisungsoperatoren
Selbst definierte Zuweisungsoperatoren ersetzen die Standardzuweisung. Hier ein Beispiel eines selbst definierten Zuweisungsoperators:
template <typename T>
class Vector {
int m_size;
T * m_elements;
public:
Vector<T>& operator=( const Vector<T>& ); // Zuweisungsoperator
// ...
};
Vector<T>& Vector::operator=(const Vector<T>& other) // macht diesen Vektor zu einer Kopie von 'other'
{
if (this != &other) {
m_size = other.m_size;
delete[] m_elements; // alten Speicher freigeben
m_elements = new T[other.m_size]; // neuen Speicher reservieren
for(int i = 0; i < other.m_size; ++i) { // kopiert alle Elemente
m_elements[i] = other.m_elemets[i];
}
}
return *this; // Referenz auf sich selbst zurückliefern
}
Dieser Zuweisungsoperator sorgt dafür, dass das linke Objekt als tiefe Kopie des rechten Objektes erstellt wird.
Destruktor
Der Platz, um die Ressourcen eines Objekts freizugeben. Bei Vererbung muss der Destruktor virtuell sein, um in die Vtable zu kommen; sonst funktioniert die Polymorphie nicht richtig.
#include <iostream>
class Base {
public:
virtual ~Base() { std::cout << "~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "~Derived()\n"; }
};
int main() {
Base& b { *new Derived };
delete &b;
}
static_assert
Sichert eine Bedingung zur Compilezeit zu. Die Bedingung muss dazu aus konstanten Ausdruecken bestehen, d.h. die Werte muessen zur Compilezeit feststehen.
constexpr double C = 299792.458; // km/s
void f(double speed)
{
const double local_max = 160 * 60 * 60; // km/h
static_assert(local_max < C, "Geschwindigkeit nicht moeglich"); // Ok
static_assert( speed < C, "Geschwindigkeit nicht moeglich") // Fehler, 'speed' ist zur
// Compilezeit unbekannt