Inhaltsverzeichnis
Tippfehler
Tippfehler können immer passieren. Besonders fies ist es, wenn der Tippfehler nicht zu einer Warnung oder zu einer Fehlermeldung führt, weil der entstandene Code korrekter C-Code ist.
Ein ; zu viel
Ein reflexartig eingetippter oder nach Änderungen stehen gebliebener ; hat schon so manches Programm ausgeknockt:
if (a == 0); { /* mach was */ }
Wenn a == 0 ist, dann wird ; ausgeführt (also im Endeffekt garnichts). Danach kommt der Block, der im if stehen sollte. Der wird immer ausgeführt, denn er gehört nicht mehr zum if.
Zuweisung statt Vergleich
if (a = 0) { /* mach was */ }
In einer if-Bedingung wurde beim Vergleich ein = vergessen, so dass der Vergleich zu einer Zuweisung wird: Zunächt wird a mit dem Wert 0 belegt, und das ist auch der Wert des C-Azsdrucks a = 0. Diese if-Bedingung ist also immer falsch und der zugehörende Block wird nie betreten. Zudem wird der Wert von a verändert, was bei einem Vergleichnicht der Fall wäre.
Abhilfe schafft, indem man sich angewöhnt zu schreiben
if (0 == a)
Wenn man dann eine Zuweisung eintippt, gibt's einen Compiler-Fehler. Zudem sollte man immer Compiler-Warnungen aktivieren, so dass bei dem problematischen Code eine Warnung ausgegeben wird. Soll tatsächlich eine Zuweisung und ein Vergleich geschehen, so kann die Warnung bei gcc mit einer Doppelklammer unterdrückt werden:
// Weise a an b zu un teste den Wert auf != 0 if ((b = a)) { // ... }
Signal/Interrupt-Name vertippt (avr-gcc)
SIGNAL (SIG_OVEFRLOW0) { /* mach was */ }
Nicht alle Compiler-Versionen meckern da. Der ISR-Code wird nicht in die Interrupt-Tabelle eingetragen. Kommt es zum Interrupt, dann landet man in RESET.
Mangelnde C-Kenntnis
Warnung ignoriert
- "Wieso soll das Probleme machen? Das ist doch nur eine Warnung!"
Warnungen zur Compile-Zeit werden gerne zu Fehlern zur Laufzeit. Letztere sind deutlich schwerer zu finden, als angewarnten Code zu korrigieren.
void foo (long*); void bar (int *p) { foo (p+1); // Was soll das sein?! // ((long*) p) + 1 oder // (long*) (p + 1) }
> gcc -c -o prog.o prog.c prog.c: In function `bar': prog.c:5: warning: passing arg 1 of `foo' from incompatible pointer type
Auch alle oben erwähnten Fallstricke werden im gcc durch eine Warnung angezeigt. In der Praxis hat es sich bewährt mit gcc -Wall
alle Warnungen einzuschalten und dann den Code solange zu bearbeiten bis keine Warnungen mehr auftreten.
Array-Index
Informatiker am Bahnhof:
- "0, 1, 2, ... Wo ist mein dritter Koffer?!"
Gleiches gilt für Arrays:
#define NUM 10; int a[NUM]; // für die N Werte a[0] ... a[N-1]
Ein Zugriff auf a[N] greift irgendwo hin. Wahlweise liest man Schrott oder überschreibt andere Daten, die in der Nähe liegen, und plötzlich seltsame Werte enthalten.
Bitweise vs. Logische Operatoren
Die Operatoren AND, OR und NOT gibt es in C in zwei Ausprägungen
- bitweise
- Die Operatoren ^, |, &, ~, |=, ^=, &=, |= operieren bitweise. Der entsprechende Operator wird also auf alle Bits des Wertes parallel angewandt und das Ergebnis für ein Bit ist unabhängig vom Inhalt der anderen Bits.
- logisch
- Die Operatoren ||, &&, !, &&=, ||= operieren auf dem ganzen (int) Wert und berücksichtigen nur, ob der Wert 0 (false) oder ungleich 0 (true) ist.
Dementsprechend liefert && i.d.R. ein anderes Ergebnis als &:
if (a && b) // erfüllt, wenn a!=0 und b!=0 if (a & b) // erfüllt, wenn in a und b an der gleichen Stelle ein Bit gesetzt ist
Ein Verwechseln bzw. unkorrektes Einsetzen der Operatoren gibt also ein falsches Programm.
TRUE ist nicht 1
Da C die Begriffe TRUE und FALSE eigentlich nicht kennt, wurde schon öfter beobachtet, daß folgendes definiert wurde:
#define TRUE 1 #define FALSE 0
solange man damit nur Schalter setzt und abfragt, ist dagegen nichts zu sagen.
a = TRUE; b = FALSE; if ( (a == TRUE) && (b == FALSE)) {.."this is true"..}
wenn man aber dann schreibt
if ( (a & b) == TRUE) {.."this is true"..}
kann man einen herbe Enttäuschung erleben. Man müßte für ein richtiges Ergebnis schreiben
if ( (a & b) != FALSE) {.."this is true"..}
Damit ist aber eine gute Lesbarkeit endgültig dahin.
Besser sind Definition wie
#define FALSE (0!=0) #define TRUE (0==0)
denn damit gilt einerseits, daß für alle boolschen Werte einschliesslich dieser Konstanten der Zusammenhang x = !!x besteht. Mit der allerersten Definition hat man das unerwünschte Ergebnis TRUE != !FALSE. Andererseits kann man direkt gegen diese Konstanten vergleichen:
a = (x < y); ... if (a == TRUE) // oder die 'klassische' Variante: if (a)
Siehe auch: http://www.dclc-faq.de/kap8.htm#8.2
Auf der anderen Seite wird oft auch schon "TRUE" in einem der Standart-include-files definiert und sollte dann nicht mehr neu definiert werden.
Ein , anstatt . in Konstante
In C sowie im angloamerikanischen Sprachraum werden Dezimalbrüche mit einem . (Punkt) geschrieben und nicht wie im Deutschen mit einem , (Komma):
float pi; pi = 3,14; // *AUTSCH* soll wohl heissen 3.14
Die zweite Zeile besteht aus zwei durch ein Komma getrennten Anweisungen, so daß das ganze etwa gleichbedeutend ist mit
float pi; pi = 3; 14;
Jedenfalls ist es korrekter C-Code! Die 14; ist ein Ausdruck, der nicht weiter gebraucht wird und daher wegfällt, da er im Gegensatz zu einer void-Funktion keine Wirkung hat. Man rechnet also mit dem Wert 3 für [math]\pi[/math], aber auch dieses Problem würdigt gcc mit einer Warnung.
Das falsche Komma hat auch schon so manchen ebay-Freak aufs Kreuz gelegt...
Führende 0 in Konstanten
In C kennzeichnet eine führende 0 bei einer Zahlenkonstante, daß die Zahl oktal dargestellt ist. Somit ist 010 nicht gleich 10.
if (a == 030) // ist a gleich 24?
Fehlende Klammer in #define
Definitionen per #define werden nicht wie mathermatische Funktionen behandelt, sondern es wird einfach der Text eingesetzt.
#define Konstante (1<<5)+(1<<7)
ist also riskant, denn 2*Konstante wird durch 2*(1<<5)+(1<<7) ersetzt, was vermutlich nicht beabsichtigt ist. Richtig müsste man
#define Konstante ((1<<5)+(1<<7))
schreiben. Man sollte also bei #define reichlich Klammern setzten: Sowohl um einen mathematischen Ausdruck als Ergebnis, als auch um Parameter, die übergeben werden.
Nicht-atomarer Code
Verwendet man in einem Programm IRQs (Interrupts) und ändert in der ISR (Service Routine) ein Datum (Variable, SFR, ...), dann wird diese Änderung möglicherweise überschrieben, wenn die IRQ zu einem ungünstigen Zeitpunkt auftritt.
Ein ausführlicheres Beispiel, das zeigt, was passieren kann, findet sich im Artikel avr-gcc im Abschnitt "Zugriff auf einzelne Bits". Ein weiteres Beispiel ist das Lesen, Schreiben oder Testen einer mehrbytigen Variable:
int volatile i; ... if (0 == i)
Weil i länger als 1 Byte ist, kann es je nach Architektur nicht in einem Befehl gelesen werden; daher kann während des Lesens eine IRQ auftreten, in deren ISR der Wert von i möglicherweise verändert wird. Der obige Code könnte etwa so assembliert werden (hier mit avr-gcc):
; i wird vom SRAM in das Registerpaar r24:r25 geladen ; low-Teil laden lds r24,i ; high-Teil laden ; wenn hier eine IRQ zuschlägt, in der i verändert wird, ist der Registerinhalt korrupt lds r25,(i)+1 or r24,r25 brne ...
Natürlich können auch komplexere Datenstrukturen von diesem Phänomen betroffen sein. Besonders unangenehm an dieser Klasse von Fehlern ist, daß sie nur sporadisch auftauchen und man sie daher selbst mit einem guten Debugger sehr schlecht orten kann, da die zugehörige Codestelle fast immer korrekt abgearbeitet wird.
Eine Möglichkeit dem zu begegnen ist, den ganzen betroffenen Block ununterbrechbar (atomar) zu machen. Wieder ein Beispiel für avr-gcc:
#include <avr/io.h> #include <avr/interrupt.h> ... // ein lokale(!) Variable, die das Status-Register und // insbesondere das im folgenden geänderte I-Flag merkt // Falls man an der Codestelle immer weiß, daß IRQs // aktiviert sind, kann man die zu atomisierende Sequenz // auch in cli()...sei() einschachteln // Falls IRQs global deaktiviert sind brauch man natürlich // keine besonderen Vorkehrungen zu treffen, da dann aller Code // atomar ist. unsigned char sreg = SREG; // Interrupts global deaktivieren (I-Flag = 0) cli(); if (0 == i) { i = 1; // IRQs sobald als möglich wieder zulassen // (I-Flag wieder herstellen) SREG = sreg; } else { // IRQs so bald als möglich wieder zulassen // (I-Flag wieder herstellen) SREG = sreg; return; } ...
Oder je nach Programmstruktur sichert man den Wert, z.B. in eine lokale Variable oder deaktiviert nur selektiv die kritischen Interrupts, also solche, die Einfluss auf die kritischen Daten nehmen.
Probleme mit volatile
Manchmal optimieren Compiler den Code auf unerwartete Weise. z.B. dieser unverdächtige Code:
int time; // updated in the interrupt main() { time = 0; while (time < 2000) { // do something... } // do something else }
Dieser Code wird sehr schnell zu einer Endlosschleife optimiert. Der Compiler sieht den Kommentar und auch die Interrupt-routine nicht und geht deshalb davon aus, dass time immer 0 ist. Der Test für < 2000 kann daher optimiert werden.
Merke: Variablen die in einer Interrupt Routine verändert werden immer als volatile
markieren und möglicherweise durch das Ausschalten des Interrupts absichern.
Wie im letzten Abschnitt bemerkt gibt es hier noch einen weiteren Fehler. Je nach Prozessor und c-library kann es vorkommen, dass die Bytes von "time" unterschiedlich upgedatet werden. In unglücker Interrupt der genau dann zuschlägt während "time<2000" geprüft wird kann das Timing gehörig durcheinander bringen.
Weitere Fallstricke
Überlauf bei Konstanten
Ohne extra Angaben werden Konstanten als Integer berechnet. Zum Beispiel wird (2<<15) als 0 behandelt, da die unteren 16 Bits Null sind. Leider geben einige Compiler hierbei nicht einmal eine Warnung aus. Um das erwartete Ergebniss zu erhalten muss man (2L<<15) schreiben. Dann wird eine Long Datentyp verwendet.