Inhaltsverzeichnis[Anzeigen]

Techniken in C und C++

Motivation

Dieses Seite enthält viele Technikvergleiche von C und C++. Die Idee ist es, typischen C-Programmiereraufgaben ihre C++-Lösungen gegenüber zu stellen. Dabei verwende ich C-Code, der nicht state of the art ist. C-Code, den ich bei meiner täglichen Arbeit sehe. Diesem stelle ich state of the art C++-Code gegenüber. Dieser Technikvergleich ist natürlich nicht objektiv, denn meine persönliche Sicht trifft das Zitat von Bjarne Stroustrup sehr gut:C++ is "a better C" in the sense that it supports the styles of programming done using C with better type checking and more notational support ...(http://www.stroustrup.com/bs_faq.html#difference).
Natürlich lassen sich auch fast alle C-Lösungen in C++ einsetzen. Die Vergleiche sind bewusst schematisch aufgebaut um die Unterschiede schnell auf den Punkt zu bringen.

Makros

  • C++ ist eine 3-Stufen Sprache
  • jede Stufe verwendet die Ergebnisse der vorherigen Stufen
  1. Präprozessor
    • einfache Textsubstitution ohne Semantik
  2. Templates
    • Instanziierung der Templates zur Compilezeit ist eine funktionale, turing-complete Subsprache in C++
    • das Ergebnis der Instanziierung ist C++-Sourcecode
  3. konventionelles C++

Konstanten

Zweck

  • werden zur Compilezeit ausgewertet
  • können als Größe eines Arrays verwendet werden
  • werden im ROM gespeichert

C

#define MAX 14

C++

const int MAX= 14;

Vergleich

SpracheSemantikSyntax
C reine Textersetzung keine Syntax
C++ Sprachkonstrukt normale C++-Syntax

Anmerkung

  • durch das Schlüsselwortconstexprlassen sich Konstanten erklären, die zur Compilezeit evaluiert werden
constexpr int MAX= 14;

Funktionen

Zweck

  • werden vor der Laufzeit evaluiert
  • ersetzen ihren Funktionsaufruf mit ihrem Funktionskörper
  • bieten größeres Optimierungspotential für den Compiler

C

#include <stdio.h>
 
#define max(i, j) (((i) > (j)) ? (i) : (j))
 
int main(void){
 
  int i= max(10,11);
  printf("%d\n",i);                   
  printf("%d\n",max(100,-10));        
  printf("%d\n",max("nonsense",3));   
  printf("%f\n",max("3",3));          
 
  return 0;
 
}
  • ergibt
11
100
4195968
0.000000

C++

#include <iostream>
 
template<typename T>
T max (T i, T j){
  return ((i > j) ? i : j);
}
 
int main(){
 
  std::cout << max(10,11) << std::endl;            // 11
  std::cout << max(100,-10) << std::endl;          // 10
  std::cout << max(10,1.5) << std::endl;           // ERROR
  // std::cout << max("nonsense",3) << std::endl;  // ERROR
  // std::cout << max("3",3) << std::endl;         // ERROR
 
}
  • das Übersetzen von max(10,1.5) führt zum Fehler, da 10 vom Typ int und 1.5 vom Typ double ist:
C++/makroCpp.cpp: In function 'int main()':
C++/makroCpp.cpp:13:26: error: no matching function for call to 'max(int, double)'
C++/makroCpp.cpp:13:26: note: candidate is:
C++/makroCpp.cpp:4:3: note: template<class T> T max(T, T)
C++/makroCpp.cpp:4:3: note:   template argument deduction/substitution failed:
C++/makroCpp.cpp:13:26: note:   deduced conflicting types for parameter 'T' ('int' and 'double')

Vergleich

SpracheSemantikSyntax
C reine Textersetzung keine Syntax
C++ Instanziierung zur Compilezeit Template Syntax

Anmerkung

  • neben Funktions-Templates bieten sich inline-Funktionen als Ersatz für Makros an
  • durch das Schlüsselconstexprlassen sich Funktionen als Compilezeit-Funktionen deklarieren
constexpr int max (int i,int j){
  return ((i > j) ? i : j);
}

Parametrisierte Container

Zweck

  • sind sehr wichtige Datenstrukturen
  • bieten eine deutliche Vereinfachung im Umgang mit sequentiellen und assoziativen Datenstrukturen an
  • bieten ein ähnliches Interface an
  • erlauben es dem Programmierer, sich auf die Verarbeitung der Daten zu konzentrieren

C

#ifndef KVEC_H
#define KVEC_H
 
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
#define VEC_t(type) struct { uint32_t n, m; type *a; }
#define VEC_init(v) ((v).n = (v).m = 0, (v).a = 0)
#define VEC_destroy(v) free((v).a)
#define VEC_A(v, i) ((v).a[(i)])
#define VEC_size(v) ((v).n)
#define VEC_max(v) ((v).m)
 
#define VEC_resize(type, v, s)  ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m))
 
#define VEC_push(type, v, x) do {                                    \
        if ((v).n == (v).m) {                                        \
            (v).m = (v).m? (v).m<<1 : 2;                             \
            (v).a = (type*)realloc((v).a, sizeof(type) * (v).m);     \
        }                                                            \
        (v).a[(v).n++] = (x);                                        \
    } while (0)
 
#endif
 
typedef VEC_t(int) MyIntVec;
 
int main(void){
 
  MyIntVec myIntVec;
  VEC_init(myIntVec);
  VEC_resize(int,myIntVec,10);
  printf("size: %d\n",VEC_size(myIntVec));              
  printf("max: %d\n",VEC_max(myIntVec));               
  VEC_push(int,myIntVec,0);
  VEC_push(int,myIntVec,1);
  VEC_push(int,myIntVec,2);
  VEC_push(int,myIntVec,3);
  VEC_push(int,myIntVec,4);
  printf("size: %d\n",VEC_size(myIntVec));              
  printf("max: %d\n",VEC_max(myIntVec));                
  printf("myIntVec[3]: %d\n",VEC_A(myIntVec,3));        
  VEC_destroy(myIntVec);
 
}
  • ergibt
size: 0
max: 0
size: 6
max: 10
myIntVec[3]: 3

C++

#include <iostream>
#include <vector>
 
int main(){
 
  std::vector<int> myIntVec;
  std::cout << "capacity: " << myIntVec.capacity() << std::endl;   
  std::cout << "size: " << myIntVec.size() << std::endl;           
  myIntVec={0,1,2,3,4,5};
  std::cout << "capacity: " << myIntVec.capacity() << std::endl;   
  std::cout << "size: " << myIntVec.size() << std::endl;           
  std::cout << "myIntVec[3]: " << myIntVec[3] << std::endl;        
 
}
  • ergibt
capacity: 0
size: 0
capacity: 11
size= 6
myIntVec[3]: 3

Vergleich

SpracheSemantikSyntaxErweiterungFehleranfälligkeitSpeichermanagement
C reine Textersetzung keine Syntax Entwickler hoch Entwickler
C++ Sprachkonstrukt normale C++-Syntax Umfang des C++-Standards keine automatisch

Anmerkung

  • die C++ Variante bietet ein deutlich mächtigeres Interface an:[1]
  • die C++-Variante verwaltet den dynamischen Speicher automatisch
  • C++ besitzt viele sequentielle und assoziative Container:[2]
  • die Anforderungen an Container steigen
    • Thread-Safe
    • verschiedene Speicherallokationsstrategien
    • Lock-Free

Konstanten

Zeiger

Nullzeiger
Zweck
  • zeigen an, dass auf nichts verwiesen wird
  • kennzeichnen einer Leerstelle
C
#include <stdio.h>
 
int main(void){
 
  int* pi= 0;
  int i= 0;
 
  int* pj= NULL;
  int j= NULL;
 
}
C++
int main(){
 
  int* pi= nullptr;
  // int i= nullptr;      // ERROR
  bool b= nullptr;
 
}
Vergleich
Spracheimplizite Konvertierung nach intimplizite Konvertierung nach 0 oder NullzeigerKonvertierung nach bool
C ja ja ja
C++ nein nein ja
Anmerkung
  • C++: das implizite konvertieren eins Nullzeigers (nullptr) in eine natürliche Zahl ist eine häufige Ursache von Fehlern ⇒nullptrlässt sich nicht nachintkonvertieren
  • C: die Nullzeiger Konstante (NULL) lässt sich nachchar*und nachintkonvertieren ⇒ der Aufruf der FunktionoverloadTest(NULL)ist nicht eindeutig
std::string overloadTest(char*){
  return "char*";
}
std::string overloadTest(int){
  return "int";
}
 
overloadTest(NULL); // ERROR
  • das Beispiel geht davon aus, dasNULLals0Loder0LLdefiniert ist
  • wirdNULLals0definiert, ist der AufrufoverloadTest(NULL)ist eindeutig

Typen

Bool

Zweck

  • Datentyp für die Wahrheitswertetrueoderfalse

C

typedef int bool;
 
#define true 1
#define false 0
 
enum { false, true };                  
enum bool { false, true };

C++

bool a= true;
bool b= false

Vergleich

Sprachebuilt-in Datentypimplizite Konvertierung nach 0/1
C nein ja
C++ ja ja

Anmerkung

  • der Vektor ist für den Datentypboolauf Speicheranforderung optimiert:std::vector<bool>

String

  • ist eine Sequenz von Zeichen, beendet durch ein Null-Zeichen

Zweck

  • elementarer Datentyp für Worte und Sätze

C

#include <stdio.h>
#include <string.h>
 
int main( void ){
 
  char text[10];
 
  strcpy(text, "The Text is to long for text.");   // undefined behavior because text is to big
  printf("strlen(text): %u\n", strlen(text));      // undefined behavior because text has no termination character '\0'
  printf("%s\n", text);
 
  text[sizeof(text)-1] = '\0';
  printf("strlen(text): %u\n", strlen(text));
 
  return 0;
}
  • ergibt
strlen(text): 29
The Text is to long for text.
strlen(text): 9
Speicherzugriffsfehler
  • obwohl das Programm undefiniertes Verhalten besitzt, lässt es sich ohne Warnung übersetzen

C++

#include <iostream>
#include <string>
 
int main(){
 
  std::string text{"The Text is not to long for text."};   // automatic storage allocation and string termination
 
  std::cout << "text.size(): " << text.size() << std::endl;
  std::cout << text << std::endl;
 
  text +=" And can still grow!";
 
  std::cout << "text.size(): " << text.size() << std::endl;
  std::cout << text << std::endl;
 
}
  • ergibt
text.size(): 33
The Text is not to long for text.
text.size(): 53
The Text is not to long for text. And can still grow!

Vergleich

SpracheSpeichermanagementDynamische GrößenanpassungString Termination ZeichenFehleranfälligkeit
C explizite Allokation und Deallokation explizit durch den Programmierer explizit durch den Programmierer hoch
C++ automatisch Allokation und Deallokation automatisches reallokieren automatisch niedrig

Anmerkung

  • der Umgang mit Strings ist neben dem Umgang mit Speicher die häufigste Fehlerquelle in C-Code
C
  • die String Buchhaltung muss vom Programmierer explizit vollzogen werden
    • Allokation
    • Deallokation
    • String Terminations Zeichen
    • Länge des Strings bestimmen
C++
  • das String Buchhaltung wird vom Compiler automatisch vollzogen
  • C++-Strings bieten ein reiches Interface an
    • die String-Methoden[3]
    • die Algorithmen der Standard Template Library[4]
    • die reguläre Ausdrücke Bibliothek[5]

Initialisierung

Zweck

  • Variablen sollen vor ihrer Verwendung initialisiert werden
  • konstante Variablen müssen vor ihrer Verwendung initialisiert werden

C

#include <stdio.h>
#include <stdint.h>
 
