Die Programmiersprache C++ - Kurz und knapp

Martin Sulzmann

Übersicht

Sichtbarkeitsbereiche und Referenzübergabe

#include <stdio.h>

// Sichtbarkeitsbereiche ("scope")
// In C gibt es nur 2 Sichtbarkeitsbereiche.
// (Im neusten C Standard gilt die gleiche Regel wie in C++)
// In C++ sind beliebige Schachtelungen moeglich.
// Jeder Codeblock { ... } entspricht einem Sichtbarkeitsbereich.

void scopeInC() {
  float i = 1.0;
  int x[5] = {1,2,3,4,5};
  int i_2;
  int j;
  int x_3;

  for(i_2 = 0; i_2 < 5; i_2++) {
    j = x[i_2];
    x_3 = j;
    printf("%d",x_3);
  }

}

// B = Begin. E = End.
// Markiert Codeblock.
void scopeInCPP() { // B1
  float i = 1.0;
  int x[5] = {1,2,3,4,5};

  for(int i=0; i < 5; i++) { // B2
    int j = x[i];
    { // B3
      int x = j;
      printf("%d", x);
    } // E3
  } // E2

} // E1

// Referenzübergabe.
// Syntaktischer Zucker in C++.


struct Pair {
  int left;
  int right;
};

// Variante 1.
void swapPair_Ref(struct Pair& p) {
  int tmp = p.left;
  p.left = p.right;
  p.right = tmp;
}

// Variante 2.
void swapPair_Pointer(struct Pair* p) {
  int tmp = p->left;
  p->left = p->right;
  p->right = tmp;
}


void testSwap() {
  struct Pair p = {1,2};
  struct Pair q = {1,2};


  printf("\nPair: p = (%d,%d)", p.left, p.right);
  swapPair_Ref(p);
  printf("\nPair swapped: p = (%d,%d)", p.left, p.right);

  printf("\nPair: q = (%d,%d)", q.left, q.right);
  swapPair_Pointer(&q);
  printf("\nPair swapped: q = (%d,%d)", q.left, q.right);

}


int main() {


  scopeInC();
  scopeInCPP();
  testSwap();
}

IO streams und Strings

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


void IO_in_CPP() {

  float f = 1.0;
  cout << f;

  int i = 5;
  cout << i;

  // "<<" ist ein binaerer Operator. Links ist das Ausgabeziel und rechts der Ausgabewert.
  // "<<" ist ueberladen, auf der rechten Seiten koennen int, float, .... Werte stehen.


  // Sequenz von Ausgaben.
  cout << f << "  " << i;


  // "<<" ist links assoziativ.
  ((cout << f) << "  ") << i;
  // Beobachtung:
   // cout << f, rechter Operand ist vom Typ float.
  // (...) << "  ",  rechter Operand ist vom Typ string.
  // (...) << i, rechter Operand ist vom Typ int.

  cout << i << "Hallo" << f;

  ((cout << i) << f) << i << f << "Hallo";
}

void vergleich_zu_C() {

  float f = 1.0;
  printf("%f",f); // cout << f;

  int i = 5;
  printf("%d",i); // cout << i;

  // C:   Dynamische Pruefung der (Typ)kompatibilitaet von Argument und Platzhalter
  // C++: Statische (zur Compilezeit) Pruefung!

}

void strings_cpp() {
  string s = "\nHallo";

  cout << s;

  string s2;

  s2 = s + s; // String Konkatenation

  cout << s2;

  string s3 = "Hallo";

  cout << "\n" << s.length();
  cout << "\n" << s2.length();
  cout << "\n" << s3.length();


}

int main() {

  IO_in_CPP();
  strings_cpp();
}

Überladene Operatoren/Funktionen

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


// Eigene Instanz fuer "<<" Operator.
// Man spricht hier von "Überladung".
struct Pair {
  int left;
  int right;
};


ostream& operator<< (ostream &out, struct Pair &p) {
  out << "(" << p.left << "," << p.right << ")";

  return out;
}


// Transformation des Compilers.
// Generiere eindeutigen Namen, normalerweise Hash-Index
// basierend auf Argumenttypen.
// Wir verwenden einfach "_Pair".

ostream& outStream_Pair (ostream &out, struct Pair &p) {
  out << "(" << p.left << "," << p.right << ")";

  return out;
}


void IO_in_CPP() {

  struct Pair p = {1,2};
  cout << p;   // Verwende obige "out stream" Instanz.
  // Compiler Transformation.
  // Verwende eindeutigen Funktionsaufruf.
  outStream_Pair(cout,p);

}



