Hallo Leute!
Ich lese gerade das Buch "Expert C Programming" von Peter van der
Linden. Dort werden unter anderem die Typkonversionen bei arithemtischen
Operationen ein wenig beleuchtet. Der Autor gibt daraufhin den Tipp,
unsigned Variablen zu vermeiden bzw. nur für Dinge wie Bitfelder zu
nutzen. Vor allem sollte es vermieden werden, eine Variable als unsigned
zu deklarieren, nur weil der Wert dieser Variable nicht negativ werden
kann.
Konkret schreibt der Autor:
"Use a signed type like int and you won't have to worry about boundary
cases in the detailed rules for promoting mixed types."
Kann mir das jemand genauer erklären? So ganz verstehe ich die
Erläuterung des Autors im Buch nämlich nicht, es wird leider nur aus dem
ANSI C Standard zitiert (bin kein blutiger C Anfänger, würde mich
vielleicht als "fortgeschrittenen Anfänger" bezeichnen ;)).
Meiner Meinung nach widerspricht dieser Tipp der gängigen Praxis,
zumindest für den Code, den ich zu Gesicht bekomme (fast ausschließlich
AVR/ARM-Code). Dort ist es eigentlich Gang und Gäbe für Variablen, die
nicht negativ werden können unsigned-Typen zu verwenden.
Vielen Dank schon mal für eure Antworten!
Andi K. schrieb:> Kann mir das jemand genauer erklären?
Wenn auf 1 auf "unsigned short" addierst, dann hat das Zwischenergebnis
üblicherweise den Typ "unsigned", wenn es eine 8/16-Bit Maschine ist und
den Typ "int", wenn es eine 32/64-Bit Maschine ist.
Ähnlich sieht es aus, wenn du 1L auf "unsigned" addierst. Das Ergebnis
ist "unsigned long" wenn sizeof(unsigned) == sizeof(long) (wie Win64),
andernfalls "long" (wie Linux64).
Hallo,
wenn du in deinem Programm sicherstellen kannst, das du nie in den
negativen Bereich kommst, ist unsigned okay.
Wenn du das nicht kannst ist es sicherer signed zu verwenden.
Außerdem musst du auch das obere Ende dieses Zahlenbereiches beobachten.
Es ist zwar nur ein Bit, leider aber das mit der höchsten Wertigkeit.
Grüße
Bei einigen 8-Bit Microcontrollern ist es jedoch ungünstig, Typen mit
Vorzeichen zu verwenden, weil deren Vergleichsbefehle bzw. Sprünge das
von Haus aus nicht hergeben und der Code deshalb umständlicher wird. Das
betrifft beispielsweise 8051 und die PICs, nicht aber AVR.
Das Problem mit unsigned Variablen ist, dass Konvertierungen zwischen
signed und unsigned sehr merkwürdig sein kann und nicht die Ergebnisse
liefert die man erwartet. Ein Beispiel:
1
unsigned int one = 1;
2
int minus_one = -1;
3
if(one < minus_one)
4
printf("This is true!");
Ein anderes Argument warum häufig unsigned benutzt wird ist wie du schon
sagst, dass man den Wertebereich auf postive Zahlen beschränken will.
Leider funktioniert das genauso wenig, da negative Zahlen einfach
komplett ohne Warnung zu unsigned konvertiert werden:
1
void foo(unsigned int i)
2
{
3
printf("%u", i);
4
}
5
...
6
foo(-1); // Gibt keine Warnung
Meine Empfehlung zur Benutzung von unsigned ist, dass man auf unsigned
verzichten sollten außer wenn:
- man mit Bitfeldern und Bitoperationen wie Shifts arbeiten möchte
- man auf modulo 2^32 (oder welche Bitzahl auch immer) Rechnungen
angewiesen ist
Leider hat man im Standard den Typen size_t eingeführt, der für Größen
benutzt wird und unsigned ist. Daher muss man die oberen Punkte wohl
erweitern um:
- wenn man mit Libraries arbeitet die unsigned Variablen nutzen
A. K. schrieb:> Das betrifft beispielsweise 8051 und die PICs, nicht aber AVR.
Selbst auf einem AVR spart einem aber der Wegfall der sign extension
oft genug etwas Code ein, gerade bei der Arbeit mit kleinen Zahlen
(uint8_t).
Über die im verlinkten Artikel genannten Probleme mit unsigned
underflow bin ich eigentlich relativ selten gestolpert. Allerdings
muss ich natürlich auch sagen, wer bei einer Rechnung wie:
1
unsignedcandidates=total-lengths[i]+1;
nicht auf den Gedanken kommt zu verifizieren, dass sein "total"
immer mindenstens so groß ist wie "lengths[i]", der würde auf einem
kleinen Microcontroller auch mit vorzeichenbehafteten Typen baden
gehen, da er dann vermutlich genauso schnell aus dem Blick verliert,
dass die Regel „vorzeichenbehaftete Typen laufen nicht über“ bei
32767 auf so einem Controller ihre jähe Grenze findet ...
Vermutlich will das Buch eher auf die nicht immer sofort mental
erschließlichen Implikationen der integer promotion rules hinaus,
und über diese muss man in der Tat immer wieder gründlich nachdenken.
A. K. schrieb:> Das> betrifft beispielsweise 8051 und die PICs, nicht aber AVR.
Auch den AVR.
Alle 8Bitter brauchen zusätzliche Schritte bei Rechnungen >8Bit.
Wenn man signed 8Bit mit 16 oder 32Bit verrechnet, muß zuerst eine
Vorzeichenerweiterung erfolgen.
Peter Dannegger schrieb:> Alle 8Bitter brauchen zusätzliche Schritte bei Rechnungen >8Bit.
Klar, aber das meinte ich nicht. AVR kann auch mit Vorzeichen auf ">"
vergleichen, PIC nicht unmittelbar. AVR könnte aber eine billigere
Vorzeichenerweiterung gebrauchen. Die hatte schon 6809, aber vielleicht
waren die Designer beim AVR dafür zu prüde.
Kapuzenzwerg schrieb:> Es geht dabei in erster linie darum, unbemerkte Überläufe zu verhindern.
Was sagt denn der C-Standard zum Thema signed/unsigned Overflow?
Nagelt mich nicht an die Wand, aber ich meine hier im Forum gelesen zu
haben, das Laut dem Standard das verhalten bei signed overflow nicht
definiert ist.
Kaj schrieb:> Nagelt mich nicht an die Wand, aber ich meine hier im Forum gelesen zu> haben, das Laut dem Standard das verhalten bei signed overflow nicht> definiert ist.
So ist es.
Unsigned „rollt definiert über“, und das ist halt das, worüber der
Verfasser des obigen Links gestolpert ist: wenn man einen 0 hat und
davon 1 abzieht, dann bekommt man eine (u. U. sehr) große Zahl. ;-)
Allerdings funktioniert seine Blauäugigkeit „ich will mir vorher keine
Gedanken um den Wertebereich machen“ nur so einigermaßen, seit der Typ
“int” auf gängigen Maschinen 32 bit hat, denn da sind die Über- und
Unterläufe einer vorzeichenbehafteten Zahl jenseits von (betragsmäßig)
zwei Milliarden, was für viele Dinge einfach wirklich ohne nähere
Größenabschätzung genügt, als dass er mit seiner Gedankenlosigkeit
mit der vorzeichenbehafteten Zahl in der Tat das kleinere Risiko hat.
Im Embedded-Bereich kann man sich derartige Sorglosigkeit normalerweise
nicht leisten (gerade nicht auf kleinen Controllern), da muss man sich
zwangsläufig um den Wertebereich Gedanken machen. Hat man sich diese
aber gemacht, dann ist unsigned underflow auch kein Problem.
Jörg Wunsch schrieb:> Unsigned „rollt definiert über“
Und das ist auch gut so.
Z.B. wenn man 2 Zeitstempel hat und die dazwischen vergangene Zeit
wissen will. Dann wäre es unschön, bei jedem Timerüberlauf falsche
Ergebnisse zu erhalten.
Wie bei allem in C muss man halt wissen was man tut und die Details der
Sprache kennen.
Welcher Wert wird c zugewiesen?
1
unsignedinta=10;
2
intb=-2;
3
intc=a/b;/* c wird 0 zugewiesen */
Was der Compiler tut:
1
intc=10/(unsignedint)-2;
So sind halt einfach die Regeln von C. Der GCC spuckt mit
-Wsign-onversion auch Warnungen aus. Für obiges Beispiel:
1
[Warning] conversion to 'unsigned int' from 'int' may change the sign of the result [-Wsign-conversion]
2
[Warning] conversion to 'int' from 'unsigned int' may change the sign of the result [-Wsign-conversion]
Um das Problem zu umschiffen, muss man die Berechnung mit einem
grösseren signed Typen durchführen.
Ich selbst nutzte sowohl signed als auch unsigned Typen. Das kommt immer
darauf an, welchen Wertebereich die Variable haben soll. Ich mische
diese auch in Berechnungen, natürlich mit entsprechenden Casts um auf
der sicheren Seite zu sein.
Auch mit den (u)int_fastXX_t Typen aus stdint muss man aufpassen. Auf
einem 32-Bit PC sind die 8, 16 und 32 Bit Typen identisch, auf einem
kleinen uC nicht. So ist es schnell passiert, dass ein Programm, das auf
einem PC korrekt arbeitet, auf einem uC scheitert.
Ich halte das üblicherweise so:
0. Falls möglich, Formeln so aufstellen, dass von vornherein nur mit
positiven Zahlen gerechnet werden muss
1. Operanden in normalen Berechnungen (+, -, *, /) nach Möglichkeit
immer signed
2. Divisionen mit negativen Operanden vermeiden
3. Bit-Operationen (&, |, ^, ~) nur mit positiven¹ Operanden
4. Shift-Operationen (<<, >>) nur mit positivem¹ linken Operanden
5. Absichtliche Überläufe für Modulo-2**n-Berechnungen) immer unsigned
6. Mischen von signed und unsigned in einer Operation, in der der signed
Operand negativ werden kann, grundsätzlich vermeiden. Führt kein Weg
daran herum, unsigned Operand explizit in signed Wert passender Größe
casten.
7. Berechnung mit größeren Zahlen immer auf mögliche Überläufe prüfen
(nicht im Programm, sondern im Kopf).
——————————
¹) "positiv" heißt: wenn signed, dann garantiert ≥0
Kaj schrieb:> Kapuzenzwerg schrieb:>> Es geht dabei in erster linie darum, unbemerkte Überläufe zu verhindern.>> Was sagt denn der C-Standard zum Thema signed/unsigned Overflow?
Bei signed ist es nicht definiert, bei unsigned gibt es keine Überläufe,
aber eine Modulo-Rechnung.
be stucki schrieb:> Auch mit den (u)int_fastXX_t Typen aus stdint muss man aufpassen. Auf> einem 32-Bit PC sind die 8, 16 und 32 Bit Typen identisch, auf einem> kleinen uC nicht. So ist es schnell passiert, dass ein Programm, das auf> einem PC korrekt arbeitet, auf einem uC scheitert.
Es wäre allerdings auch ziemlich dämlich, diese Typen zu benutzen, wenn
die Größe des Typs exakt stimmen muss.
Yalu X. schrieb:> 4. Shift-Operationen (<<, >>) nur mit positivem¹ linken Operanden
Der rechte muß ja sowieso positiv sein, also hättest du das nicht
unbedingt auf den linken einschränken müssen.
be stucki schrieb:> Wie bei allem in C muss man halt wissen was man tut und die Details der> Sprache kennen.
Das muss man eigentlich bei allen Sprachen. Deshalb stöhne ich innerlich
auf, wenn mal wieder so ein Bastel-Maker-Kiddi ankommt und die neuste
Modesprache über den grünen Klee lobt.
Sebastian V. O. schrieb:> Meine Empfehlung zur Benutzung von unsigned ist, dass man auf unsigned> verzichten sollten außer wenn:> - man mit Bitfeldern und Bitoperationen wie Shifts arbeiten möchte> - man auf modulo 2^32 (oder welche Bitzahl auch immer) Rechnungen> angewiesen ist
und wenn
- meine Daten einfach unsigned sind, wie z.B. ein 8-Bit Grauwert in
einem Bild
Peter schrieb:> und wenn> - meine Daten einfach unsigned sind, wie z.B. ein 8-Bit Grauwert in> einem Bild
Dazu müßte man sie aber erstmal explizit in einen größeren unsigned-Typ
konvertieren, denn 8-Bit-Typen werden vor einer Berechnung automatisch
nach int "promoted", somit wird die Berechnung mit Vorzeichen
durchgeführt.
C ist ja bekannt für seine gnadenlose Logik inclusive der entsprechenden
Fallstricke. Am Ende ist tatsächlich immer der Programmierer schuld wenn
etwas nicht so funktioniert wie erwartet.
Bezugnehmend auf die Frage des TO:
Ich hatte mal so einen Fall - Microcontroller Projekt, eigentlich alles
unsigned variable verwendet. Mir war nur folgendes Prinzip noch nicht
vertraut: wenn man von einer unsigned Variable, die gerade Null ist,
eins abzieht, dann ist die Variable nachher immer noch nicht kleiner als
Null. genau so einen Vergleich if(x < 0) hatte ich aber in dem Code. Es
brauchte eine Weile bis ich das begriff und mir dann vor den Kopf
schlug.
Klar, mit konsequenter Verwendung von signed Variablen hätte ich die
Klippe umschifft, hätte aber womöglich nie die Lernerfahrung gemacht.
Ich kann die Meinung des erwähnten Buchautors nicht teilen. Datentypen
sollten, insbedondere bei Microcontroller-Anwendungwen immer sorgfältig
für die konkrete Anwendung und zu erwartenden Wertebereich gewählt
werden. Praxis mit C bekommt man haupsächlich durch das Begehen und
Auffinden von Fehlern. Es ist ne komplexe Angelegenheit, und mit so
einfachen Kochbuchrezepten wie "grundsätzlich nur signed Variablen
vwerwenden" kommt man da nicht wirklich weit.
Rolf Magnus schrieb:> Yalu X. schrieb:>> 4. Shift-Operationen (<<, >>) nur mit positivem¹ linken Operanden>> Der rechte muß ja sowieso positiv sein, also hättest du das nicht> unbedingt auf den linken einschränken müssen.
Ja, da hast du natürlich recht :)
Micha schrieb:> Praxis mit C bekommt man haupsächlich durch das Begehen und> Auffinden von Fehlern. Es ist ne komplexe Angelegenheit, und mit so> einfachen Kochbuchrezepten wie "grundsätzlich nur signed Variablen> vwerwenden" kommt man da nicht wirklich weit.
Doppelt unterstreichen, einrahmen, ausdrucken und an die Wand hängen!
Genau das ist die Quintessenz.
Deshalb passt auch der Vergleich Programmieren-Radfahren so gut. Man
muss auf die Schnauze fallen um weiter zu kommen.
Ein Programmierer mit Erfahrung unterscheidet sich von einem
Programmierer ohne Erfahrung hauptsächlich dadurch, dass der eine schon
in mindestens 3000 Fallen mehr getappt ist (und sie gelöst hat), die
noch auf den anderen warten. Und die 3000 sind wörtlich zu nehmen.
Micha schrieb:> genau so einen Vergleich if(x < 0) hatte ich aber in dem Code
Wie soll denn auch eine vorzeichenlose Zahl jemals kleiner als 0
werden können?
Heutzutagen warnt einen allerdings ein Compiler vor sowas.
Jörg Wunsch schrieb:>> genau so einen Vergleich if(x < 0) hatte ich aber in dem Code>> Wie soll denn auch eine vorzeichenlose Zahl jemals kleiner als 0> werden können?
Da siehts ein Blinder mit dem Krückstock, aber hier auch?
unsigned i, n;
// .... 150 Zeilen, manchmal n == 0 ...
if (i < n-1)
Um solchen und aehnlichen Fehlern vorzubeugen, lass ich mich bei
grösseren Projekten von PC-Lint vollabern. Fehler im eigentlichen Sinne
kommen zwar nie zum Vorschein, aber unglaublich, was der an Nuancen zu
unterscheiden vermag und an Stellen Warnungen ausgibt, auf die ich nie
und nimmer selbst gekommen waere.
Micha schrieb:> C ist ja bekannt für seine gnadenlose Logik inclusive der entsprechenden> Fallstricke. Am Ende ist tatsächlich immer der Programmierer schuld wenn> etwas nicht so funktioniert wie erwartet.
Je nun, du wirst es vielleicht nicht hören wollen, aber das ist bei
allen anderen Programmiersprachen ganz genau so.
Oliver
Karl Heinz schrieb:> Ein Programmierer mit Erfahrung unterscheidet sich von einem> Programmierer ohne Erfahrung hauptsächlich dadurch, dass der eine schon> in mindestens 3000 Fallen mehr getappt ist (und sie gelöst hat), die> noch auf den anderen warten.
Doppelt unterstreichen, einrahmen, ausdrucken und an die Wand hängen!
Es gibt ne Menge Dinge, die man aus heutiger Sicht an C verbessern
könnte.
Aber nur durch das gnadenlose Festhalten an einmal getroffenen
Vereinbarungen konnte sich C so weit verbreiten.
Und dadurch ist es auch für jede Architektur verfügbar.
Diese Beständigkeit und Verbreitung gibt es bei keiner anderen
Programmiersprache.
Oliver schrieb:> Je nun, du wirst es vielleicht nicht hören wollen, aber das ist bei> allen anderen Programmiersprachen ganz genau so.
Je mehr man versucht, sich dem "do what I mean" Paradigma zu nähern, an
Stelle des für C typischen "do what I say", desto komplexer werden die
entsprechenden Implementierungen. Wodurch die Wahrscheinlichkeit wächst,
dass dann doch mal die Implementierung Schuld trägt.