Es gibt keinen Vorteil, den ich mir vorstellen kann (siehe Anmerkung zu JasonS unten), eine Codezeile als Funktion oder Unterroutine aufzuschließen. Außer vielleicht, dass Sie der Funktion einen "lesbaren" Namen geben können. Sie können die Zeile aber genauso gut kommentieren. Und da das Einschließen einer Codezeile in eine Funktion Codespeicher, Stapelspeicher und Ausführungszeit kostet, scheint es mir das meiste zu sein kontraproduktiv ist. In einer Unterrichtssituation? Es könnte einen Sinn ergeben. Das hängt jedoch von der Klasse der Schüler, ihrer Vorbereitung im Voraus, dem Lehrplan und dem Lehrer ab. Meistens denke ich, dass es keine gute Idee ist. Aber das ist meine Meinung.
Das bringt uns zum Endergebnis. Ihr breiter Fragenbereich ist seit Jahrzehnten umstritten und bleibt bis heute umstritten. Zumindest wenn ich Ihre Frage lese, scheint es mir eine meinungsbasierte Frage zu sein (wie Sie sie gestellt haben).
Wenn Sie die Situation detaillierter beschreiben und die Ziele, die Sie als vorrangig erachtet haben, sorgfältig beschreiben, könnte dies dazu führen, dass Sie nicht mehr so meinungsbasiert wie bisher sind. Je besser Sie Ihre Messinstrumente definieren, desto objektiver können die Antworten sein.
Im Großen und Ganzen möchten Sie für jede Codierung Folgendes tun . (Im Folgenden werde ich davon ausgehen, dass wir verschiedene Ansätze vergleichen, mit denen alle Ziele erreicht werden. Offensichtlich ist jeder Code, der die erforderlichen Aufgaben nicht ausführt, schlechter als erfolgreicher Code, unabhängig davon, wie er geschrieben wurde.)
- Seien Sie konsequent in Bezug auf Ihren Ansatz, damit ein erneutes Lesen Ihres Codes ein Verständnis dafür entwickeln kann, wie Sie Ihren Codierungsprozess angehen. Inkonsistent zu sein ist wahrscheinlich das schlimmste Verbrechen. Das macht es nicht nur anderen schwer, sondern auch Ihnen, Jahre später zum Code zurückzukehren.
- Versuchen Sie nach Möglichkeit, die Dinge so anzuordnen, dass die Initialisierung verschiedener Funktionsabschnitte unabhängig von der Bestellung durchgeführt werden kann. Wo eine Bestellung erforderlich ist, wenn es sich um eine enge Kupplung handelt zweier in hohem Maße verwandter Unterfunktionen eine einzige Initialisierung für beide in Betracht ziehen, damit die Reihenfolge geändert werden kann, ohne Schaden zu verursachen. Wenn dies nicht möglich ist, dokumentieren Sie die Anforderungen für die Initialisierungsreihenfolge.
- Encapsulate KnowledgeWenn möglich, an genau einem Ort. Konstanten sollten nicht überall im Code dupliziert werden. Gleichungen, die für eine Variable aufgelöst werden, sollten nur an einer Stelle vorhanden sein. Und so weiter. Wenn Sie feststellen, dass Sie eine Reihe von Zeilen kopieren und einfügen, die an verschiedenen Stellen das erforderliche Verhalten aufweisen, sollten Sie überlegen, wie Sie dieses Wissen an einem Ort erfassen und bei Bedarf verwenden können. Wenn Sie beispielsweise eine Baumstruktur haben, die auf eine bestimmte Weise begangen werden muss, tun Sie dies nichtReplizieren Sie den Tree-Walking-Code an jeder Stelle, an der Sie die Baumknoten durchlaufen müssen. Erfassen Sie stattdessen die Tree-Walking-Methode an einem Ort und verwenden Sie sie. Auf diese Weise haben Sie nur einen Grund zur Sorge, wenn sich der Baum und die Laufmethode ändern, und der gesamte Rest des Codes funktioniert einfach.
- Wenn Sie alle Ihre Routinen auf ein großes, flaches Blatt Papier verteilen und die Pfeile so verbinden, wie sie von anderen Routinen aufgerufen werden, werden Sie in jeder Anwendung feststellen, dass es "Cluster" von Routinen mit vielen, vielen Pfeilen gibt untereinander aber nur wenige pfeile ausserhalb der gruppe. Es wird also natürliche Grenzen eng gekoppelter Routinen und lose gekoppelter Verbindungen zwischen anderen Gruppen eng gekoppelter Routinen geben. Verwenden Sie diese Tatsache, um Ihren Code in Module zu organisieren. Dies wird die scheinbare Komplexität Ihres Codes erheblich einschränken.
Das oben Gesagte gilt generell für alle Codierungen. Ich habe die Verwendung von Parametern, lokalen oder statischen globalen Variablen usw. nicht erörtert. Der Grund dafür ist, dass der Anwendungsbereich für die eingebettete Programmierung häufig extreme und sehr bedeutende neue Einschränkungen enthält und es unmöglich ist, alle zu erörtern, ohne jede eingebettete Anwendung zu erörtern. Und das passiert hier sowieso nicht.
Diese Einschränkungen können eine oder mehrere der folgenden sein:
- Starke Kosteneinschränkungen, die extrem einfache MCUs mit minimalem RAM und fast keiner Anzahl von E / A-Pins erfordern. Für diese gelten ganz neue Regeln. Beispielsweise müssen Sie möglicherweise in Assemblycode schreiben, da nicht viel Codespeicherplatz vorhanden ist. Möglicherweise müssen Sie NUR statische Variablen verwenden, da die Verwendung lokaler Variablen zu kostspielig und zeitaufwendig ist. Möglicherweise müssen Sie die übermäßige Verwendung von Unterprogrammen vermeiden, da (z. B. bei einigen Microchip PIC-Teilen) nur 4 Hardwareregister zum Speichern von Unterprogramm-Rücksprungadressen vorhanden sind. Daher müssen Sie Ihren Code möglicherweise dramatisch "reduzieren". Etc.
- Schwerwiegende Leistungsbeschränkungen erfordern sorgfältig ausgearbeiteten Code, um die meisten MCUs zu starten und herunterzufahren, und beschränken die Ausführungszeit des Codes erheblich, wenn er mit voller Geschwindigkeit ausgeführt wird. Wiederum kann dies manchmal eine gewisse Assembler-Codierung erfordern.
- Strenge Timing-Anforderungen. Zum Beispiel gab es Zeiten, in denen ich sicherstellen musste, dass die Übertragung einer Open-Drain-0 GENAU die gleiche Anzahl von Zyklen wie die Übertragung einer 1 dauern musste. Und dass das Abtasten derselben Leitung auch durchgeführt werden musste mit einer genauen relativen Phase zu diesem Zeitpunkt. Dies bedeutete, dass C hier NICHT verwendet werden konnte. Die EINZIGE Möglichkeit, diese Garantie zu geben, besteht darin, den Montagecode sorgfältig zu erstellen. (Und selbst dann nicht immer bei allen ALU-Designs.)
Und so weiter. (Der Verkabelungscode für lebenswichtige medizinische Instrumente hat auch eine eigene Welt.)
Das Fazit ist, dass eingebettetes Coding oft nicht für alle kostenlos ist und Sie wie auf einer Workstation codieren können. Es gibt oft schwerwiegende Wettbewerbsgründe für eine Vielzahl sehr schwieriger Sachzwänge. Und diese mögen stark gegen die eher traditionellen und gängigen Antworten sprechen .
In Bezug auf die Lesbarkeit finde ich, dass Code lesbar ist, wenn er in einer konsistenten Weise geschrieben ist, die ich beim Lesen lernen kann. Und wo es keinen absichtlichen Versuch gibt, den Code zu verschleiern. Es ist wirklich nicht viel mehr erforderlich.
Lesbarer Code kann sehr effizient sein und alle oben genannten Anforderungen erfüllen. Die Hauptsache ist, dass Sie genau verstehen, was jede von Ihnen geschriebene Codezeile auf Assembly- oder Maschinenebene erzeugt, während Sie sie codieren. C ++ stellt hier eine ernsthafte Belastung für den Programmierer dar, da es viele Situationen gibt, in denen identische C ++ - Code- Snippets tatsächlich verschiedene Code-Snippets erzeugen, die eine sehr unterschiedliche Leistung aufweisen. Aber C ist im Allgemeinen meistens eine "was Sie sehen, ist was Sie bekommen" Sprache. In dieser Hinsicht ist es also sicherer.
EDIT pro JasonS:
Ich benutze C seit 1978 und C ++ seit ungefähr 1987 und ich habe viel Erfahrung mit beiden für Großrechner, Minicomputer und (meistens) eingebettete Anwendungen.
Jason bringt einen Kommentar zur Verwendung von 'inline' als Modifikator vor. (Aus meiner Sicht ist dies eine relativ "neue" Funktion, da sie unter Verwendung von C und C ++ nur für die Hälfte meines Lebens oder länger existierte.) Die Verwendung von Inline-Funktionen kann solche Aufrufe tatsächlich ausführen (sogar für eine Zeile von Code) ganz praktisch. Und es ist weitaus besser, wenn möglich, als ein Makro zu verwenden, da der Compiler die Eingabe anwenden kann.
Es gibt aber auch Einschränkungen. Das erste ist, dass Sie sich nicht darauf verlassen können, dass der Compiler "den Hinweis aufnimmt". Es kann oder kann nicht. Und es gibt gute Gründe, den Hinweis nicht zu verstehen. (Ein offensichtliches Beispiel: Wenn die Adresse der Funktion verwendet wird, muss die Funktion instanziiert werden, und die Verwendung der Adresse zum Tätigen des Anrufs erfordert einen Anruf. Der Code kann dann nicht in die Zeile eingefügt werden.) Es gibt auch aus anderen gründen. Compiler können eine Vielzahl von Kriterien haben, anhand derer sie beurteilen, wie sie mit dem Hinweis umgehen sollen. Und als Programmierer müssen Sie dies tunNehmen Sie sich etwas Zeit, um sich mit diesem Aspekt des Compilers vertraut zu machen. Andernfalls können Sie Entscheidungen treffen, die auf fehlerhaften Ideen beruhen. Dies belastet sowohl den Schreiber des Codes als auch jeden Leser und jeden, der plant, den Code auf einen anderen Compiler zu portieren.
Außerdem unterstützen C- und C ++ - Compiler die separate Kompilierung. Dies bedeutet, dass sie einen Teil des C- oder C ++ - Codes kompilieren können, ohne anderen zugehörigen Code für das Projekt zu kompilieren. Um Code einzufügen, muss der Compiler nicht nur die Deklaration "in scope" haben, sondern auch die Definition. Normalerweise stellen Programmierer sicher, dass dies der Fall ist, wenn sie "Inline" verwenden. Aber es ist leicht, dass sich Fehler einschleichen.
Im Allgemeinen gehe ich davon aus, dass ich mich nicht darauf verlassen kann, obwohl ich Inline auch dort verwende, wo ich es für angemessen halte. Wenn Leistung eine wesentliche Anforderung ist und ich denke, das OP hat bereits klar geschrieben, dass es einen erheblichen Leistungseinbruch gab, als es auf eine "funktionalere" Route ging, dann würde ich es mit Sicherheit vermeiden, mich auf Inline als Codierungspraxis zu verlassen und würde stattdessen einem etwas anderen, aber völlig konsistenten Muster beim Schreiben von Code folgen.
Eine abschließende Anmerkung zu 'Inline' und Definitionen, die für einen separaten Kompilierungsschritt "im Geltungsbereich" sind. Es ist möglich (nicht immer zuverlässig), dass die Arbeit in der Verbindungsphase ausgeführt wird. Dies kann nur dann der Fall sein, wenn ein C / C ++ - Compiler genügend Details in die Objektdateien einfügt, damit ein Linker auf Inline-Anforderungen reagieren kann. Ich persönlich habe kein Linker-System (außerhalb von Microsoft) erlebt, das diese Funktion unterstützt. Aber es kann vorkommen. Auch hier hängt es von den Umständen ab, ob darauf vertraut werden sollte oder nicht. Aber ich gehe normalerweise davon aus, dass dies nicht auf den Linker geschaufelt wurde, sofern ich aufgrund guter Beweise nichts anderes weiß. Und wenn ich mich darauf verlasse, wird es an prominenter Stelle dokumentiert.
C ++
Hier ist ein Beispiel dafür, warum ich beim Codieren von eingebetteten Anwendungen C ++ trotz der sofortigen Verfügbarkeit von C ++ ziemlich vorsichtig bin. Ich werde ein paar Begriffe werfen, dass ich denke , alle Embedded C ++ Programmierer müssen wissen , kalt :
- Teilschablonenspezialisierung
- vtables
- virtuelles Stammobjekt
- Aktivierungsrahmen
- Aktivierungsframe abwickeln
- Verwendung intelligenter Zeiger in Konstruktoren und warum
- Rückgabewertoptimierung
Das ist nur eine kurze Liste. Wenn Sie noch nicht alles über diese Begriffe wissen und wissen, warum ich sie aufgelistet habe (und viele andere, die ich hier nicht aufgelistet habe), rate ich von der Verwendung von C ++ für eingebettete Arbeit ab, es sei denn, dies ist keine Option für das Projekt .
Werfen wir einen kurzen Blick auf die C ++ - Ausnahmesemantik, um nur einen Vorgeschmack zu erhalten.
EINB
EIN
.
.
foo ();
String s;
foo ();
.
.
EIN
B
Der C ++ - Compiler sieht den ersten Aufruf von foo () und kann nur zulassen, dass ein normaler Aktivierungsrahmen abgewickelt wird, wenn foo () eine Ausnahme auslöst. Mit anderen Worten, der C ++ - Compiler weiß, dass an dieser Stelle kein zusätzlicher Code erforderlich ist, um den bei der Ausnahmebehandlung auszuführenden Frame-Unwind-Prozess zu unterstützen.
Sobald jedoch ein String s erstellt wurde, weiß der C ++ - Compiler, dass er ordnungsgemäß zerstört werden muss, bevor ein Frame-Unwind zugelassen werden kann, falls später eine Ausnahme auftritt. Der zweite Aufruf von foo () unterscheidet sich also semantisch vom ersten. Wenn der zweite Aufruf von foo () eine Ausnahme auslöst (was er möglicherweise tut oder nicht), muss der Compiler Code platziert haben, der für die Zerstörung von Strings vorgesehen ist, bevor der normale Frame abgewickelt werden kann. Dies unterscheidet sich von dem Code, der für den ersten Aufruf von foo () erforderlich ist.
(Es ist möglich, zusätzliche Dekorationen in C ++ hinzuzufügen , um dieses Problem zu begrenzen. Tatsache ist jedoch, dass Programmierer, die C ++ verwenden, die Auswirkungen jeder von ihnen geschriebenen Codezeile viel besser kennen müssen.)
Im Gegensatz zu Cs malloc signalisiert C ++ 's new mithilfe von Ausnahmen, wenn keine Zuweisung von Rohspeicher möglich ist. So wird 'dynamic_cast'. (Die Standardausnahmen in C ++ finden Sie in Stroustrups 3. Ausgabe, Die C ++ - Programmiersprache, Seite 384 und 385.) Durch Compiler kann dieses Verhalten möglicherweise deaktiviert werden. Im Allgemeinen entstehen Ihnen jedoch aufgrund ordnungsgemäß ausgebildeter Ausnahmebehandlungs-Prologe und -Epiloge im generierten Code zusätzliche Kosten, selbst wenn die Ausnahmen tatsächlich nicht stattfinden und selbst wenn die zu kompilierende Funktion keine Ausnahmebehandlungsblöcke enthält. (Stroustrup hat dies öffentlich beklagt.)
Ohne eine teilweise Template-Spezialisierung (nicht alle C ++ - Compiler unterstützen sie) kann die Verwendung von Templates für die eingebettete Programmierung eine Katastrophe bedeuten. Ohne sie ist Code Bloom ein ernstes Risiko, das ein Embedded-Projekt mit kleinem Arbeitsspeicher blitzschnell zum Erliegen bringen könnte.
Wenn eine C ++ - Funktion ein Objekt zurückgibt, wird ein unbenannter Compiler temporär erstellt und zerstört. Einige C ++ - Compiler können effizienten Code bereitstellen, wenn in der return-Anweisung anstelle eines lokalen Objekts ein Objektkonstruktor verwendet wird, wodurch der Konstruktions- und Zerstörungsaufwand für ein Objekt verringert wird. Aber nicht jeder Compiler tut dies, und viele C ++ - Programmierer sind sich dieser "Rückgabewertoptimierung" nicht einmal bewusst.
Das Bereitstellen eines Objektkonstruktors mit einem einzigen Parametertyp kann es dem C ++ - Compiler ermöglichen, einen Konvertierungspfad zwischen zwei Typen auf völlig unerwartete Weise für den Programmierer zu finden. Diese Art von "klugem" Verhalten ist nicht Teil von C.
Eine catch-Klausel, die einen Basistyp angibt, "schneidet" ein geworfenes abgeleitetes Objekt, da das geworfene Objekt unter Verwendung des "statischen Typs" der catch-Klausel und nicht des "dynamischen Typs" des Objekts kopiert wird. Eine nicht seltene Ursache für Ausnahmefehler (wenn Sie der Meinung sind, dass Sie sich sogar Ausnahmen in Ihrem eingebetteten Code leisten können).
C ++ - Compiler können automatisch Konstruktoren, Destruktoren, Kopierkonstruktoren und Zuweisungsoperatoren mit unbeabsichtigten Ergebnissen für Sie generieren. Es braucht Zeit, um sich mit den Details vertraut zu machen.
Das Übergeben von Arrays abgeleiteter Objekte an eine Funktion, die Arrays von Basisobjekten akzeptiert, generiert selten Compiler-Warnungen, führt jedoch fast immer zu falschem Verhalten.
Da C ++ den Destruktor von teilweise konstruierten Objekten nicht aufruft, wenn eine Ausnahme im Objektkonstruktor auftritt, erfordert die Behandlung von Ausnahmen in Konstruktoren normalerweise "intelligente Zeiger", um sicherzustellen, dass konstruierte Fragmente im Konstruktor ordnungsgemäß zerstört werden, wenn dort eine Ausnahme auftritt . (Siehe Stroustrup, Seite 367 und 368.) Dies ist ein häufiges Problem beim Schreiben guter Klassen in C ++, wird jedoch in C natürlich vermieden, da in C keine Semantik für Konstruktion und Zerstörung eingebaut ist von Unterobjekten innerhalb eines Objekts bedeutet das Schreiben von Code, der mit diesem einzigartigen semantischen Problem in C ++ fertig werden muss; mit anderen Worten "herumschreiben" von semantischen C ++ - Verhaltensweisen.
C ++ kopiert möglicherweise Objekte, die an Objektparameter übergeben werden. In den folgenden Fragmenten wird beispielsweise der Aufruf "rA (x);" kann dazu führen, dass der C ++ - Compiler einen Konstruktor für den Parameter p aufruft, um dann den Kopierkonstruktor aufzurufen, um das Objekt x an den Parameter p zu übertragen kopiert von Parameter p. Schlimmer noch, wenn Klasse A ihre eigenen Objekte hat, die gebaut werden müssen, kann dies katastrophale Folgen haben. (AC-Programmierer würden den größten Teil dieses Mülls vermeiden, da C-Programmierer keine so praktische Syntax haben und alle Details einzeln ausdrücken müssen.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Zum Schluss noch eine kurze Anmerkung für C-Programmierer. longjmp () hat in C ++ kein portierbares Verhalten. (Einige C-Programmierer verwenden dies als eine Art "Ausnahmemechanismus".) Einige C ++ - Compiler werden tatsächlich versuchen, die Dinge so einzurichten, dass sie bereinigt werden, wenn longjmp verwendet wird, aber dieses Verhalten ist in C ++ nicht übertragbar. Wenn der Compiler erstellte Objekte bereinigt, ist er nicht portierbar. Wenn der Compiler sie nicht bereinigt, werden die Objekte nicht zerstört, wenn der Code aufgrund des longjmp den Bereich der erstellten Objekte verlässt und das Verhalten ungültig ist. (Wenn die Verwendung von longjmp in foo () keinen Bereich hinterlässt, ist das Verhalten möglicherweise in Ordnung.) Dies wird von C-Embedded-Programmierern nicht allzu oft verwendet, sie sollten sich jedoch vor der Verwendung über diese Probleme im Klaren sein.