De introductie van C++ in 1985 geeft de ervaren C programmeur enkele extra mogelijkheden om zijn of haar code te structureren. C++ wordt nog steeds omschreven als een “general purpose” low-level taal op Wikipedia:
C++ is a general-purpose programming language. It has imperative, object-oriented and generic programming features, while also providing facilities for low-level memory manipulation.
“Object-oriented” en “generic” zijn de sleutelwoorden hier. De C++ taal is geschreven als een extensie van C waarbij high-level features gebruikt kunnen worden om code te structureren. Kenners van Java voelen hun hier waarschijnlijk iets meer thuis (abstracts, classes, generics, grote STL bibliotheek, overloading) en dat verbaast niemand: de Java taal is sterk beïnvloed door C++.
Enkele belangrijke zaken die onmiddellijk opvallen:
bool
std::string
(bovenop char
) en std::wstring
(bovenop wchar_t
), Extra Integral types:
wchar_t
, char16_t
, char32_t
(voor Unicode)::
en namespaces<<
, >>
streamsclass
en alles wat daar mee te maken heeft
new
, delete
in plaats van malloc()
en free()
nullptr
in plaats van NULL
,auto
als type inference zoals var
in C#, decltype
in plaats van typeof
Het is tijd om malloc()
en struct
achterwege te laten:
#include <iostream>
class Persoon {
private:
int leeftijd;
public:
Persoon(int leeftijd);
bool isOud() const {
std::cout << "checking leeftijd van persoon " << leeftijd << std::endl;
return leeftijd > 60;
}
int getLeeftijd() const { return leeftijd; }
};
Persoon::Persoon(int leeftijd) {
this->leeftijd = leeftijd;
}
int main() {
auto jaak = new Persoon(70);
std::cout << "is jaak oud? " << jaak->isOud() << std::endl;
}
Dat ziet er al heel wat properder uit:
this
pointer verwijst naar de huidige instance van de klasse.Persoon *
met C++11’s auto
keyword. Leer auto goed kennen en gebruiken: overtuig jezelf!Type inference is in combinatie met top-level const
en references niet zo triviaal, zie p.68.
Een klasse definiëren is niet meer dan een beschrijving van een structuur. Binnen de class
accolades { }
leven niet alle declaraties van de functies in de klasse zelf! Dit is nog een erfenis van C. Elke klasse heeft zijn eigen scope en functies declareren doe je na de definitie met de scope operator ::
:
class Getal {
private:
int getal;
public:
int get() const { return getal; }
void telOpMet(int ander);
void vermenigvuldigMet(int ander);
};
void Getal::telOpMet(int ander) {
this->getal += ander;
}
void Getal::vermenigvuldigMet(int ander) {
this->getal *= ander;
}
Twee van de drie methodes staan buiten de klasse definitie. We maken hier typisch twee files voor aan:
Verschillende andere source files kunnen de Getal klasse gebruiken met #include "getal.h"
. De klasse kan maar 1x gedefiniëerd worden, en 2x de header includen in je programma geeft een compilatiefout:
Wouters-MacBook-Air:c-course-gba wgroenev$ g++ -std=c++11 dubbel.cpp getal.o In file included from dubbel.cpp:4: ./getal.h:1:7: error: redefinition of 'Getal' class Getal { ^ ./getal.h:1:7: note: previous definition is here class Getal { ^ 1 error generated.
Dit lossen we op met een preprocessor constructie in de getal.h header file:
#ifndef _GETAL_H_
#define _GETAL_H_
class Getal { };
#endif
Zoals in veel andere programmeertalen, kan klasse 1 wel naar klasse 2 verwijzen, maar dan niet omgekeerd. Een mooi gesloten lus maken in schemavorm noemen we een circulaire dependency: object 1 is afhankelijk van object 2, die op zijn beurt terug afhankelijk is van object 1:
Het is niet mogelijk om twee klassen naar elkaar te laten verwijzen door middel van een #ìnclude
statement. De volgende foutboodschap schept hierin genoeg duidelijkheid:
In file included from ./inc2.h:2: In file included from ./inc1.h:2: In file included from ./inc2.h:2: In file included from ./inc1.h:2: In file included from ./inc2.h:2: In file included from ./inc1.h:2: [...] ./inc2.h:4:7: error: redefinition of 'Inc2' class Inc2 { ^ ./inc2.h:4:7: note: previous definition is here class Inc2 { ^ fatal error: too many errors emitted, stopping now [-ferror-limit=] 20 errors generated.
Waarom is de eerste methode in de body gedeclareerd? Dit is een inline
functie (zie doc). Dit zijn typisch one-liners die vaak aangeroepen worden en door de compiler geoptimaliseerd kunnen worden: de aanroep instructie vervangen door de implementatie, zoals de preprocessor doet met #define
.
Wanneer schrijven we een inline functie?
Sinds C++ 11 kan je ook default values meegeven aan data members in de definitie van de klasse zelf. Dit klinkt belachelijk omdat zoiets in Java vanaf het begin al kon. In de private sectie van de Getal klasse kunnen we dus int getal = 5;
zetten. Als er geen constructor deze member initialiseert wordt getal op 5 gezet.
De magische this
variabele wordt impliciet aangemaakt zodra je een methode van een instantie op een klasse uitvoert. This is het adres van de locatie van diezelfde instantie:
auto g = new Getal();
g->telOpMet(5);
// compiler interpretatie: Getal::telOpMet(&g, 5);
Het const
keyword achter de get methode verandert de this
pointer naar een constante pointer. Op die manier kan de body van de methode geen wijzigingen doorvoeren, enkel opvragen. (Zie p. 258)
Een klasse instantiëren roept de (default) constructor aan. Als er geen eigen gedefiniëerde constructor aanwezig is, genereert de compiler die voor u, net als in Java. Zodra je één constructor definiëert, zal C++ geen enkele zelf genereren.
class Getal {
private:
int *x;
public:
Getal(int x) : x(new int(x)) {}
};
auto g = new Getal(5); // ok: eigen constructor aangeroepen
auto g = new Getal(); // error: Too few arguments, expects 1
De default constructor is makkelijk zelf te voorzien met Getal() {}
maar met Getal() = default;
zeggen we tegen de compiler dat hij expliciet wél eentje moet genereren.
Merk op dat we hier een memory leak introduceren door *x
niet zelf op te kuisen! Als een klasse resources zoals pointers bevat is het de bedoeling dat deze zelf verantwoordelijk is voor de opkuis. Dit gebeurt in de destructor prefixed met ~
:
class Getal {
private:
int *x;
public:
Getal() = default;
~Getal() { delete x; }
Getal(int x) : x(new int(x)) {}
};
Java heeft geen destructors omdat objecten op de heap leven en door de Garbage Collector opgeruimd worden zonder invloed van de programmeur. Er is wel een finalize
die je zelf kan aanroepen om resources op te kuisen. In C# wordt ook de ~Object(){}
notatie gebruikt, maar dat is ook een soort van finalizer en geen échte destructor.
Een derde soort constructor, de “copy constructor”, wordt ook door C++ voorzien en aangeroepen wanneer de expressie getal1 = getal2
geëvalueerd wordt. De compiler maakt een nieuwe Getal
instance aan en kopiëert alle velden over.
Dit is echter helemaal niet wat we willen als we resources als members hebben zoals *x
: de pointer wordt gekopiëerd maar niet de inhoud. Beide getal instances verwijzen dan dezelfde x
waarde:
Wat is de output van dit programma?
int main() {
auto g = new Getal(5);
auto g2 = g;
g2->x = new int(10);
cout << *(g2->x) << endl; // ?
cout << *(g->x) << endl; // ?
}
Oeps. Voorzie in dat geval je eigen copy constructor met Getal(const Getal& other) : x(new int(*(other.x))) {}
. Copy constructors kan je ook defaulten.
Herinner je uit hoofdstuk 2 reference type definities zoals int &getal
. Dit kan enkel worden gecompileerd met g++
en niet gcc
. Deze notatie ga je veel tegen komen in C++ methode argumenten. Objecten die meegegeven worden zijn bijna altijd reference types in plaats van pointers. Waarom legt de C++ FAQ uit:
Use references when you can, and pointers when you have to. References are usually preferred over pointers whenever you don’t need “reseating”. This usually means that references are most useful in a class’s public interface. References typically appear on the skin of an object, and pointers on the inside.
Zo kunnen we in Getal methodes toevoegen die andere Getal instanties gebruiken:
// getal.h
class Getal {
void telOpMet(const Getal &ander);
}
// getal.cpp
void Getal::telOpMet(const Getal &ander) {
this->getal += ander.getal;
}
// dubbel.cpp, in main
auto g = new Getal();
auto nieuwGetal = new Getal();
nieuwGetal->telOpMet(*g);
We moeten g dereferencen om als reference mee te kunnen geven. const
wordt hier gebruikt om zeker te zijn dat het binnenkomende getal niet gewijzigd kan worden. Alternatief kunnen we new
twee keer weglaten: auto g = Getal();
en dan nieuwGetal.telOpMet(g);
.
Een pointer naar een object in het geheugen aanmaken (en geheugen reserveren) met new
vereist dat je die zelf opkuist met delete
! Als je dit niet doet blijft dat geheugen bezet en krijg je wat men noemt “memory leaks”: hoe langer men je programma gebruikt, hoe meer geheugen het (ongewenst) in beslag neemt.
Om dat te vermijden gebruik je best binnen functies nooit new
:
void telOpAutoClean() {
auto g = Getal(3);
g.telOpMet(5);
// block eindigt: g wordt opgekuist
}
void telOpManualClean() {
auto g = new Getal(5);
g->telOpMet(5);
// block eindigt: g blijft bestaan!
delete g;
}
Getal(3)
zonder new ziet er vreemd uit als je talen als C# en Java gewoon bent, maar de constructor wordt evenzeer aangeroepen en een object wordt evenzeer voor je geïnstantieerd.
Zonder new
: object op de stack—dit is géén pointer. Denk aan C’s struct
type.
Met new
: object op de heap—dit is wél een pointer. Denk aan C’s *struct
type.
Zoals gekend uit het hoofdstuk over de stack & de heap worden lokale variabelen op de stack bewaard. De stack bevat een tijdelijke workspace aan geheugen wanneer functies aangeroepen worden, die automatisch opgeruimd worden als die functies klaar zijn met hun werk. De Stack is een LIFO lijst.
Deze functionaliteit verandert niet als we de gcc
compiler vervangen door g++
! Je kan echter dankzij functionaliteiten van C++11 bepaalde tekortkomingen gedeeltelijk wegwerken, zoals het automatisch opruimen van dangling pointers. Daar dienen smart pointers voor.
In plaats van zelf geheugen te beheren—we hebben immers wel wat beters te doen—laten we dat over aan de taal door “smart pointers” te gebruiken. Dit zijn STL wrappers (zie onder) die een object encapsuleren. Als een block stopt, kuist deze smart variabele je wrapped object zelf ook op:
unique_ptr<Getal> g (new Getal()); // zonder auto
auto nieuwGetal = unique_ptr<Getal>(new Getal()); // met auto
nieuwGetal->telOpMet(*g);
We komen later nog op de template <>
notatie terug. Neem hier aan dat dit werkt als Generics in Java.
Zie p.470 van Smart pointers in modern C++ van de Microsoft docs, of de Smart pointer reference van cppreference.com
.
Vergeet niet dat smart pointers niet werken in combinatie met variabelen op de stack (dus altijd new
gebruiken). Stel dat een klasse een referentie naar een unique_ptr
heeft die automatisch de pointer zou moeten opkuisen:
class Holder {
public:
unique_ptr<Iets> autoDeleted;
Holder() {
Iets iets("1");
this->autoDeleted = unique_ptr<Iets>(&iets);
}
}
De deconstrutor van de holder gaat automatisch de waarde die autoDeleted vasthoudt terug vrijgeven, maar de variabele iets
bestaat al niet meer omdat die enkel op de stack binnen de constructor functie leeft. &iets
verwijst nu naar “niks” en dit crasht.
Merk op dat in bovenstaande Persoon klasse printf()
verdwenen is. In C++ gebruiken we streams: cout
als stdout
en cin
als stdin
. Deze leven in de std
namespace zodra je iostream
include. Laat de “.h” suffix achterwege bij het includen van systeembibliotheken van C++. Laat het maar aan de compiler over om de systeembestanden te zoeken tijdens het linken.
De Standard Template Library STL is een bibliotheek die meegeleverd wordt bij de meeste moderne C++ compilers waar iostream
in leeft. Deze implementeert de nieuwe standaarden, zoals C++11. Compilers vragen soms wel een flag om te kiezen met welke library er gelinkt wordt: g++ -std=c++11
.
STL bevat een hoop dingen die je het leven makkelijker maakt: strings (gek genoeg nog steeds geen deel van de taal zelf), collecties, streams, IO, … Bekijk het als de .NET library voor de C# taal of de meegebakken java.*
klassen en methodes voor Java. “Part II: The C++ Library (p. 307)” behandelt deze zaken in het handboek.
In plaats van constant std::cout
te moeten typen kunnen we alles wat in die namespace zit ook “importeren” zoals een Java import java.io.*
met using namespace std;
. cout
is een instantie van de klasse ostream
.
In C++ kan je op twee manieren aan objecten een waarde toekennen (Zie p.43). Het is belangrijk om de verschillende nuances te kunnen onderscheiden omdat met objecten verschillende constructoren gemoeid zijn.
int x(5)
of Getal g(5)
Hiervoor is een constructor (met argument) nodig.
Waarom zou je ()
doen in plaats van =
? Omdat impliciete conversie enkel via direct initialization gebeurt. Stel, ik wil een string in de constructor aanvaarden. Strings met quotes in C++ zijn nog steeds char arrays vanuit C. Dit gaat niet:
// C++
class Groet {
public:
Groet(std::string s) {}
void zegIets() {};
};
Groet heykes = "sup"; // error: no conversion from const char[3] to Groet
Groet heykes("sup"): // ok: impliciete conversie
Groet hekyes = "sup"s; // ok: char[] zelf omgezet, zie strings sectie
Groet heykes; // ok: heykes is nieuw leeg Groet object op stack
heykes.zegIets(); // ok
Vergeet niet dat het verschil tussen C++ en Java op gebied van initialisatie groot is! In Java zijn objecten na hun declaratie altijd null
:
// Java
class Groet {
public Groet(String s) {}
public void zegIets() {}
}
Groet heykes("sup"); // error: impliciete conversie gaat nooit lukken
Groet heykes; // ok: heykes verwijst naar null
heykes.zegIets(); // error: NullPointerException
heykes = new Groet(); // ok: object op heap aangemaakt
heykes.zegIets(); // ok
int x = 5
of Getal g = Getal(5)
of int x = { 5 }
Hiervoor is een copy constructor nodig.
Merk op dat gebruik van accolades { }
eigenlijk copy initialisatie doorvoert, evenals single return statements als return 3 + 4
. Dit kan je omzetten naar direct initialization met int retval(3 + 4); return retval;
.
Constructors en copy constructors hoeven niet zelf aangemaakt te worden: C++ voorziet defaults. Een lege constructor maakt een leeg object aan. Een lege copy constructor voert eenm ember-wise (shallow) copy door. Als je die toch wil maken doe je dat zo:
class Punt {
private:
int x, y;
public:
Punt() : x(1), y(1) { } // default
Punt(const Punt& other) : x(other.x), y(other.y) { } // copy
}
std::string
in #include <string>
is de vervanger van de rudimentaire char*
in C++. Met strings kan je zoals in Java dingen doen als:
.size()
of .empty()
+
operator[]
<(=)
, >(=)
De grootte van een strings is niet een int
maar een std::size_type
“companion type” om STL op eender welke machine op dezelfde manier te kunnen gebruiken (p.88). Dat is een van de nadelen van C(++) doordat we voor een specifiek systeem compileren.
Gebruik de C++ 11 range for notatie die we van C# en Java kennen:
auto str = string("sup");
for(auto c : str) {
cout << c;
}
Als c een reference is kan je in de loop zelf de karakters ook wijzigen.
In plaats van met vaste array waardes te werken kunnen we ook lijsten gebruiken. STL voorziet er een aantal, waarvan vector
de belangrijkste is - Java’s ArrayList
tegenhanger. Vanaf C++ 11 kan je die ook snel initialiseren met copy initialization:
vector<string> tekst = { "roe", "koe", "zei", "de", "duif" }; // lijst van 5 strings
vector<string> leeg; // lege lijst van strings, grootte 9
tekst = leeg; // copy constructor gebruikt; tekst is nu ook leeg
tekst[0] = "nul"; // segmentation fault: tekst heeft size 0
tekst = vector<string>(1); // lege lijst van strings, grootte 1
tekst[0] = "nul"; // ok
p.98 of cppdocs bevat basis manipulaties voor vectoren, zoals:
push_back()
for(auto i : v)
size()
en empty()
zoals std::string
begin()
en end()
Bibliotheek
die een lijst van boeken (als simpele string) bevatBibliothecaris
die de operaties op de bibliotheek uitvoertcin >> var
om de boeken van de gebruiker in te lezen.paddle
, de ball
, enzovoort. De C++ nodige cross-compiler is arm-none-eabi-g++
.Dierentuin
klasse. Een dierentuin kan verschillende dieren (Dier
klasse) ontvangen (ontvang()
functie). Elk dier heeft een grootte en een naam: Neushoorn(40), Giraf(25), Poema(10). Elke dierentuin heeft x beschikbare ruimte. Wat doet het bestuur van je dierentuin als het te ontvangen dier te groot is? Orc vecht(Orc aanvaller, Orc verdediger)
er nu uit?Vergeet het volgende niet: