Einführung in C++ - Teil 1

Martin Sulzmann

Inhalt

C++ kompakt

Weitere Themen wie Polymorphie, kopieren von Objekten/Zeigern etc werden separat behandelt.

Warm-up

C++ erlaubt

Im Vergleich dazu in C müssen

weil so ist es für den Compiler einfacher das Program zu analysieren. Damals, als C geboren wurde, war das entwickeln eines Compilers keine einfache Sache.

Folgendes ist ein legales C++ Programm (aber nicht legal in C).

int main() {
  int x = 0;
  int j = 1;

  for(int i=0; i < j; i++) {

    {
        float x;
        x = 1.0 + i;

    }

  }
  return 0;
}

Aufgabe: Wandeln Sie obiges Program in ein äquivalentes C Program um.

IO Streams

IO Stream = Ein-/Ausgabe Strom = Sequenz von Ein-/Ausgaben

#include <iostream>
#include <string>
using namespace std;

int main(void) {
  cout << "Ihr Name bitte: \n";
  string s;
  cin >> s;
  cout << "Aha, Sie heissen ";
  cout << s << endl;
}

IO Streams versus printf

int x;
float y;
printf("Int %d Float %f", x, y);
cout << "Int " << x << "Float " << y;

Statische (Compile-Zeit) versus dynamische (Laufzeit) Analyse

In C, rein dynamische Analyse des Formatstrings.

In C++, statische Analyse, ermöglicht Dank der Überladung von <<.

Betrachte

int x;
float y;

cout << x << y;

Operator << ist links-assoziativ. Obiges interpretiert als

(cout << x) << y;

Zwei verschiedene Instanzen von <<: int und float.

ostream& operator<< (ostream &out, int &i);

ostream& operator<< (ostream &out, float &i)

Was bedeutet int &i (ostream &out, ...)?

Übergabe (und auch Rückgabe) als Referenz (wie in Java). Intern dargestellt durch Zeiger.

IO Streams versus scanf

  cout << "Eingabe von Integer: ";
  cin >> x;
  // cin erwartet Referenzparameter
  cout << "Sie haben eingegeben: " << x;

  printf("Eingabe von Float: ");
  scanf("%f", &y);
  // scanf erwartet die Referenz explizit repraesentiert als Zeiger
  printf("Sie haben eingegeben: %f",y);

Komplettes Beispiel


#include <stdio.h>
#include <iostream>
#include <string>
using namespace std;

int main(void) {
  cout << "Ihr Name bitte: \n";
  string s;
  cin >> s;
  cout << "Aha, Sie heissen ";
  cout << s << endl;


  int x = 1;
  float y = 2.0;

  printf("Int %d Float %f", x, y);
  cout << "Int " << x << " Float " << y;
  // In beiden Faellen erhalten wir die Ausgabe: Int 1 Float 2.0

  printf("Int %d Float %f", y, x);
  // Warning! y passt nicht auf %d !!!

  cout << "Int " << y << " Float " << x;
  // Ausgabe liefert: Int 2.0 Float 1
  // Dies ist nur ein logischer Fehler.

  cout << "Eingabe von Integer: ";
  cin >> x;
  // cin erwartet Referenzparameter
  cout << "Sie haben eingegeben: " << x;

  printf("Eingabe von Float: ");
  scanf("%f", &y);
  // scanf erwartet die Referenz explizit repraesentiert als Zeiger
  printf("Sie haben eingegeben: %f",y);

  cout << "\n Das war's" << endl;
}

Namensräume

Namensraum effektiv eine Klassendeklaration mit genau einer Instanz.

namespace NAME { void foo(void); ... }
 NAME::foo();
 using namespace NAME;
 foo();

Namensräume Beispiel

#include <iostream>
#include <string>
// using namespace std;

int main(void) {
  std::cout << "Ihr Name bitte: \n";
  std::string s;
  std::cin >> s;
  std::cout << "Aha, Sie heissen ";
  std::cout << s << std::endl;
}
#include <iostream>
#include <string>
// using namespace std;

int main(void) {
  std::cout << "Ihr Name bitte: \n";
  string s;
  ...
}
namespace.cpp: In function ‘int main()’:
namespace.cpp:6: error: ‘string’ was not declared in this scope
namespace.cpp:6: error: expected `;' before ‘s’
namespace.cpp:7: error: ‘s’ was not declared in this scope

Klassen

Klassen Beispiel

#include <iostream>
using namespace std;

class Point {
// private:
  float x, y;
 public:
  Point() {} // Default Konstrukor, implizit vorhanden
  Point(float x2, float y2) {
    x = x2;  y = y2;
  }
  ~Point() {
    cout << "das war's" << endl;
  }
  void scale(float f) {
    x = f * x; y = f * y;
  }
}; // anders wie in Java

int main() {
  Point p = Point(1.0,2.0);
}

Mehrere Statements pro Zeile aus Platzgründen

Beachte.

Stack versus Heap

Siehe dazu auch C - Teil 3.

Stack: Verwaltet Funktionsaufrufe und deren lokale Variablen.

Verwaltung (anlegen und löschen) der Daten auf dem Stack ist einfach. Bei Funktionsaufruf wird neuer Stackbereich für Funktion und deren lokale Variablen angelegt. Bei Funktionsrücksprung (return) wird dieser Stackbereich gelöscht.

Heap: Verwaltet dynamisch angelegte Daten.

In der Regel via new angelegt. In Java wird der Heap automatisch verwaltet (sobald keine Referenz mehr auf die dynamisch angelegten Daten existiert werden diese glöscht). In C++ müssen Daten auf den Heap explizit via delete gelöscht werden.

Sprachgebrauch: Löschen von Daten. Bedeutet der von den Daten belegte Speicherbereich kann durch andere Daten belegt werden.

Sprachgebrauch: Stack versus Heap Objekte. In C++ können Objekte auf dem Stack als auch dem Heap angelegt werden. In Java werden alle Objekte auf dem Heap angelegt.

Klassen Beispiel (2)

In der Regel aufgeteilt in Schnittstelle (header) und Implementierung (source)

#ifndef __POINT__
#define __POINT__

class Point {
  float x, y;
 public:
  Point(float x2, float y2);
  ~Point();
  void scale(float f);
};

#endif // __POINT__
#include "point2.h"

#include <iostream>
using namespace std;

Point::Point(float x2, float y2) {
    x = x2;  y = y2;
}
Point::~Point() {
    cout << "das war's" << endl;
}
void Point::scale(float f) {
    x = f * x; y = f * y;
}
#include "point2.h"

int main() {
  Point p = Point(1.0,2.0);
}

Klassen Beispiel (3)

> g++ -c point2.cpp
> g++ -c mainPoint.cpp
> g++ point2.o mainPoint.o -o mainPoint.exe

Klassen - Überladung und Default Parameter

class Point {
  ...
  Point(int z = 0);
  Point(float x2, float y2);
};
#include <iostream>
using namespace std;

class Point {
  float x, y;
 public:
  Point(int z = 0) {
    x = z; y = z;
  }
  Point(float x2, float y2) {
    x = x2;  y = y2;
  }
  ~Point() {
    cout << "das war's " << endl;
  }
  void scale(float f) {
    x = f * x; y = f * y;
  }
};

int main() {
  Point p = Point(1.0,2.0);
  Point p2 = Point(1);
  Point p3;
}

Klassen - Dynamische Objekte: new, delete

Beachte. Standardmässig werden in C++ alle Objekte auf dem Stack (Laufzeitstack) angelegt. Die Verwaltung von Stack Objekten ist einfach. Bei Funktionsaufruf wird neuer Stackbereich für Funktion und deren lokale Variablen/Objekte angelegt. Bei Funktionsrücksprung (return) wird dieser Stackbereich gelöscht.

Wir betrachten hier dynamische (Zeiger) Objekte und deren Operationen:

Einfaches Beispiel mit int Objekten

Wiederholung von bekannten C Konzepten im C++ Gewand.

void f(void) {
  int x = 2;
  int* p = new int(3);
}
void f(void) {
  int x = 2;
  int* p = new int(3);
  delete p;
}

Erweitertes Beispiel

class MyInt {
 public:
  MyInt(int x = 0) {
    p = new int(x);
  }
  ~MyInt() { delete p; }
 private:
  int* p;
};
void g(void) {
   MyInt i1;
   MyInt i2 = MyInt(3);
}

Nochmal dynamische Objekte

Betrachte

void h(void) {
   MyInt i;
   MyInt* p2 = new MyInt(3);
}

-------------         -----------        -----
| MyInt* p2 | ----->  | int* p  | ---->  | 3 |
-------------         ----------         -----

Vermeiden des Speicherlecks durch explizites delete:

void h2(void) {
   MyInt i;
   MyInt* p2 = new MyInt(3);
   delete p2;
}

Klassen - Array von Objekten

SomeClass* c = new SomeClass[5];  // Zeiger auf erstes Element
delete[] c; // NICHT delete c

Für jedes Element zuerst Aufruf Destruktor, dann geben Speicher frei.

  int** ptr = new int*[4];
  for (int i=0; i<4; i++)
      ptr[i] = new int(i);
     // Freigabe Speicherplatz auf die Pointer verweisen
  for (int i=0; i<4; i++)
      delete ptr[i];
  delete[] ptr;
  // Freigabe der durch die Pointer belegte Speicherplatz

Klassen - Stack Beispiel

Betrachte

class Stack {
  public:
  Stack(int size) {
     this->size = size; st = new char[size];
  }
  ~Stack() { delete st; }
  private:
     char* st; int size;
};
int main() {
  Stack* s = new Stack(5);
  delete s; // Geschachtelter delete Aufruf!
  Stack* s1 = new Stack(5);
}

Warnung: Programm ist buggy. Erklärung folgt.

Hier die korrigierte Version.

class Stack {
  public:
  Stack(int size) {
     this->size = size; st = new char[size];
  }
  ~Stack() {
     // delete st;
     delete[] st; // Loeschen des gesamten Arrays
  }
  private:
     char* st; int size;
};
int main() {
  Stack* s = new Stack(5);
  delete s; // Geschachtelter delete Aufruf!
  Stack* s1 = new Stack(5);
  delete s1; // Manuelle Speicherverwaltung
}

Dynamisches Speicherlayout eines Stack Objektes


________________
| int size = 5  |
----------------                    -----------------------
| int* size     | --------------->  | Array der Groesse 5 |
-----------------                   -----------------------

Initialisierung von Objekten

int main() {
  Stack* s1 = new Stack(5);
  Stack s2 = Stack(5);
  delete s1;
}

Typische Probleme der manuellen Speicherverwaltung

Speicherleck ("zu wenige deletes")

int main() {
  Stack* s1 = new Stack(5);
  // delete s1;
}

"Absturz" (segmentation fault), Versuch Speicherbereich zweimal freizugeben ("zu viele deletes")

int main() {
  Stack* s1 = new Stack(5);
  Stack* s2 = s1;
  delete s1;
  delete s2;
}

Klassen - Vererbung und Zugriffsrechte

class A
{
public:
    int x; // Alle
protected:
    int y; // Nur Kinder
private:   // Nur A
    int z;
};
class B : public A
{
    // x ist public
    // y ist protected
    // z kein Zurgriff via B
};
class C : protected A
{
    // x ist protected
    // y ist protected
    // z kein Zugriff via C
};
class D : private A
{
    // x ist private
    // y ist private
    // z kein Zugriff via D
};

Klassen - friend Zugriff auf private Member

    class Vector {
      friend class Matrix;      // Alle Methoden von Matrix duerfen
                    // auf den folgenden Bereich
                    // zugreifen
    };

    class Vector {
      friend int Matrix::mult(Vector& v);
                    // Methode mult(Vector& v) von
                    // Matrix darf zugreifen
    };

Klassen - Superklassen/Member Initialisierung

class Vehicle {
  int wheels;
public:
  Vehicle(int w=4):wheels(w) {}
  int max() {return 60;}
};
class Bicycle : public Vehicle {
  bool panniers;
public:
  Bicycle(bool p=true):Vehicle(2),panniers(p) {}
  int max() {return panniers ? 12 : 15;}
};

Klassen - Vererbung und abgeleitete Methoden

Java:

C++:

Laufendes Beispiel

class Vehicle {
  ...
  int max() {return 60;}
};
class Bicycle : public Vehicle {
  ...
  int max() {return panniers ? 12 : 15;}
};
void print_speed(Vehicle &v, Bicycle &b) {
  cout << v.max() << " " << b.max() << "\n";
}
int main() {
    Bicycle b = Bicycle(true);
    print_speed(b,b);
}

Laufendes Beispiel mit virtueller Methode

class Vehicle {
  int wheels;
public:
  Vehicle(int w=4):wheels(w) {}
  virtual int max() {return 60;} // virtuelle Methode
};
class Bicycle : public Vehicle {
  bool panniers;
public:
  Bicycle(bool p=true):Vehicle(2),panniers(p) {}
  int max() {return panniers ? 12 : 15;}
};
void print_speed(Vehicle* v, Bicycle* b) {
  cout << v->max() << " " << b->max() << "\n";
}
int main() {
    Bicycle* b = new Bicycle(true);
    print_speed(b,b);
    delete b;
}

Komplettes Beispiel


#include <iostream>
#include <string>
using namespace std;

// Zwei Namespaces um Konflikte zu vermeiden.

namespace StaticMethod {

class Vehicle {
  int wheels;
public:
  Vehicle(int w=4):wheels(w) {}
  int max() {return 60;}
};

class Bicycle : public Vehicle {
  bool panniers;
public:
  Bicycle(bool p=true):Vehicle(2),panniers(p) {}
  int max() {return panniers ? 12 : 15;}
};

void print_speed(Vehicle &v, Bicycle &b) {
  cout << v.max() << " " << b.max() << "\n";
}

void test() {
  Bicycle b = Bicycle(true);
  print_speed(b,b);
}

} // Namespace StaticMethod


namespace VirtualMethod {

  class Vehicle {
  int wheels;
public:
  Vehicle(int w=4):wheels(w) {}
  virtual int max() {return 60;} // virtuelle Methode
};
class Bicycle : public Vehicle {
  bool panniers;
public:
  Bicycle(bool p=true):Vehicle(2),panniers(p) {}
  int max() {return panniers ? 12 : 15;}
};

void print_speed(Vehicle* v, Bicycle* b) {
  cout << v->max() << " " << b->max() << "\n";
}

void test() {
    Bicycle* b = new Bicycle(true);
    print_speed(b,b);
    delete b;
}

} // Namespace VirtualMethod


int main() {
  StaticMethod::test();
  VirtualMethod::test();
}

Klassen - Abstrakte Klassen

class Shape {
  public:
  virtual void draw() = 0;
};

Klassen - Templates

Ähnlich wie generics in Java.

template<typename T>
class Elem {
  public:
   T x;
  Elem(T x) { this->x = x; }
};

template<typename T>
void replace(Elem<T> e, T x) {
  e.x = x;
}
int main() {
  Elem<int> e = Elem<int>(1);
  replace<int>(e, 2);
}

Auch Funktion können Template Parameter haben

Klassen - Templates (2)

template<typename A, typename B>
class Pair {
 private:
  A a;
  B b;
 public:
  Pair(A a2, B b2) : a(a2), b(b2) {}
  A fst() { return a;}
  B snd() { return b;}
};

int main() {
  Pair<int,float> p = Pair<int,float>(1,2.0);
  Pair<bool,Pair<int,float> > p2 = Pair<bool,Pair<int,float> >(true,p);
}

Klassen - Sonstiges

Statische Member

class Point {
  ...
  static int counter;
  static int getCounter() {
    return counter;
  }
}
int Point::counter = 0;
  cout << Point::counter;
  cout << Point::getCounter();
#include <iostream>
using namespace std;

class Point {
  float x, y;
  static int counter;
 public:
  Point(float x2, float y2) {
    x = x2;  y = y2; counter++;
  }
  static int getCounter() {
    return counter;
  }
};

int Point::counter = 0;

int main() {
  Point p = Point(1.0,2.0);
  Point p2 = Point(1.0,2.0);
  cout << "Anzahl von Points:" << Point::getCounter() << endl;
}

struct versus class

Konstante Member Deklaration const

float Point::getX() const { return x; }

Initialisierung von Member Objekten

class Some {
  public:
  Other x;
  Other z;
  Some (Other y) { x = y; z = y; }
};
class Some {
  public:
  const Other x;
  Other& z;
  Some (Other y) : x(y), z(y) {}
  // Some (Other y) { x = y; z = y; }
  // geht hier nicht
};

Wie Beispiel zeigt, Initialisierung als Teil des Konstruktoraufrufs notwendig falls const oder reference Member.

Aufruf von Superklassen Konstruktor

class Super {
  ...
  Super(int) {}
};
class Sub : public Super {
  ...
  Sub(int x) : Super(x) {}
};
Super
Sub
SuperSuper
SubSub
#include<iostream>
using namespace std;

class Super {
 public:
  Super() { cout << "Super" << endl; }
  Super(int) { cout << "SuperSuper" << endl; }
};

class Sub : public Super {
 public:
  Sub() { cout << "Sub" << endl; }
  Sub(int x) : Super(x) { cout << "SubSub" << endl; }
};

int main() {
  Sub x;

  Sub y = Sub(1);

}

explicit Konstruktor

Vermeidet implizite Typkonvertierung bei Konstruktoraufruf

class C {
  public:
  C(int x) : y(x) {}
  // explicit C(int x) : y(x) {}
  int y;
};
void f(C c) {}
int main() {
  f(1);
}

Virtuelle Destruktoren

#include <iostream>
using namespace std;

class Base {
  public:
  Base() {cout<<"BaseKon\n";}
  ~Base() {cout<<"BaseDe\n";}
  // virtual ~Base() {cout<<"BaseDe\n";}
};

class Derived : public Base {
  public:
  Derived() {cout<<"DerivedKon\n";}
  ~Derived() {cout<<"DerivedDe\n";}
};

int main()
{
    Base* base = new Derived();
    delete base;
}
BaseKon
DerivedKon
BaseDe
BaseKon
DerivedKon
DerivedDe
BaseDe

Referenzparameter

void increment(int& x) {
 x++;
}

Pointer versus Referenz

void increment2(int* x) {
 *x++;
}

void increment(int& x) {
 x++;
}

int main() {
  int x;
  increment(x);
  increment2(&x);
}

Implizite Seiteneffekte durch Pointer und Referenzen

Kodierrichtline Referenzen und Pointer

  increment2(&x);  // Adressoperator
void print(const bigData& x) {
...
}

Überladung von Operatoren

 +, *, =, ++, <<, [], ...
class MyInt {

  friend ostream& operator<< (ostream &out, MyInt &i);

 public:
  MyInt(int x = 0) {
    p = new int(x);
  }
  ~MyInt() { delete p; }

  void operator++() {
    (*p)++;
  }

 private:
  int* p;
};

ostream& operator<< (ostream &out, MyInt &i) {
  out << *i.p;
  return out;
}

}

Operator als friend weil Zugriff auf private Elemente.

Verwendung:

void f2(void) {
  MyInt i1 = MyInt(1);
  ++i1;
  cout << i1;
}

Beachte:

Operator ++ hier als Prefixoperator (Details + Erklärung siehe unten)

Frage: Wieso Referenzparameter für MyInt in ostream& operator<< (ostream &out, MyInt &i)

Echt trickreich. "Recall": Kopieren mit Pointern.

Angenommen wir verwenden

ostream& operator<< (ostream &out, MyInt i) { // Keine Referenze auf i !!!!
  out << *i.p;
  return out;
}

Destruktor wird auf i aufgerufen was zu delete p führt.

Durch den call-by value Aufruf cout << i2, teilen sich i und i2 den gleichen Zeiger!

Deshalb wird nach Rücksprung aus f2, noch einmal delete p ausgeführt, wobei p auf die gleiche Adresse verweist!

struct position {
  int x;
  float y;
};
position& operator++(position& p) { // Prefix
  p.x++;
  return p;
}
position& operator++(position& p, int dummy) { // Postfix
// trick: p ++ dummy
  p.x++;
  return p;
}

Typkonvertierungen ("cast")

Notation

 lhs = cast<type>(rhs)

Casts vermeiden soweit es geht

Weitere Infos: http://www.cplusplus.com/doc/tutorial/typecasting/

Schachtelung von Blöcken

 for(int i=0; i < 10; i++) {
    { bool i=true;
       { char i = i ? 'a' : 'b';
         // character i sichtbar
       }
    // bool i sichtbar
    }
   // int i sichtbar
 }

Schachtelung von Blöcken - Anwendungsfalls Locking

Mutex m;
m.lock();
// do something exclusively
m.unlock();
{
 Lock l;
 // do something exclusively

} // Aufruf Destruktor ==> unlock Objekt l

Schachtelung von Deklarationen

class C {
  public:
  // ...
  private:
  enum E { Tag1, Tag2 };
  class D {
  // ...
  };
};

Ausnahmen ("exceptions")

Ausnahmebehandlung im Falle von z.B. Null Pointer, Speicher ist leer, File kann nicht geöffnet werden, Division durch 0 etc.

void f() { ... throw DivZeroError(); ... }
int main {
  try {
  f();
  }
  catch (DivZeroError) {
   //handle error
  }

Ausnahmen Beispiel

#include <iostream>
using namespace std;

void compute(int x, int y, int z) {


  if(y == z) throw 0;
  if(y > z) throw 'a';
  if(y < z) throw 1.0;
  float f = x / (y-z);
  cout << "f= " << f << "\n";

}

int main() {
  try{ compute(5,3,3); }
  catch(int i) { cout << "Exception " << i << "\n"; }
  catch(char) { cout << "Exception char \n"; }
}

STL - Standard Template Library

STL Beispiel

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
  vector<string> v;
  cout << "Bitte zu sortierenden Text eingaben, \n"
       << "Stop mit \"stop\" \n";
  for(;;) {
    string s;
    getline(cin,s);
    if(s == "stop")
      break;
    v.push_back(s);
  }
  sort(v.begin(), v.end());
  cout << "Text nach Sortierung: \n";

  for(int i = 0; i<v.size(); i++)
    cout << v[i] << "\n";
}

STL Beispiel - containers

#include <vector>
...
  vector<string> v;
...
  v.push_back(s);

STL Beispiel - algorithms

#include <algorithm>
...
  sort(v.begin(), v.end());
...

STL Beispiel - iterators

  for(int i = 0; i<v.size(); i++)
    cout << v[i] << "\n";
  vector<string>::iterator  iter;
  for(iter = v.begin(); iter != v.end(); iter++)
    cout << *iter << "\n";