35
Sichere C++- Programmierung Fa. Evosoft Nürnberg Zusammenfassung der vermittelten Programmierrichtlinien

Sichere C++-Programmierung Fa. Evosoft Nürnberg

Embed Size (px)

DESCRIPTION

Sichere C++-Programmierung Fa. Evosoft Nürnberg. Zusammenfassung der vermittelten Programmierrichtlinien. Const-Qualifizierung. Nutzen Sie die const-Qualifizierung für Variablen, deren Wert allein durch die Initialisierung festgelegt wird und sich anschließend nicht mehr ändert - PowerPoint PPT Presentation

Citation preview

Sichere C++-ProgrammierungFa. Evosoft Nürnberg

Zusammenfassung der vermittelten Programmierrichtlinien

Const-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

Zeiger 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

Explizite 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

Explizite 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

Explizite 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

Klassenspezifisch 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

Vererbung 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 Liskov‘sche Ersetzungsprinzip

• Bei Komposition– kann die Basisklasse private oder protected sein– kann statt einer Basisklasse auch ein Attribut

entsprechenden Typs verwendet werden

Interfaces

• 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

LSP – 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

Vererbung 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

LSP-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

Vor- 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)

Ü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

„inline“ 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-Aufrufstellen abhängt

• Im Fall sehr kleiner Methoden kann „inline“ – deutlich schnelleren Code erzeugen (da bessere „Lokalität“) – der im Gesamtumfang sogar kleiner ist

Compilezeit-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-Information)

ermittelt werden

Virtuelle 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

Virtuelle 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

Mehrfachvererbung 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)

Runtime-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!

Entwurfsmuster: 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

Ressource-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.

Ressource-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

Ressource-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– C++0x erlaubt darüberhinaus das „Sperren“ der per Default

erzeugten Kopier- und Zuweisungs-Operationen mittels einer speziellen, neuen Syntax

Ressource-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)

Vorsichtsmaß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)

Lebensdauer 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.

Klasse std::auto_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

Gemischte Verwendung von auto_ptr<T> 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

Ressource 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-Operation 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

Verwendung 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

Typ 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 std::string als

Fehlermeldung)– und Klassen (Standardklassen oder selbst definierte)

• Dennoch ist es ist es empfehlenswert, eigene Exception-Klassen von der Standard-Klassenhierarchie für Exceptions abzuleiten, z.B.– std::logic_error – wenn das Problem durch einen

Programmierfehler verursacht wurde und zur Beseitigung der Programm-Quelltext geändert und neu kompiliert werden muss

– std::runtime_error – wenn das Problem eine äußere Ursache hat, die zu seiner Beseitigung zu beheben ist

– std::exception – Mindestanforderung, damit ein zentraler catch-Block den what-Text nicht behandelter Fehler ausgeben kann

Lebensdauer 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

Performance 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

Overhead von Exceptions