1 / 35

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

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

Download Presentation

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

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Sichere C++-ProgrammierungFa. Evosoft Nürnberg Zusammenfassung der vermittelten Programmierrichtlinien

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

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

  4. 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 dannals „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

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

  6. 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 „lazyevaluation“) • 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

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

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

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

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

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

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

  13. 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)

  14. Überladen und Überschreiben(OverloadingandOverriding) • 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

  15. „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

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

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

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

  19. 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 „mostderived class“ ausgehen)

  20. 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!

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

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

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

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

  25. 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)

  26. 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)

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

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

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

  30. Ressource AcquisitionisInitialization (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

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

  32. 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. constchar *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

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

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

  35. Overhead von Exceptions

More Related