int main(void){
 
  double dou= 3.14159;
  int a= dou;               // NO ERROR or WARNING
  int8_t smallInt= 2011;    // WARNING
 
  printf("\ndou: %f\n",dou);
  printf("a: %f\n",a);
  printf("smallInt: %i\n",smallInt);
 
  if ( a == 3 ) printf("\na == 3\n");
 
  return 0;
}
  • ergibt
dou: 3.141590
a: 3.141590
smallInt: -37

a == 3

C++

  • in C++ lässt sich alles mit geschweiften ( {} ) Klammern initialisieren
  • bei der Initialisierung mit geschweiften Klammern ( {} ) findet kein Verengung des Datentyps statt
#include <iostream>
 
int main(){
 
  double dou= 3.14159;
  // int c= {dou};         // ERROR
  // int d{dou};           // ERROR
 
  // int8_t f= {2011};     // ERROR
  int8_t g= {14};
 
  std::cout << "g: " << static_cast<int>(g) << std::endl;
 
}
  • ergibt
g: 14

Vergleich

SpracheInitialisierungVerengung
C mit runden ( () ) Klammern ja
C++ mit geschweiften ( {} ) Klammern nein

Anmerkung

  • vergessene Initialisierung von Variablen führt zu undefiniertem Programmverhalten
  • eine Variable soll bei ihrer Definition initialisiert werden

Konvertierung

Zweck

  • Zuweisungen an einen anderen Datentypen
  • Konvertierung zu und vonvoid*
  • dem Compiler den Datentyp vorschreiben

C-Cast

#include <iostream>
 
int main(){
  for (int x= 0; x < 128; ++x) {
    std::cout << x <<": "<< (char)x << std::endl;
  } 
}

C++-Casts

  • drücken ihr Anliegen direkt aus
const_cast
  • erlaubt die Qualifierconstodervolatilevom Argument zu entfernen oder hinzuzufügen
double myDouble=5.5;
const double* myDoublePointC= &myDouble;
double* myDoubleP= const_cast<double*>(myDoublePointC);
static_cast
  • wird zur Compilezeit ausgeführt
  • erlaubt zwischen verwandten Typen zu konvertieren
    • Konvertierungen zwischen Zeigertypen in derselben Klassenhierarchie
    • Aufzählungen in eine Ganzzahl
    • Ganzzahl in eine Fließkommazahl
double myDouble= 5.5;
int i= static_cast<int>(myDouble);
reinterpret_cast
  • erlaubt
    • einen Zeiger in einen beliebigen anderen Zeiger
    • einen beliebigen Zeiger in einen beliebigen integralen Typ zu konvertieren und anders herum
double* myDouble= new double();
void* myVoid= reinterpret_cast<void*>(myDouble);
double* myDouble1= reinterpret_cast<double*>(myVoid);
dynamic_cast
  • prüft zur Laufzeit, ob die Konvertierung möglich ist
  • konvertiert einen Zeiger(Referenz) eines Klassentyps in ein anderen Zeiger(Referenz) in der gleichen Ableitungskette
  • kann nur auf polymorphe Typen angewandt werden
  • ist Bestandteil derrun-time type information(RTTI)
  • ist die Konvertierung nicht möglich, gibt diese einen Nullzeiger (nullptr) bei Zeigern, eine Ausnahme bei Referenzen zurück
class A{
public:
  virtual ~A();
};
 
class B : public A{
public:
  void onlyB(){};
};
 
void funWithRef(A& myA){
  try{
    B& myB = dynamic_cast<B&>(myA);
    myB.onlyB();
  }
  catch (const std::bad_cast& e){
    std::cerr << e.what() << std::endl;
    std::cerr << "No B" << std::endl;
  }
}
 
void funWithPointer(A* myA){
  B* myB = dynamic_cast<B*>(myA);
  if (myB != nullptr) myB->onlyB();
  else std::cerr << "No B" << std::endl;
}

Vergleich

Spracheeindeutige Konvertierungexplizite Syntax
C nein nein
C++ ja ja

Anmerkung

  • C-Konvertierungen
    • sind eine Kombination von verschiedenen C++-Casts→ const_cast → static_cast → reinterpret_cast
    • sind schwierig im Code zu identifizieren

Anweisungen

for-Anweisung

Zweck

  • wird verwendet, um die Elemente eines Containers zu bearbeiten

C

int i; 
for(i=0; i<10; ++i){}
printf(i);

C++

for (int i= 0; i<10;++i){}
std::cout << i << std::endl; // ERROR

Vergleich

SpracheDefinition der LaufzeitvariableSichtbarkeit der Laufzeitvariable
C vor der for-Anweisung im umgebenden Bereich der for-Anweisung
C++ in der for-Anweisung im Bereich der for-Anweisung

Anmerkung

  • meist lässt sich eine explizite for-Anweisung durch einen Algorithmus der Standard Template Library ersetzen
std::vector<int> myVev={1,2,3,4,5,6,7,8,9};
std::copy(myVec.begin(), myVec.end(), std::ostream_iterator<int>(std::cout, " "));
  • eine Range-basierte For-Schleife ist deutlich kompakter als eine for-Anweisung
std::vector<int> myVev={1,2,3,4,5,6,7,8,9};
for ( int i: myVec ) std::cout << i << " ";

case-Anweisung

Zweck

  • durch case-Anweisungen lässt sich der Programmfluss explizit steuern
  • sind ein einfaches Mittel Zustandsautomaten zu implementieren

C

#include <stdio.h>
 
typedef enum Tag_Dispatch{
  IMPL_A,
  IMPL_B,
  IMPL_C
} Impl_Tag;
 
const char* getNameA(){
  return "Implementation A";
}
 
const char* getNameB(){
  return "Implementation B";
}
 
void displayName(Impl_Tag tag){
    switch(tag){
        case IMPL_A:
            printf("%s\n",getNameA());
            break;
        case IMPL_B:
            printf("%s\n",getNameB());
            break;
    }
}
 
void displayNameWithDefault(Impl_Tag tag){
    switch(tag){
        case IMPL_A:
            printf("%s\n",getNameA());
            break;
        case IMPL_B:
            printf("%s\n",getNameB());
            break;
        default:
            printf("No Implementation\n");
    }
}
 
