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
- Präprozessor
- einfache Textsubstitution ohne Semantik
- Templates
- Instanziierung der Templates zur Compilezeit ist eine funktionale, turing-complete Subsprache in C++
- das Ergebnis der Instanziierung ist C++-Sourcecode
- konventionelles C++
Konstanten
Zweck
- werden zur Compilezeit ausgewertet
- können als Größe eines Arrays verwendet werden
- werden im ROM gespeichert
C
C++
Vergleich
Sprache | Semantik | Syntax |
C |
reine Textersetzung |
keine Syntax |
C++ |
Sprachkonstrukt |
normale C++-Syntax |
Anmerkung
- durch das Schlüsselwort
constexpr
lassen sich Konstanten erklären, die zur Compilezeit evaluiert werden
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;
}
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
Sprache | Semantik | Syntax |
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üssel
constexpr
lassen 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);
}
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;
}
capacity: 0
size: 0
capacity: 11
size= 6
myIntVec[3]: 3
Vergleich
Sprache | Semantik | Syntax | Erweiterung | Fehleranfälligkeit | Speichermanagement |
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
Sprache | implizite Konvertierung nach int | implizite Konvertierung nach 0 oder Nullzeiger | Konvertierung 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 ⇒nullptr
lässt sich nicht nachint
konvertieren
- C: die Nullzeiger Konstante (
NULL
) lässt sich nachchar*
und nachint
konvertieren ⇒ 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, das
NULL
als0L
oder0LL
definiert ist
- wird
NULL
als0
definiert, ist der AufrufoverloadTest(NULL)
ist eindeutig
Typen
Bool
Zweck
- Datentyp für die Wahrheitswerte
true
oderfalse
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
Sprache | built-in Datentyp | implizite Konvertierung nach 0/1 |
C |
nein |
ja |
C++ |
ja |
ja |
Anmerkung
- der Vektor ist für den Datentyp
bool
auf 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;
}
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;
}
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
Sprache | Speichermanagement | Dynamische Größenanpassung | String Termination Zeichen | Fehleranfä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;
}
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;
}
g: 14
Vergleich
Sprache | Initialisierung | Verengung |
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 von
void*
- 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 Qualifier
const
odervolatile
vom 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
Sprache | eindeutige Konvertierung | explizite 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
Sprache | Definition der Laufzeitvariable | Sichtbarkeit 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;
}
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);
}
Implementation A
Implementation B
Implementation A
Implementation B
No Name
Vergleich
Sprache | Programmlogik | Erweiterbarkeit | Programmfluss |
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++
Vergleich
Sprache | Argument | return-Anweisung |
C |
void bei 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-Funktion
main(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";
}
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";
}
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";
}
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
Sprache | Codelokalität | Rü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;
}
draw a Circle
draw a Triangle
- die Aufzählung
ShapeKind
dient als Diskriminator
- jede Struktur
Shape
erhält eine spezielleShapeKind
- die Funktion
createNewShape
gibt neue Objekte zurück, die ihr richtigeShapeKind
besitzen
- in der Funktion
draw
findet der virtuelle Dispatch aufgrund derShapeKind
statt
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();
}
draw a circle
draw a triangle
- die abstrakte Basisklasse
Shape
gibt das Interface für alle Shape-Klassen vor
- jede konkrete Klasse
ShapeCircle
oderShapeTriangle
muss die Methodedraw
implementieren
Vergleich
Sprache | Kapselung | Erweiterbarkeit | Programmfluss |
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;
}
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;
}
5
Vergleich
Sprache | Verbosität | Kapselung | Unterscheidung 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 explizit
inline
erklärt werden kann oder implizitinline
ist
- 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
Sprache | Initialisierung des Objekts | automatische Verwaltung des Lebenszyklus der Daten |
C |
nein |
nein |
C++ |
ja |
Smart Pointer |
Anmerkung
- die Smart Pointer
unique_ptr
undshared_ptr
verwalten 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
C++
Vergleich
Sprache | automatischer Freigabe des Objektes | automatische 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;
}
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;
}
Sum: 21
Vergleich
Sprache | Statische Größe | Speicheranforderung | Kompatibel mit der Standard Template Library |
C |
ja |
minimal |
nein |
C++ |
ja |
minimal |
ja |
Anmerkung
std::array
vereint das Beste aus zwei Welten
- es besitzt eine statische Größe und minimale Speicheranforderungen wie das C-Array
- es besitzt ein Interface wie der
std::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;
}
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;
}
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;
}
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
Sprache | Typisierung | Typsicherheit |
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;
}
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
- Initialisierung der Ressource
- Arbeiten mit der Ressource
- 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;
}
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
Sprache | Automatisches Initialisieren | Automatisches Freigeben | Ressource-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
Sprache | Statische Variable | Statische 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 Variable
number
des Objektesshock
ist an das Objekt gebunden ⇒ ein zweites Objekt vom TypShock
kann instanziiert werden, das die gleiche statische Variablenumber
nutzt
- 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;
}
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;
}
i: 2011
refi: 2011
i: 2014
refi: 2014
j: 2011
k: 2014
j: 2014
k: 2011
Vergleich
Sprache | Initialisierung | Bindungsdauer | Semantik |
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
Weiterlesen...