Hallo,
ich habe angefangen mit einem AVR (Atmega328 auf einem Arduino Board) zu
programmieren.
ich habe mal eine generelle frage zu den Datentypen.
wenn ich stdint.h includiere, kann ich uint8_t, uint16_t usw. benutzen.
Ansonsten muss ich bei den default typen bleiben unsigned char, unsigned
int usw.
Welche Datentypen sollte ich der richtigkeithalber nehmen?
Und wieso nimmt man obwohl man stdint.h includiert hat für strings
trotzdem noch char?
Hi
die Variable vom Typ "int" ist der Urgrossvater der variablen in der
Sprache C. Als das entwickelt wurde haben sich die Väter der Sprache
keine grossen Gedanken gemacht, was denn das jetzt ist. Denn es ist
klar, dass int der Registergröße des Prozessors entspricht. Dummerweise
kam nach den 8-Bit Prozessoren die 16-Bitter, dann die 32.Bitter, also
was ist int? Um da jetzt Klarheit reinzubringen wurden diese Variablen
wie uint8_t oder uint16_t definiert um ganz klar darustellen was das
jetzt für ne Variable sein soll. Und das ist jetzt portabel, also
unabhängig vom Prozessor.
Und string gibts eigentlich in C nicht. Ein string ist daher ein Array
aus Charactern, also "char" oder char[]
Gruß
Gerhard
Felix schrieb:> Welche Datentypen sollte ich der richtigkeithalber nehmen?
Die Datentypen welche sich bei deinem Problem sinnvoll einsetzen lassen!
Da das Problem geheim ist, ist mir leider keine konkretere Antwort
möglich.
Felix schrieb:> Und wieso nimmt man obwohl man stdint.h includiert hat für strings> trotzdem noch char?
Weil Strings eben aus Buchstaben bestehen....
Würde ich mal sagen....
Arduino Fanboy D. schrieb:> Weil Strings eben aus Buchstaben bestehen....> Würde ich mal sagen....
Das ja, aber wenn ich nach dieser Tabelle gehe
https://de.wikibooks.org/wiki/C-Programmierung_mit_AVR-GCC/_Datentypen,
dann ist char mit uint8_t ersetzt worden.
Das beides das selbe ist, ist mir bewusst. Aber es ist mir trotzdem
schon öfters aufgefallen, dass für variablen uint8_t, uint16_t usw.
genommen worden ist und für Arrays aus charachtern (danke Franko P.)
eben char.
Franko P. schrieb:> Um da jetzt Klarheit reinzubringen wurden diese Variablen> wie uint8_t oder uint16_t definiert um ganz klar darustellen was das> jetzt für ne Variable sein soll. Und das ist jetzt portabel, also> unabhängig vom Prozessor.
Dann werde ich auch alles in uint8_t uint16_t usw. machen
> Das beides das selbe ist, ist mir bewusst.
Dann ist Dir etwas Falsches bewusst.
char hat minimal 8bit; ein int8_t hat genau 8bit ("exact width
type")
Details findest Du z.B. hier: https://en.wikipedia.org/wiki/C_data_types
Ingo E. schrieb:> char hat minimal 8bit; ein int8_t hat genau 8bit ("exact width> type")
außerdem ist char entweder signed oder unsigned, je nach
Compiler(-optionen).
int8_t hingegen ist immer signed.
Felix schrieb:> dann ist char mit uint8_t ersetzt worden.
Nein, das ist keine Ersetzung!
uint8_t gibt es nur auf Prozessoren, welche auch wirklich 8Bit Grüppchen
verarbeiten können.
uint8_t ist also ein optionaler Datentype.
char gibts auf JEDEM Kessel
Für 8 Bit CPUs gilt
- so kurz wie möglich uint8_t ist efektiver als uint16_t
- vorzeichenlose Typen verwenden wo möglich
- long nur dort wo das notwendig ist
beim ARM ist das allerdings nicht mehr so. Ich habe aber gemerkt, dass
auch dort in vielen Fällen mit kurzen Variablen kompakterer Code
generiert wird.
Thomas Z. schrieb:> beim ARM ist das allerdings nicht mehr so. Ich habe aber gemerkt, dass> auch dort in vielen Fällen mit kurzen Variablen kompakterer Code> generiert wird.
Zauberei?
Felix schrieb:> wenn ich stdint.h includiere, kann ich uint8_t, uint16_t usw. benutzen.> Ansonsten muss ich bei den default typen bleiben unsigned char, unsigned> int usw.
So weit, so offensichtlich.
> Welche Datentypen sollte ich der richtigkeithalber nehmen?
Den passenden! Und wenn das Programm nicht länger als nötig werden soll;
den kürzesten passenden.
> Und wieso nimmt man obwohl man stdint.h includiert hat für strings> trotzdem noch char?
Wieso "trotzdem"? Wenn man ein char haben will, nimmt man ein char.
Wieso sollte das davon abhängen, welche Typen es außerdem noch gibt?
Erinnere dich dann auch daran, dass es noch fastest und least Typen
gibt, die oft angebrachter sind.
uint8_least_t funktioniert auch, wenn CHARBITS größer als 8 ist.
Die int8_t Familie nimmt man zum rechnen, Sie sind ein Ersatz für signed
char und unsigned char.
Für char ist vom Standard nicht vorgegeben, ob es signed oder unsigned
ist.
Zudem sind alle Stringfunktionen der Standard Library auf char
ausgelegt.
Franko P. schrieb:> Dummerweise kam nach den 8-Bit Prozessoren die 16-Bitter, dann die> 32.Bitter,
8 Bitter kamen erst später für C. Die Väter haben sich große Gedanken
gemacht, die vielen verschiedenen Architekturen zu unterstützen. Darum
gab es 3 char, und 6 int.
Erst später haben sich Architekturen mit 8*2^n Bit durchgesetzt. Und nur
deshalb konnte man konkret werden,
> wurden> diese Variablen wie uint8_t oder uint16_t definiert um ganz klar> darustellen was das jetzt für ne Variable sein soll
Verwendung: char für Strings.
Ob Du nun unsigned char oder uint8_t für Zahlen verwendest, ist eher
Geschmacksache. Der Vorteil von (u)int8_t: es knallt wenn der Prozessor
keinen 8bit-typen hat. Bei unsigned char wirst Du höchstwahrscheinlich
auf die Schnauze fallen, weil Du erwartest, dass nach 255 0 kommt.
Bei 16- oder 32 bit-Zahlen hilft ein uintXX_t nur bedingt. Spätestens
bei Vergleichen, Rechnungen oder printf musst Du wissen, wie groß int
ist oder größeren Aufwand treiben.
Beispiele: (keine Zeile funktioniert wie erwartet)
uint16_t u=10000*8/4;
uint16_t u2=100; int16 i=-1; if(i<u2) ...
printf("%u, %u", u, u2);
Felix schrieb:> Und wieso nimmt man obwohl man stdint.h includiert hat für strings> trotzdem noch char?
Weil char der Typ für Text ist.
Franko P. schrieb:> Hi> die Variable vom Typ "int" ist der Urgrossvater der variablen in der> Sprache C. Als das entwickelt wurde haben sich die Väter der Sprache> keine grossen Gedanken gemacht, was denn das jetzt ist. Denn es ist> klar, dass int der Registergröße des Prozessors entspricht. Dummerweise> kam nach den 8-Bit Prozessoren die 16-Bitter, dann die 32.Bitter, also> was ist int? Um da jetzt Klarheit reinzubringen wurden diese Variablen> wie uint8_t oder uint16_t definiert um ganz klar darustellen was das> jetzt für ne Variable sein soll. Und das ist jetzt portabel, also> unabhängig vom Prozessor.
Ziemlich genau falsch herum. Die ursprünglichen Typen wurden gerade aus
Gründen der Portabilität nicht auf bestimmte Größen festgezurrt. Damit
lässt sich ein effizienter C-Compiler für fast jede erdenkliche
Plattform erzeugen, was auch geschehen ist. Da man manchmal aber feste
Größen braucht (oft in dem Teil des Code, der prozessorspezifisch und
damit sowieso unportabel ist), wurden entsprechende typedefs mit C99
nachgerüstet.
MaWin schrieb:> Ingo E. schrieb:>> char hat minimal 8bit; ein int8_t hat genau 8bit ("exact width>> type")>> außerdem ist char entweder signed oder unsigned, je nach> Compiler(-optionen).> int8_t hingegen ist immer signed.
char sollte man auch nie für was anderes als für Text einsetzen. Da man
damit nicht rechnet, spielt auch die Signedness keine Rolle.
Kevin M. schrieb:> Thomas Z. schrieb:>> beim ARM ist das allerdings nicht mehr so. Ich habe aber gemerkt, dass>> auch dort in vielen Fällen mit kurzen Variablen kompakterer Code>> generiert wird.>> Zauberei?
Vermutlich. Ich hätte erwartet, dass der Code etwas größer wird.
A. S. schrieb:> Spätestens bei Vergleichen, Rechnungen oder printf musst Du wissen, wie> groß int ist oder größeren Aufwand treiben.
Du musst wissen, welchen Zahlenraum der Typ mindestens können muss,
damit die Rechnung hinein passt.
A. S. schrieb:> uint16_t u2=100; int16 i=-1; if(i<u2) ...
Signed und unsigned zu mischen, ist sowieso oft keine gute Idee.
Typischerweise faengt man mit int an (fuer zahlen, counter etc)
und erst wenn einem das Ram ausgeht, probiert man halt arrays etc auf
int8_t
uint8_t etc umzustellen.
Fuer strings eigentlich immer char*. Da ist es ganz egal ob stdint
inkludiert ist oder nicht. Solange du nicht herumkonvertierst isses auch
egal ob der jetzt signed oder unsigned ist.
Das unsigned ist wohl wegen der funktionen im ctype.h
<https://stackoverflow.com/questions/17975913/does-ctype-h-still-require-unsigned-char>
(ist halt die Frage ob du die verwenden willst.)
Felix schrieb:> Welche Datentypen sollte ich der richtigkeithalber nehmen?
Das bleibt dir überlasssen. Ich verwende auf embedded Code fast
ausschließlich die stdint Typen, da ich dadurch die Bit-Breite der
Variablen absolut im Griff habe und nicht jedes mal nachdenken muss, wie
groß ein "in"t oder ein "long int" etc. auf der aktuellen Architektur
sind.
Man kann sich bspw nur auf gewisse Randbedingungen verlassen:
1) ein char ist immer "1" groß. => sizeof(char) ist immer 1!
2) ein Integer "int" ist mindestens 16 bit breit. (AVR: 16 bit, x86: 32
bit)
3) etc...
Felix schrieb:> Und wieso nimmt man obwohl man stdint.h includiert hat für strings> trotzdem noch char?
Weil ein "char" der Korrekte Typ für einen Buchstaben (Text) ist. Das
ist wie oben geschrieben auch safe, da ein char nach C-Standard immer 1
groß ist.
Das Hauptproblem an der Sache ist, dass C nicht spezifiziert, wie groß
die anderen Typen sind, bzw. eben nur in auslegbaren Grenzen.
Für Text im Speicher in struct oder array ist "char" sinnvoll. Für einen
Buchstaben in einer lokalen Variablen gilt das nicht zwangsläufig auch.
Besonders nicht bei Nettigkeiten wie getchar():
char c;
while ((c = getchar()) != EOF)
...
geht garantiert irgendwie in die Hose.
Rolf M. schrieb:> Du musst wissen, welchen Zahlenraum der Typ mindestens können muss,> damit die Rechnung hinein passt.
Das ist ja durch intxx_t in den Beispielen klar. Trotzdem hängt das
Verhalten von der Größe von int ab.
(prx) A. K. schrieb:> Für Text im Speicher in struct oder array ist "char" sinnvoll. Für einen> Buchstaben in einer lokalen Variablen gilt das nicht zwangsläufig auch.
Für ein Zeichen schon.
> Besonders nicht bei Nettigkeiten wie getchar():> char c;> while ((c = getchar()) != EOF)> ...> geht garantiert irgendwie in die Hose.
Klar. Der Returntyp von getchar() ist ja auch nicht char.
Auch wenn der Name ("get char") das suggeriert.
Axel S. schrieb:>> Für Text im Speicher in struct oder array ist "char" sinnvoll. Für einen>> Buchstaben in einer lokalen Variablen gilt das nicht zwangsläufig auch.>> Für ein Zeichen schon.
Bedingt. Wenn (u)int_fast8_t nicht mit (u)int8_t identisch ist, ist char
wahrscheinlich weniger effizient als int.
Axel S. schrieb:> Klar. Der Returntyp von getchar() ist ja auch nicht char.> Auch wenn der Name ("get char") das suggeriert.
Der Name passt schon, es wird ja ein char vom Stream gelesen.
Dirk B. schrieb:> Axel S. schrieb:>> Klar. Der Returntyp von getchar() ist ja auch nicht char.>> Auch wenn der Name ("get char") das suggeriert.>> Der Name passt schon, es wird ja ein char vom Stream gelesen.
Jein. Aus dem Stream wird schon ein Zeichen gelesen. Das wird
allerdings nicht direkt zurück gegeben, weil man dann ja EOF oder einen
Fehler nicht in den Returnwert tun könnte (inband signaling).
Also wird das gelesene unsigned(!) char in ein signed(!) int konvertiert
und bei Bedarf eine magische Konstante (eben EOF) zurück gegeben. Ja,
der eigentliche Fehler ist das inband signaling.
A. S. schrieb:> Rolf M. schrieb:>> Du musst wissen, welchen Zahlenraum der Typ mindestens können muss,>> damit die Rechnung hinein passt.>> Das ist ja durch intxx_t in den Beispielen klar.
Damit ist es aber meistens überspezifiziert. Wichtig ist, dass deine
Werte rein passen, d.h. eine Mindestgröße reicht. Eine exakte Größe ist
da nicht nötig. Für das beste Optimierungspotenzial ist es sinnvoll,
möglichst genau zu nennen, was man braucht und nur das. Das lässt dem
Compiler mehr Freiheiten.
> Trotzdem hängt das Verhalten von der Größe von int ab.
Klar, man muss die Mindestgrößen eben kennen und darf für portablen Code
int nicht für Werte größer 32767 verwenden. Aber man hat ja alle
Möglichkeiten, den Typ passend zu wählen. Entweder nimmt man int und
akzeptiert das, weiß aber auch, dass es 32 oder gar 64 Bit groß sein
kann. Oder man nimmt, wenn es auf Platzsparen kommt, dafür int_least16_t
und bekommt das kleinstmögliche, was diesen Wertebereich kann, und das
sogar auf Prozessoren, die gar keinen exakt 16 Bit großen Typen kennen.
Oder man nimmt int_fast16_t, wenn es einem eher auf Geschwindigkeit
ankommt. Dann kann der Compiler notfalls auch einen größeren Typen
nehmen, wenn der auf diesem Prozessor effizienter ist.
Mit einem int16_t geht nichts davon, ohne dass man dafür irgendeinen
echten Vorteil hätte. Die einzige Ausnahme sind die Stellen, für die die
exakte Größe wirklich zwingend erforderlich ist, wie z.B. Zugriff auf
Hardware-Register.
Axel S. schrieb:> Klar. Der Returntyp von getchar() ist ja auch nicht char.> Auch wenn der Name ("get char") das suggeriert.
Tatsächlich verwendet C für einzelne Zeichen durchgängig int als Typ. So
wird z.B. auch bei Funktionen toupper() oder isalpha() immer int
verwendet. Nur bei Arrays/Pointern darauf wird char verwendet.
Axel S. schrieb:> Also wird das gelesene unsigned(!) char in ein signed(!) int konvertiert
Dabei geht aber nicht die Information über das Zeichen verloren. Das
passiert dummerweise erst, wenn es wieder in einem signed char abgelegt
wird. Dann erst hat es möglicherweise einen negativen Wert.
Aber eigentlich ging es um den Namen
A. S. schrieb:> Zeichen sind meist int in c. Nur in Strings oder %c sind es char.
Etwas genauer zu %c: da geht es nur bei den scanf-Funktionen um char
bzw. char*.
Bei den printf-Funktionen wird tatsächlich eine int übergeben (von der
dann aber nur das unterste char ausgegeben wird).
A. S. schrieb:> Bei 16- oder 32 bit-Zahlen hilft ein uintXX_t nur bedingt. Spätestens> bei Vergleichen, Rechnungen oder printf musst Du wissen, wie groß int> ist oder größeren Aufwand treiben.>> Beispiele: (keine Zeile funktioniert wie erwartet)>> uint16_t u=10000*8/4;>> uint16_t u2=100; int16 i=-1; if(i<u2) ...>> printf("%u, %u", u, u2);
Kommt drauf an, woher deine Erwartungen kommen :-)
Wenn man die Formatkürzel aus inttypes.h nimmt für die (u)intxx_t
anstatt der falschen Formatbeschreibungen wie in deinem Beispiel,
entspricht es zumindest meinen Erwartungen.
Rolf M. schrieb:>> Trotzdem hängt das Verhalten von der Größe von int ab.>> Klar, man muss die Mindestgrößen eben kennen und darf für portablen Code> int nicht für Werte größer 32767 verwenden.
Du hast die Beispiele anscheinend nicht verstanden: Es geht nicht um den
Wertebereich der Variablen (der ist fest), sondern dass (obwohl int
nirgendwo auftaucht) das Verhalten auf Plattformen mit 16 Bit ein völlig
anderes ist als auf Plattformen mit 32 Bit.
Klaus W. schrieb:> Kommt drauf an, woher deine Erwartungen kommen :-)
Wie Du an anderen Kommentaren siehst, sind sich einige der
unterschiedlichen Verhalten nicht mal bewusst. Da ist ganz leichtfertig
ein "immer uintxx_t" gefällt und plötzlich sieht man sich damit
konfrontiert, sämtliche Formatspecifier umzubauen. Noch schlimmer, von
reinen Strings im Quelltext auf weniger flüssige Konstrukte zu wechseln
(die ggf. auch schwieriger zu finden sind)
> Wenn man die Formatkürzel aus inttypes.h nimmt für die (u)intxx_t> anstatt der falschen Formatbeschreibungen wie in deinem Beispiel,> entspricht es zumindest meinen Erwartungen.
Aber nur das printf. Die beiden anderen Zeilen hängen noch immer von der
int-Größe ab.
Ja, es gibt für alles Workarounds, klar. Doch irgendwo in der Matrix
[Warnungen/Casts/implizite Typumwandlungen/..] hakt es dann doch, wenn
ich den gleichen Code auf 16- und 32-Bit-Plattformen laufen lassen will.
Und das ist z.B. immer dort der Fall, wo der 16er-µC-Code auch am PC
läuft.
A. S. schrieb:> Rolf M. schrieb:>>> Trotzdem hängt das Verhalten von der Größe von int ab.>>>> Klar, man muss die Mindestgrößen eben kennen und darf für portablen Code>> int nicht für Werte größer 32767 verwenden.>> Du hast die Beispiele anscheinend nicht verstanden:
Ich glaube, ich habe missverstanden, was du damit sagen wolltest.
> Es geht nicht um den Wertebereich der Variablen (der ist fest), sondern> dass (obwohl int nirgendwo auftaucht) das Verhalten auf Plattformen mit> 16 Bit ein völlig anderes ist als auf Plattformen mit 32 Bit.
int taucht quasi implizit in den Integer-Literalen auf. Dazu kommt dann
noch ggf. Integer-Promotion.
Rolf M. schrieb:> int taucht quasi implizit in den Integer-Literalen auf.
OK, dann ausführlich:
A. S. schrieb:> uint16_t u=10000*8/4;
Nur bei 32 Bit 40.000, sonst etwa 3600
> uint16_t u2=100; int16 i=-1; if(i<u2) ...
Bei 32 Bit erfüllt, bei 16 nicht.
> printf("%u, %u", u, u2);
Bei beiden OK ;-)
A. S. schrieb:> Rolf M. schrieb:>> int taucht quasi implizit in den Integer-Literalen auf.>> OK, dann ausführlich:
Warum?
> A. S. schrieb:>> uint16_t u=10000*8/4;> Nur bei 32 Bit 40.000,
Ich hätte ja 20.000 erwartet…
> sonst etwa 3600
Ja natürlich, weil 10000, 8 und 4 vom Typ int sind. Wie ich sagte: int
taucht in den Literals implizit auf.
>> uint16_t u2=100; int16 i=-1; if(i<u2) ...> Bei 32 Bit erfüllt, bei 16 nicht.
Ja, auch wie ich sagte: Integer Promotion.
>> printf("%u, %u", u, u2);> Bei beiden OK ;-)
Ja, ist in dem Fall ok, weil unsigned int nicht kleiner sein kann als
uint16_t, und wenn es größer sein sollte, macht das auf Grund der
Integer Promotion nichts. Es kann aber trotzdem zu einer
Compiler-Warnung führen.
Auch da kommt der "Rechenfehler" nicht ganz überraschend, würde ich mal
sagen!
uint16_t u=10000UL*8/4;
Das UL kostet einen doch nichts.....
Und "Zack" läuft es auf jedem Kesselchen, wie man sich das wünscht.
(wenn denn uint16_t darauf funktioniert)
Arduino Fanboy D. schrieb:> Auch da kommt der "Rechenfehler" nicht ganz überraschend, würde ich mal> sagen!
Ja klar geht das alles. Hab ich ja auch geschrieben. Und auch auf jene
eingegrenzt, die nur wenige Warnungen haben, z.B. -Wall.
Die Diskussionen sind nur dann immer die gleichen, wenn es nicht geht:
"Ne, auf dem PC gehts ja, ... und der Wertebereich ist auch nicht
überschritten mit 40.000, ... Der Compiler ist kaputt, der kann kein *".
A. S. schrieb:> Die Diskussionen sind nur dann immer die gleichen, wenn es nicht geht:> "Ne, auf dem PC gehts ja, ... und der Wertebereich ist auch nicht> überschritten mit 40.000, ... Der Compiler ist kaputt, der kann kein *".
Naja...
Das ist ja auch kein Wunder....
Da muss(te) jeder mal durch!
Nicht einer für alle, sondern jeder einzeln für sich.
Beim ersten mal, wird einem danach klar, dass es doch Sinn macht, die
Sprachreferenz mal durchzublättern.
Beim zweiten mal kann man sich in den Arsch beißen.
Und bei jedem weiteren mal, sieht man es schon beim schreiben.
Der ganz normale Weg...
Einige der "Erstlinge" tauchen hier auf, was dann scheinbar zu "immer
die gleichen Diskussionen" führt. Sind es aber nicht, da sich die
Individuen unterscheiden.
nein, weder noch.
Mit dem Compilerschalter meinst du die Entscheidung, ob char als signed
char oder als unsigned char betrachtet werden soll.
(u)int8_t hat damit nicht mehr zu tun, als daß bei den allermeisten
Systemen die Länge gleich ist wie bei den char-Typen.
Wobei man die Feinheit beachten sollte, dass char zwar entweder das
Vorzeichenverhalten von signed char oder von unsigned char hat, aber
char
unsigned char
signed char
im Typsystem trotzdem drei formal verschiedene Typen darstellen.
Da (u)int8_t in stdint.h üblicherweise auf (un)signed char abgebildet
wird, ist char also mit beiden nicht identisch.
In C wird man oft erst dann damit konfrontiert, wenn man die Warnungen
einschaltet und dann vielleicht darauf hingewiesen wird, dass die
"signedness" sich von beiden Alternativen unterscheidet.
In C++ spielt das eine grössere Rolle.
(prx) A. K. schrieb:> In C++ spielt das eine grössere Rolle, weil man sonst bei Templates> schnell gegen die Wand läuft.
Da gibts dann aber ein is_signed<char>::value oder auch
is_signed<char>() für.
Und seine ganzen Brüder.
(prx) A. K. schrieb:> In C++ spielt das eine grössere Rolle, weil man sonst bei Templates> schnell gegen die Wand läuft.
Oder bei überladenen Funktionen. So bricht der Compiler hier z.B. mit
Fehler ab, weil er nicht weiß, welche Überladung er nehmen soll:
1
voidfoo(signedchar)
2
{
3
}
4
5
voidfoo(unsignedchar)
6
{
7
}
8
9
intmain()
10
{
11
charc=5;
12
foo(c);
13
}
Andererseits erstaunt dann, dass bei einem:
1
charc=65;
2
signedchard=66;
3
unsignedchare=67;
4
uint8_tf=68;
5
int8_tg=69;
6
7
std::cout<<c<<d<<e<<f<<g<<'\n';
sämtliche Variablen als Text ausgegeben werden. Man könnte eigentlich
erwarten, dass die Überladungen des <<-Operators so definiert sind, dass
nur die erste als Text ausgegeben wird.
Arduino Fanboy D. schrieb:>> In C++ spielt das eine grössere Rolle, weil man sonst bei Templates>> schnell gegen die Wand läuft.>> Da gibts dann aber ein is_signed<char>::value oder auch> is_signed<char>() für.> Und seine ganzen Brüder.
Es geht freilich nicht nur um das Vorzeichen. Sondern auch um Overloads
und Templates, in denen das drei verschiedene Typen sind. Und man das
nicht vergessen darf.
An dieser Stelle wird es bei C vs C++ noch etwas tricky. Denn in C ist
in alter Tradition 'x' vom Typ int - nicht etwa char, wie man annehmen
sollte. Hätte man das in C++ genauso durchgezogen, wären
char c;
f(c); // f(char)
f('x'); // C: f(int), C++: f(char)
verschiedene Fälle. Weshalb in C++ 'x' vom Typ char ist.
Rolf M. schrieb:> Man könnte eigentlich> erwarten, dass die Überladungen des <<-Operators so definiert sind, dass> nur die erste als Text ausgegeben wird.
Dann macht Arduino ja doch mal was richtig..... ;-)
1
#include<Streaming.h> // die Lib findest du selber
Arduino Fanboy D. schrieb:> Da muss(te) jeder mal durch!> Nicht einer für alle, sondern jeder einzeln für sich.
+1
Man hat hier sonst immer den Eindruck, dass sowas gar nicht passieren
darf und man vor seinem ersten "Hallo Welt" doch zumindest (alles)
verstanden haben sollte.
Thomas Z. schrieb:> - vorzeichenlose Typen verwenden wo möglich
Das ist unnötig gefährlich.
Sehr viele Berechnungen kommen teilweise in's Negative. Der Klassiker
sind rückwärts zählende Schleifen.
Vorzeichenlose Typen produzieren gerne schwer zu findende Bugs.
> Für 8 Bit CPUs gilt> - so kurz wie möglich uint8_t ist efektiver als uint16_t
In den allermeisten Fällen ist das eine unnötige Einschränkung. Auf
8-Bit-CPUs ist bei den meisten Operationen nur ein einfacher Takt und
Opcode mehr notwendig, für eine 16-Bit-Operation.
Ein int16_t sollte auf 8-Bit-CPUs der "Standard-Datentyp" sein. Damit
kann man in der Regel den kompletten RAM addresieren. Die Größe passt
gut zu einem 8-Bit-System.
Experte schrieb:> 8-Bit-CPUs ist bei den meisten Operationen nur ein einfacher Takt und> Opcode mehr notwendig, für eine 16-Bit-Operation.
CPUs mit einem einzigen 8-Bit Akkumulator magst du nicht
berücksichtigen?
Kevin M. schrieb:> Thomas Z. schrieb:>> beim ARM ist das allerdings nicht mehr so. Ich habe aber gemerkt, dass>> auch dort in vielen Fällen mit kurzen Variablen kompakterer Code>> generiert wird.>> Zauberei?
Vorab: genau wissen tu ich es nicht.
Aber: ich gehe davon aus, dass bei kleinerem Wertebereich der Compiler
mehr Tricks anwenden kann.
D.h. selbst wenn der Compiler ein int16_t und ein int32_t auf ARM beide
in 4 Byte speichert, weil es eine 32-bit-Architektur ist, so kann er
später doch zusätzliche Optimierungen anwenden. Weil der Compiler genau
weiß, dass dein Programm niemals etwas größeres als 32767 speichern
wird. (Das wäre nämlich undefined behaviour und der Compiler wäre dann
frei, irgendwas beliebiges zu machen)
Das klingt jetzt vielleicht unglaubwürdig, weil, welche Tricks sollen
das sein, wenn mir keiner einfällt?
Hier empfehle ich folgendes Video:
https://www.youtube.com/watch?v=w0sz5WbS5AM
Das ist die Keynote der Konferenz C++ on Sea 2019:
What Everyone Should Know About How Amazing Compilers Are - Matt Godbolt
Tilo R. schrieb:> Aber: ich gehe davon aus, dass bei kleinerem Wertebereich der Compiler> mehr Tricks anwenden kann.
Es gibt beide Effekte.
Der andere zeigt sich bei:
uint16_t x;
x++;
denn ARMs haben keine 16-Bit Register.
Fall 1: x besteht ausschliesslich aus den unteren 16 Bits, die oberen
sind undefiniert. Dann kann man mit dem Register oft nicht rechnen.
Fall 2: x ist mit Nullen erweitert im Register gespeichert. Dann muss
man den Überlauf aus den unteren 16 Bits berücksichtigen, der ggf den
zulässigen Wertebereich verletzt. Also hinterher maskieren.
Auch aufgrund solcher Sauereien gibt es uint_fast16_t.
Und zur Optimierung solcher Sauereien ist Überlauf mit Vorzeichen
undefiniert. Rechne damit, dass dann ein Wert in x stehen kann, der da
eigentlich nicht reinpasst - statt dem intuitiv angenommenen
Modulo-Verhalten. Das erspart die Reduktion auf 16-Bits, mit
Nebeneffekt.
Experte schrieb:> Sehr viele Berechnungen kommen teilweise in's Negative. Der Klassiker> sind rückwärts zählende Schleifen.>> Vorzeichenlose Typen produzieren gerne schwer zu findende Bugs.
In der Regel ist es genau umgekehrt. Signed-Typen dürfen NICHT
überlaufen. Man muss z.B. vor einem ++ abfragen, ob das passiert. Bei
einer Rechnung sowieso.
Konstrukte wie "while(u-->=0)" sind keine Bugs sondern Arroganz des
Programmierers der alle Warnungen in den Wind schlägt.
Ich habe nichts gegen Schleifenzähler int i. Aber meist reicht unsigned
und ist auch einfach sauberer. Z.B. weil bei Arrays die Prüfung auf
beide Grenzen wegfällt (i>=0 && i<N). Weil der %-Operator über den
ganzen Wertebereich gleichmäßig funktioniert. Weil ein Überlauf
definiertes Verhalten hat. Weil eine Division durch 2^n durch >> ersetzt
werden kann, ...
A. S. schrieb:> In der Regel ist es genau umgekehrt.
Andererseits muss der Compiler bei Rechnung ohne Vorzeichen u.U. das
definierte Modulo-Verhalten berücksichtigen. Mit Vorzeichen kann er auf
Teufel komm raus rechnen wie seine Bits gewachsen sind.
Ist nicht so einfach zu erkennen, was summarum besser ist.
(prx) A. K. schrieb:> Mit Vorzeichen kann er auf> Teufel komm raus rechnen wie seine Bits gewachsen sind.
Du meinst, weil Überträge dann UB sind? Dass solltest Du aber mit einem
Ironietag versehen.
A. S. schrieb:> Du meinst, weil Überträge dann UB sind? Dass solltest Du aber mit einem> Ironietag versehen.
Das war ausnahmsweise nicht wirklich ironisch. UB bei Rechnung mit
Vorzeichen ist ein grösseres Risiko für unvorsichtige Programmierer,
kann aber effizienteren Code zur Folge haben.
A. S. schrieb:> In der Regel ist es genau umgekehrt. Signed-Typen dürfen NICHT> überlaufen. Man muss z.B. vor einem ++ abfragen, ob das passiert. Bei> einer Rechnung sowieso.>> Konstrukte wie "while(u-->=0)" sind keine Bugs sondern Arroganz des> Programmierers der alle Warnungen in den Wind schlägt.
Vollkommen korrekt!
Mein Beispiel war mit Bedacht gewählt. Der Signed-Überlauf ist undefined
behaviour (im Gegensatz zu unsigned, wo das definiert ist). Es ist pures
Glück, dass es so oft funktioniert.
Und wenn man ganz großes Pech hat, ist jahrelang funktionierender Code
plötzlich kaputt, weil der neue Compiler eine zulässige,
standardkonforme Optimierung macht.
Es ist ziemlich praxisfern zu argumentieren, dass unbeabsichtigte
Überläufe mit undefinierten Verhalten schlimmer wären als
unbeabsichtigte Überläufe mit definierten Verhalten.
In den allermeisten Fällen ist ein Überlauf in einem Programm eine
Katastrophe.
Und genau hier liegt der Knackpunkt bei unsigned: Man arbeitet ständig
an der "Klippe", der Null rum. Wenn durch einen Rundungsfehler in
Berechnungen eine Ergebnis "etwas" unter Null rutscht, ist bei unsigned
die Katastrophe vorprogrammiert, und bei signed oft folgenlos.
A. S. schrieb:> Konstrukte wie "while(u-->=0)" sind keine Bugs sondern Arroganz des> Programmierers der alle Warnungen in den Wind schlägt.
Wenn 'u' ein vorzeichenbehafteter Datentyp ist, wirst Du niemals eine
Compilerwarnung bekommen, weil einfach nichts passieren kann. Das ist
deutlich robuster als einen Datentyp zu verwenden, der potentiell
gefährlich ist.
Zusätzlich ist bei vorzeichenlosen Datentypen die mentale Belastung von
Programmierern höher, da sie sicherstellen müssen, niemals in den
negativen Bereich zu rutschen.
Jede "räumliche Nähe" von vorzeichenlosen Datentypen und Minuszeichen
ist eine scharfe Tretmine, die nur darauf wartet, ausgelöst zu werden.
Fehler in Software haben einen "Hard-Fact" als letzten Auslöser, z.B.
einen Unterlauf einer vorzeichenlose Variable, aber der Weg dorthin wird
geebnet durch unzählige "Soft-Facts", und dazu gehört die Verwendung
eben solcher Datentypen die solche Unterläufe (in der Nähe der Null)
erst ermöglichen.
Experte schrieb:> Es ist ziemlich praxisfern zu argumentieren, dass unbeabsichtigte> Überläufe mit undefinierten Verhalten schlimmer wären als> unbeabsichtigte Überläufe mit definierten Verhalten.
Ganz im Gegenteil!
Ein definiertes Verhalten ist eine absolute Notwendigkeit.
Das undefinierte Verhalten, ist da eher auf dem Niveau einer tödlichen
Falle.
Experte schrieb:> Wenn 'u' ein vorzeichenbehafteter Datentyp ist, wirst Du niemals eine> Compilerwarnung bekommen,
Da kommt sowas wie „Ausruck ist immer wahr“
Arduino Fanboy D. schrieb:> Experte schrieb:>> Es ist ziemlich praxisfern zu argumentieren, dass unbeabsichtigte>> Überläufe mit undefinierten Verhalten schlimmer wären als>> unbeabsichtigte Überläufe mit definierten Verhalten.>> Ganz im Gegenteil!> Ein definiertes Verhalten ist eine absolute Notwendigkeit.
In beiden Fällen baut dein Programm Mist. Im zweiten ist es immerhin
definierter Mist.
Experte schrieb:> In den allermeisten Fällen ist das eine unnötige Einschränkung. Auf> 8-Bit-CPUs ist bei den meisten Operationen nur ein einfacher Takt und> Opcode mehr notwendig, für eine 16-Bit-Operation.>> Ein int16_t sollte auf 8-Bit-CPUs der "Standard-Datentyp" sein. Damit> kann man in der Regel den kompletten RAM addresieren. Die Größe passt> gut zu einem 8-Bit-System.
NACK.
Sorry, aber wer nicht im Griff hat, welche Werte seine Daten annehmen
können, kann auch mit int16_t Probleme bekommen. Es ist nur weniger
Wahrscheinlich.
Es gilt: Als Datentype bevorzugt die "Datengröße" des µC, uint8_t bei
8bit, uint16_t bei 16bit etc. Datentype ansolsten so groß wie nötig, so
klein wie möglich.
Gruß
Robert
Der Robs schrieb:> Datentype ansolsten so groß wie nötig, so> klein wie möglich.
Oder so schnell wie möglich, oder so aligned wie möglich, oder...
Pauschalaussagen sind da nutzlos.
Oliver
Oliver S. schrieb:> Der Robs schrieb:>> Datentype ansolsten so groß wie nötig, so klein wie möglich.>> Oder so schnell wie möglich, oder so aligned wie möglich, oder...
Und idealerweise sich automatisch an die konkrete Plattform anpassend,
wo es keinen konkreten Grund gibt, warum das an der Stelle nicht geht.
> Pauschalaussagen sind da nutzlos.