Hallo zusammen, bis jetzt hab ich sehr einfach Software einfach so vor mich hin programmiert (ohne vorher gross zu überlegen oder pläne zu zeichnen). Aber wie geht man vor wenn die Software etwas komplizierter wird? Gibt ja schnell sehr viele Fehlerquellen. Habt ihr ein paar Tipps? Gibt es irgendwelche Grundlegende sachen dir man beachten sollte? Viel if else benutzen oder wenig. Viel ineinander verschachteln oder möglichst wenig. Viele verschachtelungen sollte man maximal benützen? Wann benutzt man State Machines? Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein? Zeichnet ihr Struktorgramme oder einfach Kommentare? Wann verwendet man C++ bei uCs? Und zum Schluss, habt ihr noch eine Buchempfehlung für C speziell für uCs? Danke und schönen Abend C-Master
Die wichtigsten Dinge: - Main Loop immer schön klein halten Hier so wenig Funktionalität wie möglich unterbringen und diese hinter klingenden Funktionsaufrufen verbergen. - Selbsterklärende Namen für Variablen und Funktionen wählen, ganz egal wie lang diese werden. - So wenig globale Variablen wie möglich, dies sollte die absolute Ausnahme sein! - Selbsterklärenden Code schreiben Viele hier versuchen immer noch das kleinste Quäntchen an Performance herauszukitzeln. Dies geht oft zu Lasten der Lesbarkeit, Wartbarkeit und Portierbarkeit und ist (fast) nie nötig. - Zustandsautomaten sind ein sehr gutes Stichwort Diese Dinger sind ein Garant für guten Code, weil man von Anfang an gezwungen wird alles nach Lehrbuch zu machen. Hier erstellt man zuerst das Konzept, und dann erst den Code. Das Konzept ist gleichzeitig die beste Doku. Bei der Implementierung gibt es keine Fehler, da das vorher erstellte Zustandsdiagramm eindeutig ist. War das Konzept richtig, dann funktioniert das Programm auch. Grundsätzlich sollte man sich bei jeder Aufgabe fragen, ob diese sich sinnvoll mit einem Automaten lösen lässt.
Thomas wrote: > - So wenig globale Variablen wie möglich, dies sollte die absolute > Ausnahme sein! > Wie macht Ihr es dann, wenn Ihr Zustände o.ä. in mehreren Funktionenen benötigt? Ein Beispiel: Meine Rolladensteuerung - Ich benötige Uhrzeit, Schaltzeiten etc. in vielen Funktionen. Daher habe ich diese als globale Variablen implementiert. Die Alternative wäre, alle diese Werte als Variablen in Funktionsaufrufen zu übergeben. Dies würde m.E. aber zu großen Overhead bei Funktionsaufrufen führen ... Was macht man hier? Ist es Zielführend alle mehrfach benötigten Variablen in ein globes Struct zu packen? Damit hätte man dann nur eine globale Variable ? Also wie macht Ihrs ? Gruß Andreas
Alle nötigen Zustandsvariablen in einer Struktur zusammenfassen und dann einen Pointer auf diese Struktur den einzelnen Funktionen übergeben (=Handle auf den Kontext).
Wieso eigentlich keine globalen Variablen? Gibts da einen vernünftigen Grund?
Der Globale Namensbereich ist begrenzt. Häufig hat man ja auch "status" Variablen, die dann mehrfach vorkommen, dann gibt es Kollisionen, also einfach in ne struct und die struct nach dem Modul benennen. Auch sehr hilfreich, die sache mit dem Kontextpointer, dann kann man den selben Code auch mehrfach benutzen (z.B. Filteranwndungen, ...). Meines Erachtens geht Leistung nicht gegen Lesbarkeit. Ich habe erst letzten Monat ein in C geschriebenes Modul (sehr gut lesbar, 100% portierbar), in ASM umgesetzt und nur etwa 40% eingespart. Das meisste natürlich beim Push/Pop (Interrupts). Interrupts sind eh ein gutes Stichwort: ich habe gute Erfahrung gemacht, dieses moeglichst klein zu halten, also wenig Funktionalität dort reinzubringen. Wenn man dann noch ausschließlich Inline Funktionen dort verwendet, kommen auch brauchbar schnelle und kleine Binärdaten aus dem Compiler raus.
> Wieso eigentlich keine globalen Variablen? Gibts da einen vernünftigen > Grund? Der Hauptgrund ist der, dass bei globalen Variablen schwer nachvollziehbar ist, woher sie ihre Inhalte stammen. Um das herauszufinden, muss man den Quellcode des gesamten Programms nach Funktionen durchscannen, die die eine bestimmte Variable beschreiben, anschließend nachschauen, welche anderen Funktionen diese Funktionen aufrufen usw. Problematisch wird es vor allem dann, wenn globale Variablen an sehr vielen Stellen im Programm beschrieben werden. Bei lokalen Variablen muss hingegen nur eine Funktion durchgelesen werden. Selbst wenn Werte als Argumente an eine Funktion übergeben werden, ist die Suche nach ihrem Ursprung immer noch mit begrenztem Aufwand möglich. Es ist aber überhaupt kein Problem, Uboot-Stockis Uhrzeitvariable global zu machen, wenn aus Namensgebung und Dokumentation klar wird, dass sie nur an einer Stelle, nämlich vielleicht in einem zyklisch aufgerufenen Interrupthandler beschrieben wird. Würde man diese als Argument über mehrere Aufrufebenen durchreichen, würde die Lesbarkeit sogar schlechter, weil dabei nicht sofort klar wird, dass es sich bei den Argumenten in Wirklichkeit immer um Kopien der gleichen Variable handelt. Es ist ebenfalls überhaupt kein Problem, eine Satz Parameter global zu machen, wenn klar ist, dass diese einmal beim Programmstart initialisiert (bspw. aus einem EEPROM gelesen) werden, und danach kaum noch verändert werden. Idealerweise packt man all diese Variablen in eine Struktur, um damit auszudrücken, dass ihre Inhalte alle aus der gleichen Quelle stammen. Bei der Übergabe von Werten zwischen Interrupthandlern und anderen Programmteilen führt sowieso kein sinnvoller Weg an globalen Variablen vorbei. Aber auch hier kann man die Lesbarkeit verbessern, wenn man jeweils alle globalen Variablen eines Interrupthandlers zu einer Struktur (bspw. timer_irqvars, uart_irqvars usw.) zusammenfasst. Globale Variablen sind also nicht böse, genauso wenig wie Gotos. Man muss sie nur mit Bedacht einsetzen.
yalu wrote: > Globale Variablen sind also nicht böse, genauso wenig wie Gotos. Man > muss sie nur mit Bedacht einsetzen. Hi yalu, bei dem meisten was du von dir lässt nicke ich oft und gerne, wie auch hier bei den globalen Variablen, aber bei gotos muss ich dir widersprechen. Gotos führen zu Sphagetti Code und es gibt immer eine Möglichkeit diese zu vermeiden.
Sehe ich ähnlich, gotos habe ich in den letzten 15 Jahren Programmierung nicht mehr gebraucht. Ich glaub auch nicht, dass mein Programmcode dadurch vorteilhafter geworden wäre. Bin aber auch ein Gegner vom blinden Befolgen irgendwelcher Regeln. Wenn jemand den sinnvollen Einsatz von Gotos findet, darf er die gerne einsetzen.
Die Verteufelung von goto in C ist wahrscheinlich so alt wie die Sprachdefintion. Sicher, man kann gotos immer irgendwie vermeiden und es gibt einige übergreifende oder in-house Richtlinien, die die Verwendung von goto auch verbieten. Aber vor allem in Funktionen mit vielen Verschachtelungen, bei denen z.B. sehr viele mögliche Fehlerzustände abgefragt werden, vermeiden machmal "mit Bedacht" eingesetzte label und ein paar gotos ausufernde und unübersichtliche if-else Konstrukte und manchmal auch die meiner Meinung nach viel fehleranfälligeren innerhalb einer Funktion verstreuten return fehlercodeX (mehr als ein return in einer Funktion wird in zumindest in zwei mir bekannten Richtlinien ebenfalls untersagt). Das ist dann im Prinzip ein realtiv übersichtliches, im Vergleich zu C++ allerdings recht primitives, exception-handling. Nur meine 0,02EUR dazu; Diskussionen zu goto in C füllen wahrscheinlich inzwischen zehntausende von Foren-, Mailinglist-Threads und Internetseiten.
goto ist so eine Sache. Ich kannte dies anfangs garnicht, (was vielleicht auch gut so war), aber mittlerweile nutze ich es ab und zu. Es gibt ein paar Fälle in denen es durchaus brauchbar ist: z.B. wenn man mehrere Schleifen auf einmal verlassen möchte. OK, man kann es auch anderst machen indem man eine Variable setzt, die dann in allen Schleifen nacheinander ein break auslöst, oder gleich ein return um die komplette Funktion zu verlassen. Manchmal hat goto aber ein paar kleine Vorteile.
Mir ging es ähnlich, ich kannte goto noch aus basic, war mir aber lange nicht bewusst das ich damit auch in c arbeiten kann, heute bin ich froh das ich es nicht von anfang an kannte. Was ich häufiger sehe ist das gotos meistens schlechten Code unterstützen, weil man sich vorher keine Gedanken gemacht hat. Ein Beispiel was mir spontan einfällt ist das Rausspringen aus irgendwelchen Such-Schleifen, bei einfachen Konstruktionen tut es ein break, bei verschachtelten Angelegenheiten packt man das Ganze einfach in eine Unterfunktion. Wenn man von vornerein gotos gedanklich ausblendet wird der Code meistens strukturierter. Im Idealfall hat man also dann eine Suchfunktion die als Rückgabewert einen Pointer auf die Gesuchte Stelle oder NULL enthält. Oder wenn nur das Enthalten eines Wertes in einem Feld interessiert, ist ein if (beinhaltet(Feld, x)) {}; auch besser lesbar als irgendwelche Sprünge. Das schlimmste Negativ Beispiel was ich bisher gesehen hab war ein Code mit etwa 800 Zeilen und alles in der main, dafür aber massig gotos... (Funktionen aus Standardheadern mal ausgenommen.)
Erinnert mich an den alten Spruch: “Real Programmers can write FORTRAN programs in any language.” Sprich: man kann alles, was es gibt, immer gehörig missbrauchen (und C setzt dem Missbrauch wenig Schranken). Sinnvoll benutzt, ist aber oft nichts dagegen einzuwenden. Gerade bei sehr kleinen Controllern können globale Variablen u. U. zu einfacherem (und kleinerem) Code führen, und eine fehlende Übersichtlichkeit ist dort nicht das Problem, was es bei einem Projekt auf einem Controller mit 64 und mehr KiB an Code sein kann. Gleichermaßen bringt ein "goto", das fünf Klammerungsebenen einspart, ggf. mehr Übersichtlichkeit in das Projekt, als man bei strikter Meidung dieses Worts erreichen kann.
Was die Globalen Variablen angeht, bastel ich auch bei Kleinstprojekten jeden Funktionsblock (Hauptprogramm, Timer, UART, etc.) in jeweils eigene C Files und binde sie bei Bedarf in andere Programme wieder ein, damit erledigen sich die meisten globalen Variablen von selbst.
Für was brauchts eigentlich Pointer?? Das hab ich nie gecheckt. Man kann doch gleich die Variable übergeben! Bei Java wurde das doch auch abgeschafft.
panam wrote: > Für was brauchts eigentlich Pointer?? Das hab ich nie gecheckt. Man kann > doch gleich die Variable übergeben! Das ist dann aber eine Einbahnstrasse in C. Der Wert geht zwar in die Funktion hinein, aber eine Veränderung des Wertes in einer Funktion führt nicht zu einer Veränderung der aufrufenden Variablen des Aufrufers. Wenn du das so willst: super, genau das was du brauchst. Du kannst nach Herzenslust innerhalb der Funktion mit dem Argument fuhrwerken, ohne das du dem Aufrufer etwas zerstörst. Wenn du aber haben willst, dass eine Änderung an einem Argument auch für den Aufrufer sichtbar ist, dann geht das in c nur, in dem der Aufrufer die Adresse übergibt (aka. einen Pointer) an der diese Änderung gemacht werden soll. Die andere Spielwiese für Pointer sind: dynamische Datenstrukturen. Also Arrays, Listen, Maps, Strings die zur Laufzeit bei Bedarf anwachsen und kleiner werden können. Pointer sind ganz einfach: Das sind Variablen, der Inhalt eine Speicheradresse ist. So wie ein int eine ganze Zahl aufnimmt, ein double eine Fliesskommazahl, so nimmt eine Pointer-Variable eine Speicheradresse auf. Mehr steckt da nicht dahinter :-) > Bei Java wurde das doch auch > abgeschafft. Auch in Java gibt es noch Pointer. Nur heissen sie dort AFAIK Referenzen.
panam wrote: > Für was brauchts eigentlich Pointer?? Das hab ich nie gecheckt. Man kann > doch gleich die Variable übergeben! Bei Java wurde das doch auch > abgeschafft. Jein, in Java und C++ gibts Referenzen, das ist praktisch ein stark vereinfachter Pointer. Wenn man einen Pointer übergibt, bleiben Änderungen an der Variablen erhalten da direkt der Inhalt der Speicheradresse der Variablen geändert wird. Bei Variablenübergabe wird mit einer Kopie innerhalb der Funktion gearbeitet und nach dem rausspringen aus dieser Funktion ist diese weg. Ausserdem kostet das Anlegen dieser Kopie Speicher und Zeit. EDIT: Immer wieder erstaunlich wie schnell Mr. Buchegger solche Erklärungen aus dem Ärmel zaubert.
Das war der Original-Beitrag (nur noch mal zur Erinnerung): ------------>> Hallo zusammen, bis jetzt hab ich sehr einfach Software einfach so vor mich hin programmiert (ohne vorher gross zu überlegen oder pläne zu zeichnen). Aber wie geht man vor wenn die Software etwas komplizierter wird? Gibt ja schnell sehr viele Fehlerquellen. Habt ihr ein paar Tipps? Gibt es irgendwelche Grundlegende sachen dir man beachten sollte? Viel if else benutzen oder wenig. Viel ineinander verschachteln oder möglichst wenig. Viele verschachtelungen sollte man maximal benützen? Wann benutzt man State Machines? Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein? Zeichnet ihr Struktorgramme oder einfach Kommentare? Wann verwendet man C++ bei uCs? Und zum Schluss, habt ihr noch eine Buchempfehlung für C speziell für uCs? Danke und schönen Abend C-Master <<------------ Ende des Beitrag Eines sollte man beachten: Das Programmieren ist kein Selbstzweck! Ein Programm setzt eine definierte Aufgabe in Software um(, weil es so am einfachsten geht). Das Ganze sieht dann sinngemäß folgendermaßen aus: 1. Es zeichnet sich eine Aufgabe ab, die zu lösen ist. 2. Die Aufgabe wird ausreichend durchdacht und dabei dokumentiert. 3. Der Benutzer des Ganzen versteht das Ergebnis von 2. und segnet es ab. 4. Die Lösung der Aufgabe wird aus Benutzersicht beschrieben und vom Benutzer abgesegnet. 5. Die Hardware für die Lösung wird konzipiert. 6. Das Design der Software erfolgt: 6.1 Grobdesign, 6.2 Feindesign bis herunter zum Pseudocode. 7. Die Hardware wird hergestellt. 8. Erst jetzt wird die Software programmiert, und zwar in den Pseudocode hinein. 9. Der Software-Produzent testet (White Box Test). 10. Der Benutzer nimmt das System ab (einschließlich Black Box Test). Die Dauer der einzelnen Schritte kann hierbei von Minuten bis hin zu Jahren betragen. Das Ergebnis des Ganzen ist jedenfalls ein durchdachtes, sauber dokumentiertes, wartbares und (meist) funktionsfähiges System. Der Anteil der eigentlichen Codierung am gesamten Zeitaufwand liegt erfahrungsgemäß bei weniger als 20%. Zum Schluß noch eins: Wenn ein Programm kompliziert ist, dann stimmt etwas mit dem Programm nicht! Grüße Bernhard
Genau, nochmal zu ein paar Ausgangsfragen zurück: >Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein? Eigentlich nur durch selbsterklärende Funktions/Variablennamen und an komplizierteren Stellen vereinzelte Kommentare im Quelltext. >Zeichnet ihr Struktorgramme oder einfach Kommentare? Nur wenns nach 3 Versuchen im Kopf nicht hinhaut, dann aber lieber Flussdiagramme, Struktogramme mag ich irgendwie nicht.
Bitte entschuldigt, wenn ich das Topic wieder ein wenig verlasse. Habe den folgenden Text vor etwa zwei Stunden geschriebeni, als der Inhalt noch aktueller war, bin aber noch nicht dazu gekommen, ihn abzuschicken: Tim T. schrieb: > aber bei gotos muss ich dir widersprechen. Das sollte natürlich auch überhaupt kein Aufruf zur Verwendung von Gotos sein, sondern eher einer gegen pauschale Verteufelungen im Allgemeinen. Martin Thomas schrieb: > Aber vor allem in Funktionen mit vielen Verschachtelungen, bei denen > z.B. sehr viele mögliche Fehlerzustände abgefragt werden, vermeiden > machmal "mit Bedacht" eingesetzte label und ein paar gotos > ausufernde und unübersichtliche if-else Konstrukte Genau das ist der Fall, und wahrscheinlich der einzige, wo Gotos sinnvoll sein können. Beispiel:
1 | void function(void) { |
2 | // Aktion 1
|
3 | if(Fehler1) goto error; |
4 | |
5 | // Aktion 2
|
6 | if(Fehler2) goto error; |
7 | |
8 | // Aktion 3
|
9 | if(Fehler3) goto error; |
10 | |
11 | // Aktion 4
|
12 | if(Fehler4) goto error; |
13 | |
14 | // Aktion 5
|
15 | if(Fehler5) goto error; |
16 | |
17 | goto end; |
18 | |
19 | error:
|
20 | // Fehlerbehandlung
|
21 | |
22 | end:
|
23 | // abschließende Aktionen, die in jedem Fall ausgeführt werden
|
24 | // müssen
|
25 | }
|
Hier wird sofort klar, dass nacheinander die Aktionen 1 bis 5 ausgeführt werden und im Fehlerfall in die gemeinsame Fehlerbehhandlung verzweigt wird. Dieselbe Funktion ohne Gotos:
1 | void function(void) { |
2 | int ok = 0; |
3 | |
4 | // Aktion 1
|
5 | if(!Fehler1) { |
6 | // Aktion 2
|
7 | if(!Fehler2) { |
8 | // Aktion 3
|
9 | if(!Fehler3) { |
10 | // Aktion 4
|
11 | if(!Fehler4) { |
12 | // Aktion 5
|
13 | if(!Fehler5) { |
14 | ok = 1; |
15 | }
|
16 | }
|
17 | }
|
18 | }
|
19 | }
|
20 | |
21 | if(!ok) { |
22 | // Fehlerbehandlung
|
23 | }
|
24 | |
25 | // abschließende Aktionen, die in jedem Fall ausgeführt werden
|
26 | // müssen
|
27 | }
|
Hier ist es nicht sofort offensichtlich, was beim Auftreten eines Fehlers passiert. Um das zu erkennen, muss man sich mühsam die If-Treppe am anderen Ende wieder hochhangeln (man stelle sich zudem vor, die einzelnen Aktionen bestehen aus jeweils mehreren Anweisungen, so dass der Code insgesamt länger wird). Außerdem benötigt man eine izusätzliche Variable, um die Erfolgsinformation aus der innersten If-Anweisung nach außen zu tragen. Ganz abgesehen davon wird der Code unnötig "breit", vor allem, wenn man nicht nur 5 sondern vielleicht 20 Fehlerfälle zu untersuchen hat. Ein Switch-Case in C ist auch nicht viel anderes als ein Mehrfach- Goto, bei dem an Stelle der Labels abzuprüfende Werte stehen. Würde man den Switch deswegen durch eine verschachtelte und mehrfach eingerückte If-Anweisung ersetzen? > Das ist dann im Prinzip ein realtiv übersichtliches, im Vergleich zu > C++ allerdings recht primitives, exception-handling. Genau richtig. Die C++-Exceptions sind für die Fehlerbehandlung wie im obigen Beispiel gemacht. Damit kann man in C++ nicht nur Gotos, sondern auch Longjumps auf saubere Art und Weise ersetzen. Jetzt aber wieder zurück zur Ausgangsfrage ;-)
Sorry aber dafür würde ich:
1 | uint8_t FehlerInFunktion(void){ |
2 | // Aktion 1
|
3 | if(Fehler1) return 1; |
4 | |
5 | // Aktion 2
|
6 | if(Fehler2) return 1; |
7 | |
8 | // Aktion 3
|
9 | if(Fehler3) return 1; |
10 | |
11 | // Aktion 4
|
12 | if(Fehler4) return 1; |
13 | |
14 | // Aktion 5
|
15 | if(Fehler5) return 1; |
16 | |
17 | return 0; |
18 | }
|
19 | |
20 | void function(void){ |
21 | if (FehlerInFunktion()){ |
22 | // Fehlerbehandlung
|
23 | };
|
24 | // abschließende Aktionen, die in jedem Fall ausgeführt werden
|
25 | // müssen
|
26 | }
|
verwenden. Aber ich will keine Endlosdiskussion starten, jeder wie er mag... und nun B2T
Was aber, wenn du für die Fehlerbehandlung ganz viele Variablen aus den Aktionen 1 bis 5 brauchst? Die darin enthaltenen Werte müsstest du irgendwie aus FehlerInFunktion() heraus- und in function() hineinbekommen. Vielleicht ein Fall für globale Variablen? ;-) Man müsste jetzt ein konkretes Anwendungsbeispiel parat haben. Ich habe jetzt gerade keins hier, da der Fall zugegebenermaßen nicht allzu oft auftaucht. Aber ich denke, wenn jemand tatsächlich auf so einen Fall stößt, der ohne Gotos nur ungeschickt gelöst werden kann, wird er es nach dem bisher Geschriebenen wahrscheinlich selbst merken. Wie gesagt, es geht mir ja nicht darum, dass jetzt jeder seinen bereits funktionsfähigen Code mit Gotos spickt. Von Gotos ist nach wie vor abzuraten, nur generell vertaufeln sollte man es nicht.
Ich breche alle Konzepte auf saubere State-Maschines runter. DIese sind (zuerst) unabhängig voneinander und könnn sich nur durch funktion(saufrufe) beeinflussen...
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.