int main(void){
 
  displayName(IMPL_A);
  displayName(IMPL_B);
  displayName(IMPL_C);
 
  printf("\n");
 
  displayNameWithDefault(IMPL_A);
  displayNameWithDefault(IMPL_B);
  displayNameWithDefault(IMPL_C);
 
  return 0;
}
  • ergibt
Implementation A
Implementation B

Implementation A
Implementation B
No Name

C++

#include <iostream>
#include <string>
 
class Interface{
public:
  virtual std::string getName() const = 0;
  virtual std::string getNameWithDefault() const{
    return "No Implementation";
  }
};
 
class ImplementationA : public Interface{
  std::string getName() const {
    return "Implementation A";
  }
  std::string getNameWithDefault() const{
    return "Implementation A";
  }
};
 
class ImplementationB : public Interface{
  std::string getName() const {
    return "Implementation B";
  }
  std::string getNameWithDefault() const{
     return "Implementation B";
  }
};
 
class ImplementationC : public Interface{
  std::string getName() const {
     return "Implementation C";
   }
};
 
void showMyName(const Interface& a){
   std::cout << a.getName() << std::endl;
}
 
void showMyNameWithDefault(const Interface& a){
   std::cout << a.getNameWithDefault() << std::endl;
}
 
int main(){
 
  const Interface& impA= ImplementationA();
  const Interface& impB= ImplementationB();
 
  showMyName(impA);
  showMyName(impB);
 
  std::cout << std::endl;
 
  const Interface& impC= ImplementationC();
 
  showMyNameWithDefault(impA);
  showMyNameWithDefault(impB);
  showMyNameWithDefault(impC);
 
}
  • ergibt
Implementation A
Implementation B

Implementation A
Implementation B
No Name

Vergleich

SpracheProgrammlogikErweiterbarkeitProgrammfluss
C implementiert jede Funktion jede case-Anweisung muss überarbeitet werden bestimmt explizit der Programmierer
C++ definiert Klassenstruktur Klassenstruktur wird erweitert bestimmt implizit der Compiler

Anmerkung

  • sind eine Verallgemeinerungen der bedingten (if/else) Anweisungen
  • case-Anweisung gelten als schlechter Programmstil, da sie
    • sehr viel Pflegeaufwand benötigen
    • sehr fehleranfällig sind
    • die Kontrolllogik vervielfältigen

Funktionen

Overloading

Zweck

  • eine Funktion soll mit verschiedenen Typen aufgerufen werden

C

#include <stdio.h>
 
void print_Int(int i){
  printf("%d\n",i);
}
 
void print_Char(const char* c){
  printf("%s\n",c);
}
 
int main(void){
 
  print_Int(2011);
  print_Char("Hello world");
}

C++

  • in C++ können Funktionen mit dem selben Namen definiert werden, die sich nur in den Typen der Parameter unterscheiden
#include <iostream>
#include <string>
 
void print(int i){
  std::cout << i << std::endl;
}
 
void print(const std::string& str){
  std::cout << str << std::endl;
}
 
int main(){
  print(2011);
  print("Hello World");
}

Anmerkung

main-Funktion

Zweck

  • die main-Funktion ist der Startpunkt des Programms

C

int main( void ){
  . . . 
  return 0;
}

C++

int main(){
  . . .
}

Vergleich

SpracheArgumentreturn-Anweisung
C voidbei keinem Argument notwendig notwendig
C++ kein Argument notwendig vom Compiler wird automatischreturn 0;</code erzeugt

Anmerkung

  • C-Funktionen benötigen immer ein Argument; <code>void steht für die Abwesenheit eines Arguments
  • die Kommandozeilenargumente lassen sich in C und C++ durch die erweiterte main-Funktionmain(int argc, char* argv[])einlesen
    • argc enthält die Anzahl der Argumente
    • argv enhält ein Array von C-Strings; argv[0] ist der Name des Programms

Funktionszeiger, Funktoren und Lambda-Funktionen

Zweck

  • Algorithmen lassen sich über Zeiger auf Funktionen, Funktionsobjekte und Lambda-Funktionen parametrisieren
  • je mehr Einsicht der Compiler in den Code hat, desto besser kann er optimieren
  • Lambda-Funktionen besitzen des höchste Optimierungspotential, gefolgt von Funktionsobjekten und Funktionszeiger

C

  • Parametrisierung über einen Funktionszeiger
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>
 
void add3(int& i){
  i +=3;
}
 
int main(){
  std::cout << std::endl;
 
  std::vector<int> myVec1{1,2,3,4,5,6,7,8,9,10};
  std::cout << std::setw(20) << std::left << "myVec1: i->i+3:     ";
  std::for_each(myVec1.begin(),myVec1.end(),&add3);
  for (auto v: myVec1) std::cout << std::setw(6) << std::left << v;
 
  std::cout << "\n\n";
}
  • ergibt
myVec1: i->i+3:     4     5     6     7     8     9     10    11    12    13 

C++

Funktionsobjekte
  • werden durch das Überladen des Klammeroperators zum Funktionsobjekt
  • verhalten sich wie Funktionen mit Zustand
  • lassen sich über den Konstruktor parametrisieren
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>
 
class AddN{
public:
  AddN(int n):num(n){};
  void operator()(int& i){
    i +=num;
  }
private:
  int num;
};
 
int main(){
  std::cout << std::endl;
 
  std::vector<int> myVec2{1,2,3,4,5,6,7,8,9,10};
  AddN add4(4);
  std::for_each(myVec2.begin(),myVec2.end(),add4);
  std::cout << std::setw(20) << std::left << "myVec2: i->i+4:     ";
  for (auto v: myVec2) std::cout << std::setw(6) << std::left << v;
  std::cout << "\n";
 
  std::vector<int> myVec3{1,2,3,4,5,6,7,8,9,10};
  AddN addMinus5(-5);
  std::for_each(myVec3.begin(),myVec3.end(),addMinus5);
  std::cout << std::setw(20) << std::left << "myVec3: i->i-5:     ";
  for (auto v: myVec3) std::cout << std::setw(6) << std::left << v;
 
  std::cout << "\n\n";
}
  • ergibt
myVec2: i->i+4:     5     6     7     8     9     10    11    12    13    14    
myVec3: i->i-5:     -4    -3    -2    -1    0     1     2     3     4     5  
Lambda-Funktionen
  • erlauben den Code an Ort und Stelle zu definieren
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <vector>
 
int main(){
  std::cout << std::endl;
 
  std::vector<int> myVec4{1,2,3,4,5,6,7,8,9,10};
  std::cout << std::setw(20) << std::left << "myVec4: i->3*i+5:   ";
  std::for_each(myVec4.begin(),myVec4.end(),[](int& i){i=3*i+5;});
  for (auto v: myVec4) std::cout << std::setw(6) << std::left << v;
  std::cout << "\n";
 
  std::vector<int> myVec5{1,2,3,4,5,6,7,8,9,10};
  std::cout << std::setw(20) << std::left << "myVec5: i->i*i   ";
  std::for_each(myVec5.begin(),myVec5.end(),[](int& i){i=i*i;});
  for (auto v: myVec5) std::cout << std::setw(6) << std::left << v;
  std::cout << "\n";
 
  std::vector<double> myVec6{1,2,3,4,5,6,7,8,9,10};
  std::for_each(myVec6.begin(),myVec6.end(),[](double& i){i=std::sqrt(i);});
  std::cout << std::setw(20) << std::left << "myVec6: i->sqrt(i): ";
  for (auto v: myVec6) std::cout << std::fixed << std::setprecision(2) << std::setw(6) << v ;
 
  std::cout << "\n\n";
}
  • ergibt
myVec4: i->3*i+5:   8     11    14    17    20    23    26    29    32    35    
myVec5: i->i*i      1     4     9     16    25    36    49    64    81    100   
myVec6: i->sqrt(i): 1.00  1.41  1.73  2.00  2.24  2.45  2.65  2.83  3.00  3.16 

Vergleich

SpracheCodelokalitätRückgabetyp
C sehr gering bei Funktionszeiger muss explizit angegeben werden
C++ sehr hoch bei Lambda-Funktionen wird vom Compiler bei Lambda-Funktionen automatisch bestimmt

Anmerkung

  • der Compiler erzeugt aus der Lambda-Funktion implizit ein Funktionsobjekte und instanziiert dies
  • Lambda-Funktionen sollten
    • kurz und knackig sein
    • selbsterklärend sein
  • da Funktoren und Lambda-Funktionen direkt an der Stelle ihre Verwendung instanziiert werden, ist ihre Verwendung in der Regel performanter als die von Funktionszeigern

Klassen

Methoden

Virtuelle Methoden

Zweck
  • virtuelle Methoden sind an Objekte gebunden Methoden, die sich abhängig vom Typ des Objektes verhalten
C
#include <stdio.h>
#include <stdlib.h>
 
typedef enum {Circle, Triangle} ShapeKind;
 
typedef struct {
  ShapeKind sKind;
} Shape;
 
// constructor
Shape* createNewShape(ShapeKind s) {
  Shape* p = (Shape*) malloc(sizeof(Shape));
  p->sKind = s;
  return p;
}
 
// virtual dispatch
void draw(Shape* s){
  switch(s->sKind){
  case Circle:
    printf("draw a Circle\n");
  break;
  case Triangle:
    printf("draw a Triangle\n");
    break;
  }
};
 
int main(void){
  Shape* shapeCirc= createNewShape(Circle);
  Shape* shapeTriang= createNewShape(Triangle);
  draw(shapeCirc);     
  draw(shapeTriang);    
  return 0;
}
  • ergibt
draw a Circle
draw a Triangle
  • die AufzählungShapeKinddient als Diskriminator
  • jede StrukturShapeerhält eine spezielleShapeKind
  • die FunktioncreateNewShapegibt neue Objekte zurück, die ihr richtigeShapeKindbesitzen
  • in der Funktiondrawfindet der virtuelle Dispatch aufgrund derShapeKindstatt
C++
#include <iostream>
 
class Shape{
public:
  virtual void draw()= 0;
  virtual ~Shape()= default;
};
 
class ShapeCircle: public Shape{
public:
  void draw(){
    std::cout << "draw a circle" << std::endl;
  }
};
 
class ShapeTriangle: public Shape{
public:
  void draw(){
    std::cout << "draw a triangle" << std::endl;
  }
};
 
int main(){
  Shape* myShapeCir= new ShapeCircle;
  Shape* myShapeTriang= new ShapeTriangle;
 
  myShapeCir->draw();     
  myShapeTriang->draw(); 
}
  • ergibt
draw a circle
draw a triangle
  • die abstrakte BasisklasseShapegibt das Interface für alle Shape-Klassen vor
  • jede konkrete KlasseShapeCircleoderShapeTrianglemuss die Methodedrawimplementieren
Vergleich
SpracheKapselungErweiterbarkeitProgrammfluss
C nein Diskriminiator und jede case-Struktur muss angepasst werden explizit durch den Programmierer
C++ ja Erzeugung einer neuen Klasse implizit durch den Compiler
Anmerkung
C
  • simuliert nur virtuelle Methoden
C++
  • ermöglicht deutlich komplexere Varianten
    • Mehrfachvererbung
    • virtuelle, nicht virtuelle oder rein virtuelle Methoden

Variablen

Kapseln von Variablen in Strukturen

Zweck
  • um den globalen Namensraum nicht zu verschmutzen, werden Variablen in Strukturen gepackt
C
#include <stdio.h>
 
typedef struct{
  int a;
} Test;
 
int main(void){
  Test test;
  test.a= 5;
  printf("%d\n",test.a);
  return 0;
}
  • ergibt
5
C++
#include <iostream>
 
class Test{
public:
  int getA() const { return a; }   // implicit inline
  inline void setA(int);
private:
  int a;
};
 
void Test::setA(int a_){
  a=a_;
}
 
int main(){
  Test test;
  test.setA(5);
  std::cout << test.getA() << std::endl;
}
  • ergibt
5
Vergleich
SpracheVerbositätKapselungUnterscheidung Schreib- Lesezugriff
C gering nur durch den Struktur-Qualifier nein
C++ hoch vollständig ja
Anmerkung
  • die C++-Varianten ist so effizient wie die C-Variante, da die Methode explizitinlineerklärt werden kann oder implizitinlineist
  • die Kapselung einer Variable in einer Struktur in C trägt nur dazu bei, dass der globale Namensraum durch diese nicht verschmutzt wird

Speicher

Allokation

Zweck

  • Speicher wird dann erst allokiert, wenn er benötigt werden
  • Datenstrukturen fordern zur Laufzeit Speicher nach

C

Monitor* monC= (Monitor*)malloc(sizeof(Monitor));
if (monC == 0) error("memory exhausted");
monC->init(2);

C++

new
Monitor* monCpp= new Monitor(2);
Smart Pointer
explizite Besitzverhältnisse
std::unique_ptr<Monitor> uniquePtr(new Monitor(2));
auto uniquePtr2= std::make_unique<Monitor>(2);      // C++14
geteilte Besitzverhältnisse
std::shared_ptr<Monitor> shardPtr(new Monitor(2));
std::shared_ptr<Monitor> sharedPtr2= sharedPtr;
auto sharedPtr3= std::make_shared<Monitor>(2);

Vergleich

SpracheInitialisierung des Objektsautomatische Verwaltung des Lebenszyklus der Daten
C nein nein
C++ ja Smart Pointer

Anmerkung

  • die Smart Pointerunique_ptrundshared_ptrverwalten automatisch die ihnen anvertraute Ressource
shared_ptr
  • bietet Reference Counting an
  • besitzt eine Referenz auf seine Ressource und eine auf den gemeinsamen Zähler
    • inkrementiert seinen Referenzzähler, wenn er kopiert wird
    • dekrementiert seinen Referenzzähler, wenn er gelöscht wird
    • wenn der Referenzzähler den Wert 0 erreicht, gibt es sich und seine Ressource sofort frei
unique_ptr
  • verwaltet den Lebenszyklus einer Ressource
  • besitzt die gleiche Performance wie ein native Pointer an
  • wenn er seine Gültigkeit verliert, gibt es sich und seine Ressource sofort frei

Deallokation

Zweck

  • Speicher muss zeitnah freigegeben werden um wieder zur Verfügung zu stehen

C

free monC;

C++

delete monCpp;

Vergleich

Spracheautomatischer Freigabe des Objektesautomatische Verwaltung des Lebenszyklus der Ressource
C nein nein
C++ nein bei native Zeigern mittels Smart Pointer

Anmerkung

  • der C-Ausdruck gibt den Speicher frei
  • der C++-Ausdruck gibt den Speicher frei und ruft den Destruktor des Objektes auf
  • Speicher soll mit Smart Pointern verwaltet werden

Bibliothek

Array

Zweck

  • Arrays die sequentiellen Datenstrukturen in C

C

#include <stdio.h>
 
int main( void ){
  int myArray[]={1,2,3,4,5,6};
  int sum;
  int i;
  for( i = 0; i < 6; i++ ){
    sum += myArray[i];
  } 
  printf("Sum: %d \n", sum );     
  return 0;
}
  • ergibt
Sum: 21

C++

#include <array>
#include <iostream>
#include <numeric>
 
int main(){
  std::array<int,6> myArray= {1,2,3,4,5,6};
  std::cout << "Sum: " << std::accumulate(myArray.begin(),myArry.end(),0) << std::endl;   
}
  • ergibt
Sum: 21

Vergleich

SpracheStatische GrößeSpeicheranforderungKompatibel mit der Standard Template Library
C ja minimal nein
C++ ja minimal ja

Anmerkung

  • std::arrayvereint das Beste aus zwei Welten
    • es besitzt eine statische Größe und minimale Speicheranforderungen wie das C-Array
    • es besitzt ein Interface wie derstd::vector
#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
 
const int NUM= 10;
 
int main(){
 
  std::cout << std::boolalpha;
 
  std::array<int,NUM> arr{{0,1,2,3,4,5,6,7,8,9}};
  std::cout << std::endl << "arr: ";
  for ( auto a: arr){
    std::cout << a << " " ;
  }
 
  std::cout << std::endl;
 
  // initializer list
  std::array<int,NUM> arr2{{19,11,14,18,14,15,16,12,17,13}};
  std::cout << std::endl << "arr2 unsorted: ";
  std::copy(arr2.begin(),arr2.end(), std::ostream_iterator<int>(std::cout, " "));
  std::sort(arr2.begin(),arr2.end());
  std::cout << std::endl << "arr2 sorted: ";
  std::copy(arr2.rbegin(),arr2.rend(), std::ostream_iterator<int>(std::cout, " "));
 
  std::cout << std::endl;
 
  // get the sum and the mean of arr2
  double sum= std::accumulate(arr2.begin(),arr2.end(),0);
  std::cout << "sum of a2: " << sum << std::endl;
  double mean= sum / arr2.size();
  std::cout << "mean of a2: " << mean << std::endl;
 
 
  // swap arrays
  std::swap(arr,arr2);
  std::cout << std::endl << "arr2: ";
    for ( auto a: arr){
      std::cout << a << " " ;
    }
 
  std::cout << std::endl;
 
  // comparison
  std::cout << "(arr < arr2): " << (arr < arr2 ) << std::endl;
 
  auto count= std::count_if(arr.begin(),arr.end(),[](int i){ return (i<15) or (i>18) ; });
  std::cout << "Numbers smaller then 15 or bigger then 18: " << count << std::endl;
 
  std::cout << std::endl;
 
}
  • ergibt
arr: 0 1 2 3 4 5 6 7 8 9 

arr2 unsorted: 19 11 14 18 14 15 16 12 17 13 
arr2 sorted: 19 18 17 16 15 14 14 13 12 11 
sum of a2: 149
mean of a2: 14.9

arr2: 11 12 13 14 14 15 16 17 18 19 
(arr < arr2): false
Numbers smaller then 15 or bigger then 18: 6

Techniken

Ausgabe

Zweck

  • formatierte Ausgabe von verschiedenen Datentypen auf der Konsole

C

#include <stdio.h>
 
int main(){
 
  printf("Characters: %c %c \n", 'a', 65);
  printf("Decimals: %d %ld\n", 2011, 650000L);
  printf("Preceding with blanks: %10d \n", 2011);
  printf("Preceding with zeros: %010d \n", 2011);
  printf("Doubles: %4.2f %+.0e %E \n", 3.1416, 3.1416, 3.1416);
  printf("%s \n", "From C to C++");
  return 0;
}
  • ergibt
Characters: a A 
Decimals: 2011 650000
Preceding with blanks:       2011 
Preceding with zeros: 0000002011 
Doubles: 3.14 3.141600e+00
From C to C++ 

C++

#include <iomanip>
#include <iostream>
 
int main(){
 
  std::cout << "Characters: " << 'a' << " " <<  static_cast<char>(65) << std::endl;  
  std::cout << "Decimals: " << 2011 << " " << 650000L << std::endl;
  std::cout << "Preceding with blanks: " << std::setw(10) << 2011 << std::endl;
  std::cout << "Preceding with zeros: " << std::setfill('0') << std::setw(10) << 20011 << std::endl;
  std::cout << "Doubles: " << std::setprecision(3) << 3.1416 << " " << std::setprecision(6) << std::scientific <<  3.1416 << std::endl;
  std::cout << "From C to C++" << std::endl;
 
}
  • ergibt
Characters: a A
Decimals: 2011 650000
Preceding with blanks:       2011
Preceding with zeros: 0000020011
Doubles: 3.14 3.141600e+00
From C to C++

Vergleich

SpracheTypisierungTypsicherheit
C explizit durch den Programmierer nein
C++ implizit durch den Compiler ja

Anmerkung

C
  • falsche Formatspezifier führen zu undefinierten Verhalten
printf("%d\n",2011);             // 2011
printf("%d\n",3.1416);           // 2147483643
printf("%d\n","2011");           // 4196257
printf("%s\n",2011);             // Speicherzugriffsfehler
C++
std::cout << 2011 << std::endl;     // 2011
std::cout << 3.1416 << std::endl;   // 3.1416 
std::cout << "2011" << std::endl;   // "2011"
#include <iostream>
#include <stdexcept>
#include <string>
 
void printf_(const char *s){
  while (*s) {
    if (*s == '%' && *(++s) != '%')
      throw std::runtime_error("invalid format string: missing arguments");
      std::cout << *s++;
  }
}
 
template<typename T, typename... Args>
void printf_(const char *s, T value, Args... args){
  while (*s) {
    if (*s == '%' && *(++s) != '%') {
      std::cout << value;
      ++s;
      printf_(s, args...); // call even when *s == 0 to detect extra arguments
      return;
    }
    std::cout << *s++;
  }
  throw std::logic_error("extra arguments provided to printf");
}
 
int main() {
  std::cout << std::endl;
 
  const char* m = "The value of %s is about %g.\n";
  printf_(m,"pi", 3.14159);
  printf(m,"pi", 3.14159);
 
  // printf_("A string: %s");      // std::runtime_error
                                   // what(): invalid format string: missing argument
  printf("A string: %s");       
 
  std::cout << std::endl;
 
}
  • ergibt beim Compilieren
g++ -std=c++11 -Wall -g -c -o obj/printf.o C++/printf.cpp
C++/printf.cpp: In function ‘int main()’:
C++/printf.cpp:39:24: warning: format ‘%s’ expects a matching ‘char*’ argument [-Wformat]
g++ -std=c++11 -Wall -g -o bin/printf obj/printf.o
  • ergibt bei der Ausführung
The value of pi is about 3.14159.
The value of pi is about 3.14159.
A string: A string: of pi is about 3.14159ut
  • C-printf
    • besitzt undefiniertes Verhalte
  • C++-printf_
    • führt beim Compilieren zu einer Warnung
    • führt zu einem Laufzeitfehler

Lebenszyklus einer Ressource

Zweck

  • Typischer Weise besteht der Lebenszyklus einer Ressource aus den 3 Abschnitten
    1. Initialisierung der Ressource
    2. Arbeiten mit der Ressource
    3. Freigeben der Ressource

C

#include <stdio.h>
 
void initDevice(const char* mess){
  printf("\n\nINIT: %s\n",mess);
}
 
void work(const char* mess){
  printf("WORKING: %s",mess);
}
 
void shutDownDevice(const char* mess){
  printf("\nSHUT DOWN: %s\n\n",mess);
}
 
int main(void){
 
  initDevice("DEVICE 1");
  work("DEVICE1");
  {
    initDevice("DEVICE 2");
    work("DEVICE2");
    shutDownDevice("DEVICE 2");
  }
  work("DEVICE 1");
  shutDownDevice("DEVICE 1");
 
  return 0;
 
}
  • ergibt
INIT: DEVICE 1
WORKING: DEVICE1

INIT: DEVICE 2
WORKING: DEVICE2
SHUT DOWN: DEVICE 2

WORKING: DEVICE 1
SHUT DOWN: DEVICE 1

C++

  • RAIIsteht für für das sehr häufig verwendete C++-IdiomResourceAcquisitionIsInitialization
  • dabei wird die Ressource im Konstruktor gebunden und im Destruktor wieder freigegeben
#include <iostream>
#include <string>
 
class Device{
  private:
    const std::string resource;
  public:
    Device(const std::string& res):resource(res){
      std::cout << "\nINIT: " << resource << ".\n";
    }
    void work() const {
      std::cout << "WORKING: " << resource << std::endl;
    }
    ~Device(){
      std::cout << "SHUT DOWN: "<< resource << ".\n\n";
    }
};
 