// Beachte:
//  - swapPair ist eine überladene Funktion.
//  - Mehrfache Definition, aber für verschiedene Argumenttypen.

// Variante 1.
void swapPair(struct Pair& p) {
  int tmp = p.left;
  p.left = p.right;
  p.right = tmp;
}

// Intern passiert folgendes.

// Annahme, der Funktionsname swapPair_Ref ist eindeutig.
void swapPair_Ref(struct Pair& p) {
  int tmp = p.left;
  p.left = p.right;
  p.right = tmp;
}

// Weitere Transformation.
// Uebersetze call-by-reference in call-by-value + pointer

void swapPair_Ref_Transformed(struct Pair* p) {
  int tmp = p->left;
  p->left = p->right;
  p->right = tmp;
}

// Variante 2.
void swapPair(struct Pair* p) {
  int tmp = p->left;
  p->left = p->right;
  p->right = tmp;
}

void swapPair_Pointer(struct Pair* p) {
  int tmp = p->left;
  p->left = p->right;
  p->right = tmp;
}

// Variante 3 (call-by-value)
/*

"'swapPair' is ambiguous"


void swapPair(struct Pair p) {
  int tmp = p.left;
  p.left = p.right;
  p.right = tmp;
}

*/


void testSwap() {
  struct Pair p = {1,2};
  struct Pair q = {1,2};

  cout << "\nPair: " << p;
  swapPair(p);
  // Compiler verwendet hier Variante 1 mit explizitem eindeutigem Namen.
  // 1ter Transformationsschritt.
  swapPair_Ref(p);
  // 2ter Transformationsschritt.
  swapPair_Ref_Transformed(&p);
  cout << "\nPair swapped: " << p;

  cout << "\nPair: " << q;
  swapPair(&q);
  swapPair_Pointer(&q);
  cout << "\nPair swapped: " << q;

}


int main() {

    IO_in_CPP();
    testSwap();

}

Klassen

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

///////////////////////
// Klassen

class Point2D {
  float x, y; // private
public:
  Point2D(float x2=0, float y2=0) {
  x = x2;  y = y2;
  }
  void scale(float f) {
  x = f * x; y = f * y;
 }
  float getX() { return x; }
  float getY() { return y; }
  void setXY(float x2, float y2) {
    x = x2; y = y2;
  }

  string show() {

    // + Konkatenation zweier Strings.
    // Konvertierung von float nach String via to_string.
    // C++ unterstuetzt Ueberladung (overloading).
    // D.h wir koennen z.B. to_string auf Werte vom Typ float,
    // als auch auf Werte vom Typ int anwenden.
    // Konkret.
    // string str = to_string(1) + to_string(1.0);
    return ("\nx = " + to_string(x) + "\ny = " + to_string(y));

  }

}; // Point2D


void testPoint2D() {
  Point2D p1; // Wir haben den Default Auruf
              // p1 = Point2D();
              // Aber wir haben Default Werte definiert.
              // Deshalb wir obige Anweisung interpretiert als
              // p1 = Point2D(0,0);
  Point2D p2 = Point2D(1,2);
  Point2D p3(3,4); // Verschieden Schreibweise fuer einen Konstruktoraufruf.
  Point2D p4 = Point2D(1); // Point2D(1,0)

  // Beachte. In Java werden Objekte immer mit Hilfe von "new" angelegt!
  // In Java liegen alle Objekte auf dem Heap.
  // In C++ koennen Objekte auf dem Stack ("kein new") oder dem Heap ("new") liegen.
  // Im obigen Beispiel, liegen die Objekte p1, p2, p3 und p4 auf dem Stack.

  cout << p1.show();
  cout << p2.show();
  p3.scale(2);
  cout << p3.show();
}



// Erweiterung der Klassenhierarchie ("extends").
class Point3D : public Point2D {
  float z;
public:
  Point3D(float x2=0, float y2=0, float z2=0) : Point2D(x2,y2) {
    z = z2;
  }

  void scale(float f) {
    this->setXY(f * this->getX(), f * this->getY());
    z = f * z;
 }

  string show() {
    return ("\nx = " + to_string(getX())
      + "\ny = " + to_string(getY())
      + "\nz = " + to_string(z));

  }

};

void testPoint3D() {
  Point3D p = Point3D(1,2,3);

  cout << p.show();

  p.scale(2);

  cout << p.show();

  // Point3D <= Point2D,
  // bedeutet jedes Objekt der Klasse Point3D kann auch an einer Stelle verwendet werden,
  // an welcher ein Objekt der Klasse Point2D erwartet wird.
  Point2D q = p;

  cout << q.show();

}


int main() {
  testPoint2D();
  testPoint3D();
}

Virtuelle (dynamisch) versus nicht-virtuelle (statisch) Methoden(auswahl) und static

Virtuelle Methoden:

Nicht-virtuelle Methoden:

static:

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

class Vehicle {
public:
  static int number;
  static int getNo() { return number; }
  virtual int maxSpeed() { return 0; }
  string info() { return "I'm a vehicle"; }
};

int Vehicle::number = 0;

// car <= Vehicle
class car : public Vehicle {
public:
  car() { Vehicle::number++; }
  int maxSpeed() { return 100; }
  string info() { return "I'm a car"; }
};


// Bike <= Vehicle
class Bike : public Vehicle {
public:
  Bike() { number++; }
  int maxSpeed() { return 30; }
  string info() { return "I'm a bicycle"; }
};


void test1(Vehicle v) {
  cout << "\nInfo: " << v.info();
  cout << "\nSpeed: " << v.maxSpeed();
}

void test2(Vehicle* v) {
  cout << "\nInfo: " << v->info();
  cout << "\nSpeed: " << v->maxSpeed();
}

void test3(car v) {
  cout << "\nInfo: " << v.info();
  cout << "\nSpeed: " << v.maxSpeed();
}


int main() {
  Vehicle::number = 0;
  Bike b = Bike();
  car c = car();

     test1(c); // car <= Vehicle
               // das car Objekt c wird konvertiert
               // in ein Vehicle Objekt

     test2(&c); // *car <= *Vehicle
                // Die Referenz auf ein car Objekt
                // wird uebergeben.

     test3(c);

     test1(b);
     test2(&b);

  cout << "\n There are "
       // << Vehicle::number
       << Vehicle::getNo()
       << " vehicles";
}

Templates

“Look-and-feel” ähnlich wie Generics in Java.

Elementare Unterschiede (z.B. Template Expansion zur Compile-Zeit, mehr dazu später).

Templatifizierte Klasse und Funktion

#include <iostream>

using namespace std;

// class template
template<typename T>
class Elem {
  T x;
public:
  Elem() {}
  Elem(T y) { x = y; }
  void replace(T y) { x = y; }
  T get() { return x; }
};


// function template
template<typename T>
void mySwap(T& x, T& y) {
  T tmp;
  tmp=x; x=y; y=tmp;
}


// template instantiation
int main() {
  Elem<int> i = Elem<int>(1);
  Elem<int> i2 = Elem<int>(2);
  Elem<char> c = Elem<char>('a');

  i.replace(3);

  mySwap<Elem<int>>(i,i2);
  // Take note.
  // 1. Because of T tmp in mySwap,
  //    we need to provide the parameterless constructor in the Elem class
  // 2. Pior to "c++11" we need to include a space in between >>.

  // in some cases, template instances can be inferred by the compiler
  mySwap(i2, i);


  int j, j2;
  j = 1; j2 = 3;

  mySwap<int>(j, j2);
  mySwap(j2, j);

  return 1;
}


// Compiler generates


class Elem_int {
  int x;
public:
  Elem_int() {}
  Elem_int(int y) { x = y; }
  void replace(int y) { x = y; }
  int get() { return x; }
};

class Elem_char {
  char x;
public:
  Elem_char() {}
  Elem_char(int y) { x = y; }
  void replace(char y) { x = y; }
  char get() { return x; }
};

void mySwap_Elem_int(Elem_int& x, Elem_int& y) {
  Elem_int tmp;
  tmp=x; x=y; y=tmp;
}

void mySwap_int(int& x, int& y) {
  int tmp;
  tmp=x; x=y; y=tmp;
}



int main2() {
  Elem_int i = Elem_int(1);
  Elem_int i2 = Elem_int(2);
  Elem_char c = Elem_char('a');

  i.replace(3);

  mySwap_Elem_int(i,i2);
  // Because of T tmp in mySwap,
  // we need to provide the parameterless constructor in the Elem class

  // in some cases, template instances can be inferred by the compiler
  mySwap_Elem_int(i2, i);


  int j, j2;
  j = 1; j2 = 3;

  mySwap_int(j, j2);
  mySwap_int(j2, j);

  return 1;
}