Benutzer:Havaniceday/C++ Anmerkungen

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

↑ C++

Hinweis: Du darfst diese Seite editieren!
Ja, wirklich. Es ist schön, wenn jemand vorbeikommt und Fehler oder Links korrigiert und diese Seite verbessert. Sollten deine Änderungen aber dem Inhaber dieser Benutzerseite nicht gefallen, sei bitte nicht traurig oder verärgert, wenn sie rückgängig gemacht werden.
Wikipedia ist ein Wiki, sei mutig!

Funktionen[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

Den (letzten) Funktionsparemetern können "default-Werte" zugewiesen sein:

int function( int arg1, int arg2, int arg3=0, char arg4='x') { ... }

Array-Argumente[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

#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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

Mittels Casts können Typumwandlungen von Objekten vorgenommen werden. Es gibt drei verschiedene Cast-Formen:

static_cast[Bearbeiten | Quelltext bearbeiten]

Mit static_cast können Umwandlungen zwischen verwandten Typen vorgenommen werden: Umwandlungen zwischen Zeigertypen und Objekten.

reinterpret_cast[Bearbeiten | Quelltext bearbeiten]

Mit reinterpret_cast können Umwandlungen zwischen nicht verwandten Typen vorgenommen werden, wie zwischen Zeigern und Objekten.

const_cast[Bearbeiten | Quelltext bearbeiten]

const_cast hebt eine const-Deklaration auf (verwirft sie).

Beispiele:[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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)[Bearbeiten | Quelltext bearbeiten]

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)[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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[Bearbeiten | Quelltext bearbeiten]

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