Title: Sichere C -Programmierung Fa. Evosoft N
1Sichere C-ProgrammierungFa. Evosoft Nürnberg
- Zusammenfassung der vermittelten
Programmierrichtlinien
2Const-Qualifizierung
- Nutzen Sie die const-Qualifizierung
- für Variablen, deren Wert allein durch die
Initialisierung festgelegt wird und sich
anschließend nicht mehr ändert - zur Unterscheidung von in- und
inout-Parametern wenn Zeiger oder Referenzen
übergeben wird - um Methoden zu markieren, welche für
const-qualifizierte Objekt-Instanzen aufrufbar
sein sollen
3Zeiger vs. Referenzen
- Nutzen Sie Referenzen,
- wenn dadurch immer ein Objekt referenziert wird,
- und es sich während der Lebensdauer der Referenz
stets ein und dasselbe Objekt handelt - Nutzen Sie Zeiger
- wenn auch der Sonderfall kein Objekt (
Null-Zeiger) darstellbar sein muss - oder während der Lebensdauer des Zeigers
unterschiedliche Objekte referenziert werden
4Explizite Typumwandlung
- Nutzen Sie static_cast für Umwandlungen
- zwischen arithmetischen Datentypen, wenn der
Zieltyp einen kleineren Wertebereich hat und mit
dem Cast eine Warnung des Compilers vermieden
wird - von Ganzzahlen in Gleitpunktzahlen, wenn ein
Quotient mittels Gleitpunkt-Division berechnet
werden soll - nur dann als Down-Cast in einer
Vererbungslinie, wenn es sich um extrem
zeitkritischen Code handelt und die zusätzliche
Sicherheit eines dynamic_cast als absolut
verzichtbar erscheint
5Explizite Typumwandlungen
- Nutzen Sie dynamic_cast
- um Down-Casts in einer Vererbungslinie
abzusichern - mit der Zeiger-Syntax, wenn sie den Fehlerfall
mit explizitem Code behandelt wollen - in der Referenzsyntax, wenn Sie im Fehlerfall
eine Exception auslösen möchten - Einschränkung
- dynamic_cast funktioniert nur für Objekte von
Klassen mit mindestens einer virtuellen Methode - machen Sie notfalls den Destruktor virtuell
6Explizite Typumwandlungen
- Sofern Ihr Klassen-Design nicht ohne Verwendung
von const_cast auskommt - überprüfen Sie das Design auf mögliche
Alternativen - verwenden Sie ggf. mutable (z.B. bei redundanten
Attributen mit lazy evaluation) - Die Notwendigkeit zur Verwendung von
reinterpret_cast - sollte sich auf hardware-nahen Code beschränken
(z.B. Programmierung vom Embedded Devices oder
Treibern) - kann in sehr generischem Code oft durch die
Verwendung von Templates reduziert werden
7Klassenspezifisch definierte Typumwandlungen
- Konstruktoren mit genau einem Argument vom Typ T
- werden ggf. automatisch zur Umwandlung des Typs T
in die betreffende Klasse angewendet - um diese automatische Anwendung zu vermeiden
können solche Konstruktoren als explicit markiert
werden - Sogenannte Type-Cast-Methoden in der Syntax
operator T() - werden ggf. automatisch zur Umwandlung der
betreffenden Klasse in den Typ T angewendet - um diese automatische Anwendung zu vermeiden sind
stattdessen Methoden der Art T to_T() zu
verwenden
8Vererbung und Komposition
- Bei Vererbung wie bei Komposition
- sind die Datenelemente einer Klasse als Teil in
einer anderen Klasse enthalten - kann die enthaltene Klasse als Basis-Klasse
angegeben werden - Bei Vererbung
- muss die Basis-Klasse public sein
- gilt das Liskovsche Ersetzungsprinzip
- Bei Komposition
- kann die Basisklasse private oder protected sein
- kann statt einer Basisklasse auch ein Attribut
entsprechenden Typs verwendet werden
9Interfaces
- Können als Bündel von Funktionszeigern
verstanden werden - Bei der Definition von Interfaces
- gibt es (anders als in Java) kein spezielles
Konstrukt - sind Klassen mit ausschließlich rein virtuellen
Methoden zu verwenden - Bei der Implementierung von Interfaces
- werden diese als public-Basisklassen verwendet
- gilt (genau wie in Java), dass eine einzelne
Klasse auch mehrere Interfaces auf einmal
implementieren kann
10LSP Liskov Substituion Principle
- Barbara Liskov formulierte folgendes
Ersetzungsprinzip - Ein Objekt einer abgeleiteten Klasse muss überall
dort akzeptabel sein, wo eine seiner Basisklassen
erwartet wird. - In C ist das LSP i.d.R. zur Laufzeit ein
No-Op, da die Attribute der Basisklasse am
Anfang des Datenbereichs der abgeleiteten Klasse
liegen - d.h. der this-Zeiger gilt unverändert für beide
Objekte. - Das LSP gilt nicht in umgekehrter Richtung
- d.h. Basisklassen werden niemals (automatisch)
dort akzeptiert, wo eine abgeleitete Klasse
erwartet wird - sondern erfordern ggf. stets eine explizite
Typumwandlung (Down-Cast) - Auch dieser Down-Cast kann zur Laufzeit ein
No-Op sein - außer im Fall von Mehrfachvererbung
11Vererbung und Überschreiben von Methoden in
abgeleiteten Klassen
- Vererbung kann als Erweiterung verstanden
werden, denn eine abgeleitete Klasse kann - ihrer Basis-Klasse weitere Attribute hinzufügen
- ihrer Basis-Klasse weitere Methoden hinzufügen
- einer geerbten Methode weitere Anweisungen
hinzufügen - Letzteres geht allerdings nur durch Überschreiben
(overriding) - d.h. die abgeleitete Klasse ersetzt die geerbte
Methode durch eine neue ... - ruft dort jedoch die Methode der Basisklasse
auf und - kann jetzt davor und dahinter Anweisungen
hinzufügen
12LSP-Problematikbei Zeigern und Arrays
- C hat von C den engen Zusammenhang zwischen
Zeigern und Arrays übernommen - Zeiger auf Array-Elemente können inkrementiert
werden - und zeigen dann auf das nächste Element
- Es entspricht zumindest in C der üblichen Praxis,
eine Schleife über alle Elemente eines Arrays mit
Zeigern zu implementieren - Durch das LSP
- kann ein Basisklassen-Zeiger jederzeit auf ein
Element in einem Array von abgeleiteten Klassen
verweisen - wird aber falsch inkrementiert, wenn die
abgeleitete Klasse gegenüber der Basisklasse mehr
Speicherplatz benötigt - Das Problem tritt oft etwas verschleiert in
Erscheinung, - wenn ein Array als Parameter an eine Funktion
übergeben wird - wobei technisch gesehen lediglich Zeiger
verwendet werden
13Vor- und Nachbedingungen(Pre- und
Post-Conditions)
- Beim Überschreiben von Methoden ist das LSP zu
beachten - Vorbedingungen dürfen niemals strenger gefasst
sein als die der überschriebenen Methode - Nachbedingungen dürfen niemals schwächer gefasst
sein als die der überschriebenen Methode - Andernfalls würde Code. der für die Basisklasse
korrekt ist, mit der abgeleiteten Klasse nicht
mehr funktionieren - Vor- und Nachbedingungen
- sollten daher für Methoden einer als Basisklasse
entworfenen Klasse ausdrücklich spezifiziert sein
- ansonsten ist beim Überschreiben von Methoden
nicht erkennbar, ob das LSP evtl. verletzt wurde
(möglicherweise unbeabsichtigt)
14Überladen und Überschreiben(Overloading and
Overriding)
- Von Überladen spricht man wenn
- mehrere Methoden (oder globale Funktionen) mit
identischem Namen aber unterschiedlicher Anzahl
bzw. unterschiedlichem Typ von Argumenten
existieren - die beim Aufruf angegebenen Argumente bestimmen,
welche Methode aufgerufen wird - Von Überschreiben spricht man wenn
- eine abgeleitete Klasse eine Methode ihrer
Basisklasse durch eine gleichnamige Methode
ersetzt - hierdurch werden zugleich alle überladenen
Methoden der Basisklasse verdeckt - die abgeleitete Klasse sollte daher ggf. alle
überladenen Methoden überschreiben
15inline vs. normale Methoden
- Methoden (Member-Funktionen von Klassen)
- entsprechen üblicherweise Unterprogrammen
- mit einem zusätzlich (versteckt) übergebenen
Argument (this-Zeiger) - Bei Verwendung von inline
- wird der Methoden-Inhalt (Body) an der
Aufrufstelle direkt eingesetzt - im Unterschied zu Präprozessor Makros erfolgt
dies semantisch korrekt - Normalerweise ergibt sich mit inline
- eine etwas bessere Ausführungsgeschwindigkeit
- aber mehr Bedarf an Speicherplatz (im Code)
- der konkret von der Zahl der Methoden-Aufrufstelle
n abhängt - Im Fall sehr kleiner Methoden kann inline
- deutlich schnelleren Code erzeugen (da bessere
Lokalität) - der im Gesamtumfang sogar kleiner ist
16Compilezeit-Typ und Laufzeit-Typ
- Der Compilezeit-Typ einer Variablen
- ist der aus der Deklaration/Definition
ersichtliche Typ - bestimmt bei Objekten, welche Methoden aufgerufen
werden können - Der Laufzeit-Typ einer Variablen
- kann bei einem Zeiger oder einer Referenz auch
eine vom Compilezeit-Typ abgeleitete Klasse sein
(LSP!) - stimmt ansonsten mit dem Compilezeit-Typ überein
- legt im Falle virtueller Methoden fest, welche
Methode tatsächlich aufgerufen wird - kann bei Bedarf mittels RTTI (Runtime-Type-Informa
tion) ermittelt werden
17Virtuelle Methoden
- Ein großer Teil der Flexibilität
Objektorientierter Programmierung resultiert aus
der Verwendung virtueller Methoden - Sie verschieben externe Fallunterscheidungsketten
in die Klassenhierarchie selbst und - führen damit zu besserer Wartbarkeit und
Erweiterbarkeit - Virtuelle Methoden haben grundsätzlich einen
geringfügigen Overhead - der relativ betrachtet um so mehr ins Gewicht
fällt, je weniger Code die Methode enthält - bei sehr kleinen Methoden ist daher der Vorteil
der flexiblen Erweiterbarkeit gegenüber dem
Geschwindigkeits-Nachteil abzuwägen
18Virtuelle und Methoden und inline
- Der Aufrufmechanismus für virtuelle Methoden
- erlaubt die Auswahl gemäß dem Laufzeit-Typ
- setzt aber den Weg über eine Einsprungtabelle
voraus - insofern muss immer ein Unterprogramm-Sprung
erfolgen - Da sich Compilezeit- und Laufzeit-Typ aber nur
bei Bezugnahme über Zeiger und Referenzen
unterscheiden können - ist der Weg über die Sprungtabelle nicht
erforderlich, wenn das Objekt direkt angesprochen
wird - entfaltet inline in diesem Fall auch bei
virtuellen Methoden seine Wirkung
19Mehrfachvererbung undVirtuelle Basisklassen
- Mehrfachvererbung
- bezeichnet den Fall, dass eine Klasse mehr als
eine Basisklasse hat - ist so lange unproblematisch, wie die
Vererbungslinien nicht wieder in einer
gemeinsamen Basisklasse zusammentreffen - Ist letzteres doch der Fall, wird die gemeinsame
Basisklasse per Default mehrfach enthalten sein
(disjoint) - weshalb das LSP nicht mehr für diese gemeinsame
Basisklasse greift - Virtuelle Basisklassen
- sind die Lösung für den Fall, dass eine
gemeinsame Basisklasse bei Mehrfachvererbung nur
einmal enthalten sein soll (overlapping) - bedingen Overhead durch einen zusätzliche Zeiger
(pro Objekt) in den direkt abgeleiteten Klassen
und eine Indirektionsstufe (bei Zugriff auf
Attribute der virtuellen Basisklasse) - sind in besondere Weise in Initialisierungs-Listen
zu berücksichtigen (Initialisierung muss von der
most derived class ausgehen)
20Runtime-Type-Information (RTTI)
- Mittels dynamic_cast kann ermittelt werden,
- ob der Laufzeit-Typ ggf. wie der in der
Cast-Operation vorgegebene Typ verwendbar wäre - also ob er exakt diesem Typ entspricht
- oder dem einer davon abgeleiteten Klasse
- Die Anwendung ist nur im Zusammenhang mit Klassen
möglich, die wenigstens eine virtuelle Methode
haben - Mittels typeid kann ermittelt werden,
- ob der Laufzeit-Typ exakt einem bestimmten Typ
entspricht - können einige weitere Informationen zum
betreffenden Typ gewonnen werden (z.B. eine
Text-Darstellung) - Die Anwendung ist auch auf die in C enthaltenen
Grundtypen und Klassen ohne virtuelle Methoden
möglich - bezieht sich dann allerdings auf den
Compilezeit-Typ!
21Entwurfsmuster Template Method
- Im Sinne des Open-Close-Principles
- wird hier ein fest vorgegebener Ablauf ( close)
- an vorher festgelegten Stellen mit variabel zu
füllenden Erweiterungspunkten ausgestattet (
open) - Die klassische Implementierung der letzeren
- erfolgt mit Hilfe virtueller Methode
- die von abgeleiteten Klassen nach Bedarf
implementiert werden - Alternativ kann dieses Muster auch
- auf C-Templates zurückgreifen und
- Erweiterungspunkte in einer bei der späteren
Template-Instanziierung anzugebenden Basisklasse
implementieren
22Ressource-Management
- Konstruktoren
- sind verantwortlich für die Bereitstellung von
Ressourcen, die ein Objekt privat (für sich
allein) benötigt - werden bei der Definition des Objekts automatisch
aufgerufen (können also nicht vergessen werden) - Destruktoren
- sind verantwortlich für die Freigabe von
Ressourcen, die ein Objekt privat (für sich
allein) benötigt - werden am Ende der Lebensdauer des Objekts
automatisch aufgerufen (können also nicht
vergessen werden) - Bereitstellung und Freigabe privater Ressourcen
außerhalb von Konstruktoren / Destruktoren ist
fehlerträchtiger und nur in seltenen Fällen
sinnvoll.
23Ressource-Leaks (1)
- Hierunter versteht man u.a. den schleichenden
Verlust an verfügbarem Hauptspeicher, - wenn ein Zeigers zwar mit new initialisiert wird,
- das referenzierte Objekt aber nicht vor Ende der
Lebensdauer des Zeigers mit delete wieder
freigegeben wird - Um Ressource-Leaks im Fall von Exceptions
vorzubeugen - ist sicherzustellen, dass die Freigabe einer
bereits erfolgreich belegten Ressource in jedem
Fall geschieht, - z.B. indem alle Operationen, die möglicherweise
(direkt oder indirekt) ein throw auslösen), in
einen try-Block eingeschlossen werden, - sodass ein nachfolgender catch-Block die Freigabe
vornehmen kann - Ist eine Gruppe von Ressourcen zu belegen
- kann die Anforderung nur eine nach der anderen
geschehen, - womit sich (ohne RAII) verschachtelte try-Blöcke
ergeben
24Ressource-Leaks (2)
- Ein sehr bekanntes Problem, das zu
Ressource-Leaks führen kann, wenn keine
Vorkehrung dagegen getroffen werden, - sind Klassen, die im Konstruktor Speicherplatz
mit new anfordern, - in einem lokalen (Member-) Attribut halten
- bis dieser im Destruktor wieder freigegeben wird.
- Solche Klassen müssen zugleich
- den per Default erzeugten Kopier-Konstruktor und
Zuweisungs-Operator vermeiden - indem entweder entsprechende eigene Methoden
definiert - oder zumindest deklariert und nicht implementiert
werden - C0x erlaubt darüberhinaus das Sperren der per
Default erzeugten Kopier- und Zuweisungs-Operation
en mittels einer speziellen, neuen Syntax
25Ressource-Leaks (3)
- Scheitert die Anforderung einer Ressource in
einem Konstruktor - muss das Problem lokal gelöst werden,
- da der Destruktor für ein Objekt erst dann
freigeschaltet wird, wenn der Konstruktor
vollständig und fehlerfrei sein Ende erreicht hat - Die Behandlung von Problemen bei der
Anforderungen im Konstruktor - führt oft zu geschachtelten try-Blöcken,
- die sich u.U. auch über die MI-Liste erstrecken
müssen - Eine ebenso wirksame aber deutlich elegantere
Lösung bieten Ressource-Wrapper (RAII)
26Vorsichtsmaßnahmen bei der Verwendung von
Auto-Pointern
- Bei der Initialisierung ist sicherzustellen,
- dass ein Zeiger auf frischen ( mit new
angeforderten) Heap-Speicherplatz verwendet wird - der Zeiger darf nicht von new geliefert worden
sein - der Zeiger darf nicht mit dem Adress-Operator
bestimmt worden sein - der Zeiger darf nicht von einem anderen
Auto-Pointer mit get ermittelt worden sein - Zur Übergabe eines Auto-Pointers als Argument an
eine Funktion ist meist eine Referenz sinnvoll - Bei der Wert-Übergabe wird
- die Eigentümerschaft auf den Parameter übertragen
- das referenzierte Objekt mit Ende der Funktion
gelöscht und - der als aktuelles Argument verwendete
Auto-Pointer zum Nullzeiger - Die Rückgabe eines Auto-Pointer in einer
return-Anweisung ist OK und sinnvoll (z.B. aus
Factory-Funktionen/-Methoden)
27Lebensdauer von Objekten
- Globale Objekte und Klassen-Attribute (static
Member) werden - vor dem Start der main-Funktion initialisiert und
- nach dem Ende von main-Funktion aufgeräumt
- Block-lokale static Objekte werden
- direkt vor der ersten Verwendung initialisiert
und - nach dem Ende der main-Funktion aufgeräumt
- Block-lokale automatische Objekte werden
- wenn der Kontrollfluss ihre Definitionsstelle
erreicht initialisiert und - wenn der Kontrollfluss den enthaltenden Block
verlässt aufgeräumt - Auf dem Heap angelegte Objekte
- werden im Rahmen der new-Anweisung initialisiert
und - im Rahmen der delete-Anweisung aufgeräumt
- Sie werden jedoch nicht aufgeräumt, wenn
lediglich die Lebensdauer des auf sie verweisende
Zeigers endet.
28Klasse stdauto_ptr
- Auto-Pointer bieten einen leichtgewichtigen
Ersatz für Zeiger - Sie gehen davon aus, dass sie Eigentümer des
über sie erreichbaren Speicherplatzes sind - ein Konstruktor sorgt für dessen Initialisierung
- ein Destruktor räumt am Ende der Lebensdauer des
Auto-Pointer das dadurch referenzierte Objekt weg - Damit sichergestellt ist, dass immer nur genau
ein Auto-Pointer ein bestimmtes Objekt
bezeichnet, wird - im Kopierkonstruktor der zur Initialisierung
verwendete Auto-Pointer zum Null-Pointer gemacht - im Zuweisungsoperator der auf der rechten Seite
stehende Auto-Pointer zum Null-Pointer gemacht
29Gemischte Verwendung von auto_ptrltTgt und T
- Die get-Methode eines Auto-Pointer
- gibt die Adresse des referenzierten Objekts
zurück - aber der Auto-Pointer ist weiterhin der
Eigentümer, wird also zum Ende seiner Lebensdauer
das referenzierte Objekt löschen - Sinnvoll, um einem Dritten Zugriff auf das
referenzierte Objekt zu geben - Dieser darf den erhaltenen Zeiger nur nicht in
einer langlebigen Variablen speichern - Die release-Methode eines Auto-Pointer
- gibt die Adresse des referenzierten Objekts
zurück - macht den Auto-Pointer in diesem Fall aber zum
Nullzeiger - Sinnvoll, um einem Dritten die Eigentümerschaft
des Objekts zu übertragen - Dieser darf nur nicht vergessen, den über den
erhaltenen Zeiger erreichbaren Speicherplatz
irgendwann mittels delete freizugeben
30Ressource Acquisition is Initialization (RAII)
- Ein u.a. von Bjarne Stroustrup favorisiertes
Idiom, gemäß dem - für Ressourcen mit expliziten Anforderungs- und
-Freigabe-Operation ein Objekt angelegt werden
sollte (Ressource-Wrapper) - das in seinem Konstruktor die Anforderungs-Operati
on und - in seinem Destruktor die Freigabe-Operation
durchführt. - Vorteile eines solchen Ressource-Wrappers sind,
dass die Anforderung/Freigabe einfach und
risikolos - an einen Code-Block gebunden werden kann, indem
dort ein lokales Wrapper-Objekt angelegt wird - an die Lebensdauer eines Objekts gebunden werden
kann, indem dort ein Wrapper-Objekt als Attribut
angelegt wird
31Verwendung von Exceptions
- Die Verwendung der throw-Anweisung im Fehlerfall
entspricht einem go-to auf einen passenden
catch-Block - Es kommen nur catch-Blöcke in Betracht, deren
vorangehender try-Block noch aktiv ist - der Kontrollfluss verzweigt somit grundsätzlich
zurück in Richtung auf main - Passend bedeutet, dass
- der Typ des formalen Parameters im catch-Block
mit dem Typ des Ausdrucks nach throw
übereinstimmt - oder letzterer in ersteren umwandelbar ist, und
zwar nach den selben Regeln wie bei einem
Funktions-Aufruf - Folgen ein und demselben try-Block sowohl
catch-Blöcke für Basisklassen wie auch davon
abgeleiteten Klassen - sind letztere weiter vorne anzuordnen
- sonst werden sie niemals ausgeführt
32Typ des Exception-Objekts
- C macht keine Einschränkungen hinsichtlich des
Typs, der als Exception geworfen wird - Grundtypen (z.B. int oder enum als Fehler-Code)
funktionieren ebenso - wie Zeiger (z.B. const char oder stdstring
als Fehlermeldung) - und Klassen (Standardklassen oder selbst
definierte) - Dennoch ist es ist es empfehlenswert, eigene
Exception-Klassen von der Standard-Klassenhierarch
ie für Exceptions abzuleiten, z.B. - stdlogic_error wenn das Problem durch einen
Programmierfehler verursacht wurde und zur
Beseitigung der Programm-Quelltext geändert und
neu kompiliert werden muss - stdruntime_error wenn das Problem eine äußere
Ursache hat, die zu seiner Beseitigung zu beheben
ist - stdexception Mindestanforderung, damit ein
zentraler catch-Block den what-Text nicht
behandelter Fehler ausgeben kann
33Lebensdauer des Exception-Objekts
- Der bei der throw-Anweisung angegebene Ausdruck
- wird (zumindest formal) kopiert,
- in einen Speicherbereich der auch bei der
Ausführung des catch-Blocks noch zur Verfügung
steht - Um ein nochmaliges Kopieren zu vermeiden
- sollte das Argument des catch-Blocks eine
Referenz sein, und - falls der catch-Block die Exception weiterreichen
muss, lediglich die Anweisung throw (ohne
nachfolgenden Ausdruck) benutzt werden - Die Verwendung von Zeigern als Exception-Objekte
ist - nicht nur überflüssig sonder auch
- unnötig fehlerträchtig
34Performance von Exceptions
- Die Implementierung von Exceptions ist im
ISO/ANSI-Standard von C nicht exakt vorgegeben - typischerweise ist der Code für den Normalfall
( kein throw) ähnlich schnell wie ein return - beim tatsächlichen Auslösen einer Exception
aber sehr viel langsamer
35Overhead von Exceptions