int main(){
 
 
  Device resGuard1{"DEVICE 1"};
  resGuard1.work();
 
  {
    Device resGuard2{"DEVICE 2"};
    resGuard2.work();
  }
  resGuard1.work();
 
}
  • der Lebenszyklus der Ressource
INIT: DEVICE 1.
WORKING: DEVICE 1

INIT: DEVICE 2.
WORKING: DEVICE 2
SHUT DOWN: DEVICE 2.

WORKING: DEVICE 1
SHUT DOWN: DEVICE 1.

Vergleich

SpracheAutomatisches InitialisierenAutomatisches FreigebenRessource-Löcher
C nein nein ja (Nachlässigkeit des Programmierers, Ausnahmen)
C++ ja (Konstruktor) ja (Destruktor) nein

Anmerkung

  • RAII ist ein sehr beliebtes Idiom in C++ für
    • Speicher: die Smart Pointer verwalten ihren Speicher
    • Mutexe: die Locks verwalten ihre Mutexe

Globale Variablen und Funktionen

Zweck

  • Variablen und Funktionen definieren, die global zur Verfügung stehen

C

int showNumber= 0;
 
void increaseNumberShocks(void){
  ++shockNumber;
}

C++

class Shock{
static int number;
public:
  static void increaseNumber();   
};
 
int Shock::number= 0;
void Shock::increaseNumberShocks(){
  ++numberShock;
}
  • statische Attribute und Methoden einer Klassen
    • müssen außerhalb der Klasse definiert werden
    • sind an die Klasse, nicht an die Objekte gebunden
    • können ohne Objekt aufgerufen werden
    • werden über den Klassenraum gekapselt

Vergleich

SpracheStatische VariableStatische Methode
C nein nein
C++ ja ja

Anmerkung

  • statische Attribute und Methoden einer Klasse
    • eignen sich gut um globale Variablen und Funktionen zu ersetzen
    • verschmutzen nicht den globalen Namensraum
  • die Variablenumberdes Objektesshockist an das Objekt gebunden ⇒ ein zweites Objekt vom TypShockkann instanziiert werden, das die gleiche statische Variablenumbernutzt
  • Namensräume sind ein weiteres Mittel um den globalen Namensraum frei zu halten
namespace Shock{
  int number= 0;
}
Shock::number++;

Zeiger versus Referenzen

Zweck

  • Variablen werden oft indirekt über ihre Adresse (Zeiger) oder einen alternativen Namen (Referenz) angesprochen
  • diese Indirektion erlaubt weitere Anwendungsfälle

C

#include <stdio.h>
 
void swap(int* x, int* y){
 
  int tmp = *x;
  *x = *y;
  *y = tmp;
 
}
 
int main( void ){
 
  int i= 2011;
  int* iptr;
  iptr= &i;    // set iptr to the adress of i
  int j;
  int k= 2014;
 
  j= *iptr;         // set j to the value of i
  printf("iptr: %p \n",iptr);
  printf("*iptr: %i \n", *iptr);
  printf("j: %i\n",j);
  printf("k: %i\n",k);
 
  printf("\n");
 
  *iptr= k;         // set i to the value of k
  printf("iptr: %p \n",iptr);
  printf("*iptr: %i \n", *iptr);
  printf("j: %i\n",j);
  printf("k: %i\n",k);
 
  printf("\n");
 
  swap(&j,&k);
 
  printf("j: %i\n",j);
  printf("k: %i\n",k);
 
  return 0;
}
  • ergibt
iptr: 0x7fff080e7734 
*iptr: 2011 
j: 2011
k: 2014

iptr: 0x7fff080e7734 
*iptr: 2014 
j: 2011
k: 2014

j: 2014
k: 2011

C++

#include <iostream>
 
void swap(int& x,int& y){
 
  int tmp = x;
  x = y;
  y = tmp;
 
}
 
int main(){
 
  int i= 2011;
  int& refi= i;    // refi is a alias of i
  std::cout << "i: "  << i << std::endl;
  std::cout << "refi: "<< refi << std::endl;
 
  std::cout << std::endl;
 
  refi= 2014;
  std::cout << "i: "  << i << std::endl;
  std::cout << "refi: "<< refi << std::endl;
 
  std::cout << std::endl;
 
  int j= 2011;
  int k= 2014;
  std::cout << "j: "  << j << std::endl;
  std::cout << "k: " << k << std::endl;
 
  swap(j,k);
  std::cout << std::endl;
 
  std::cout << "j: "  << j << std::endl;
  std::cout << "k: " << k << std::endl;
 
}
  • ergibt
i: 2011
refi: 2011

i: 2014
refi: 2014

j: 2011
k: 2014

j: 2014
k: 2011

Vergleich

SpracheInitialisierungBindungsdauerSemantik
C Deklaration und Initialisierung können getrennt sein Zeiger können auf neue Adressen verweisen Unterscheidung zwischen dem Wert (*iptr) und der Adresse des Zeigers (iptr) ist notwendig
C++ muss bei der Definition erfolgen eine Referenz ist immer an das gleiche Objekt gebunden verhalten sich wie Variablen

Anmerkung

  • Zeiger und Referenzen sind die Grundlage für Polymorphie in der objektorientierten Programmierung in C++
Zeiger
  • unterstützen Zeigerarithmetik
 
int p[10] = {0,1,2,3,4,5,6,7,8,9};
int* point= p;
printf("%d\n", *point);       // 0
point++;
printf("%d\n", *point);       // 1
 
point +=8;
printf("%d\n", *point);       // 9
 
point--;
printf("%d\n", *point);       // 8
  • vom Typ void können auf Daten beliebigen Typs verweisen
double d= 3.17;
void* p= &d;
  • können auf Funktionen verweisen
void addOne(int& x){
  x+= 1;
}
void (*inc)(int& x)= addOne;
Referenzen
  • verhalten sich wie konstante Zeiger
  • verweisen ihre ganze Lebenszeit auf das gleiche Objekt

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare