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;          // 100
  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 too 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 Gleitkommazahl
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

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 433

Gestern 1599

Woche 6035

Monat 21977

Insgesamt 3320889

Aktuell sind 34 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare