PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : int Variable, Grenzbereiche



frabe
08.05.2020, 15:38
Hallo - folgend mal wieder Basiswissen;
Was passierte wenn Variablen "unbeabsichtigt" über ihr Grenzbereiche hinaus laufen?

int Bsp1 = 1/2; // wird 0 gespeichert und der Dezimalwert ignoriert?
int Bsp2 = 3/2; // wird 1 gespeichert und der Dezimalwert ignoriert?
unsigned int Bsp3 = 1-2; // wird 1 gespeichert und das Minus ignoriert?
unsigned int Bsp4 = 2-5; // wird 3 gespeichert und das Minus ignoriert?

Was ist wenn solche Fehler innerhalb einer Formal "unbeabsichtigt" vorkommen, das Endergebnis aber eine korrekten int Var. ergibt?

int Bsp5 = 3/2*2; // wir 3 gespeichert?
unsigned int Bsp6 = 1-2+10; // wird 9 gespeichert?

PS: Mir ist bewusst, dass man die oberen Bsp generell mit "float" und ohne "unsigned" umgehen kann.

Holomino
08.05.2020, 16:35
Abgesehen davon, dass der Controller gar nix rechnet, weil der Precompiler Deine Literale schon weit vorher auflöst und der Optimizer die Variablen wegen Nichtverwendung gleich wieder aus dem Code kickt:

Warum probierst Du solche Dinge nicht einfach selber (in Terminal, Console, Simulator, Listing, auf dem PC, ...) aus? Wenn Du dann noch Fragen hast, …

frabe
08.05.2020, 17:58
Natürlich stehe die reellen Zahlen in den Bsp für mögliche Variablenwerte.
Und warum das Rad neu erfinden...

Interessant in diesem Zusammnehang;
short Zahl = 32768; // Wert 32768 gespeichert
Zahl++; // statt 32769, Wert -32767 gespeichert
Quelle: http://www.c-howto.de/tutorial/variablen/datentypen/ganze-zahlen/

HaWe
08.05.2020, 19:06
das ist ja alles noch triviales Grundwissen - dazu gehört auch, was bei Überlauf passiert und dass die Nachkommastellen bei Integer-Division abgeschnitten werden ;)

holomino hat aber völlig Recht:
du kannst dir alle deine Fragen selber beantworten, wenn du deine Rechnungen per Serial.println() zur Kontrolle ausgeben lässt.

interessant wird es, wenn du mit
char
rechnest, denn auf AVRs entspricht
char == int8_t (signed)
aber auf ARM (SAM, SAMD) und ESPs ist
char == uint8_t (unsigned) !!

Außerdem unterscheidet sich der Wertebereich von int, short, long je nach der verwendeten MCU.

Besser: <stdint.h> bzw. besser noch <inttypes.h> Datentypen verwenden (int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t), denn die funktionieren plattformübergreifend identisch.

Holomino
08.05.2020, 19:21
Doch, letztlich musst Du en Detail das Rad für jede Plattform und jede Programmiersprache neu erfinden, weil sowohl Datentypen (int = 16 oder 32Bit) als auch das Verhalten der Bibliotheken (in C# ist ein int z.B. ein Objekt, das bei einem Overflow eine entsprechende Exception auslöst) unterschiedlich sein können. Auf anderen Systemen, als in Deinem Link angegeben, erfolgt der Überlauf z.B. schon bei 32767++.

Wenn man also eine neue Plattform in Betrieb nimmt und keine Lust hat, Specs zu lesen, probiert man es aus.

HaWe
08.05.2020, 19:23
Doch, letztlich musst Du en Detail das Rad für jede Plattform und jede Programmiersprache neu erfinden, weil sowohl Datentypen (int = 16 oder 32Bit) als auch das Verhalten der Bibliotheken (in C# ist ein int z.B. ein Objekt, das bei einem Overflow eine entsprechende Exception auslöst) unterschiedlich sein können. Auf anderen Systemen, als in Deinem Link angegeben, erfolgt der Überlauf z.B. schon bei 32767++.

Wenn man also eine neue Plattform in Betrieb nimmt und keine Lust hat, Specs zu lesen, probiert man es aus.
nein, muss man nicht, wenn man <stdint.h> bzw. <inttypes.h> Datentypen verwendet, die gelten ja gerade plattformübergreifend.
Im Gegensatz zu char, int, short, long etc.
und wir reden ja über C/C++ hier im Arduino-Subforum, daher ist C# offtopic.

frabe
09.05.2020, 09:16
Datentypen, ein spannendes und wirklich wichtiges Thema.
So dumm war meine obige Frage gar nicht...
Das es je nach Plattform unterschiedliche Ausprägungen gibt, beantwortet auch, warum so wenig dieser Grenzüberschreitungen im Netz steht.

Im aktuellen Fall habe ich die Unsicherheit mit typecast beseitigt.


...
float PWMschritt = PWMbereich / FadeZeit;
int PWMakt = (int)(AktZeit * PWMschritt + 0.5);
...

Vielleicht kehre ich aber auch wieder zu <stdint.h> bzw. <inttypes.h> Datentypen zurück.
Fand ich als Anfänger aber etwas unübersichtlicher (unleserlicher).

DANKE für eure Inspirationen!!!

HaWe
09.05.2020, 10:16
Grundsätzlich sind die Phänomene wie Überlauf oder Nachkommastellen-Abschneiden bei integer-Division NICHT von der Bitbreite der Variablen abhängig, sie treten stattdessen IMMER auf.
Deine Beispiele oben mit Bsp1-Bsp6 und mit deinem short Beispiel treten daher auch genauso auf, wenn du mit <stdint.h> bzw. <inttypes.h> Datentypen arbeitest - du kannst nur besser vorausplanen beim Programmieren 8)

Sisor
09.05.2020, 13:22
int Bsp5 = 3/2*2; // wir 3 gespeichert?
Sicher? Sollte 2 sein, nicht 3.

Holomino
09.05.2020, 16:33
Sicher? Sollte 2 sein, nicht 3.

Vorsicht!
Die Auflösung des Literals wird vom Precompiler übernommen.
Für das Fragment:
int main (void)
{

int8_t test = 3.1415/2 *2;
if (test == 3)
SetLED2();

bekomme ich vom Compiler im AVR-GCC weder eine Warnung, noch einen Fehler. Im Listing sehe ich aber:

000028b6 <main>:
static inline void SetLED2()
{
PORTD.OUTSET = 0x02; }
28b6: c0 e6 ldi r28, 0x60 ; 96
28b8: d6 e0 ldi r29, 0x06 ; 6
28ba: 12 e0 ldi r17, 0x02 ; 2
28bc: 1d 83 std Y+5, r17 ; 0x05

Da wird also weder eine Variable angelegt, noch im ganzzahligen Bereich, wie auf der eigentlichen Maschine, gerechnet.
Insofern sind die oben angegebenen Beispielzeilen nicht wirklich tauglich, die Sache zu durchschauen.

Besser (so, wie erwartet) wird's mit:
int main (void)
{
int8_t test = 3.1415;
test/=2;
test*=2;
if (test == 3)
SetLED2();

Hier castet sich der fraktale Anteil im Precompiler weg:
test = 3.1415 ->3
test /= 2 ->1
test *= 2 ->2

Es wird vom Compiler im Listing zwar immer noch keine Variable angelegt, aber der Aufruf von SetLED2() kommt auch nicht mehr.

HaWe
09.05.2020, 17:23
Sicher? Sollte 2 sein, nicht 3.
ja, stimmt ntl!



void setup() {
Serial.begin(115200);
delay(1000);
Serial.println();


int x=3;
int y=2;
int z=2;

int Bsp5 = 3/2*2;
Serial.println(Bsp5);

Bsp5 = x/y*z;
Serial.println(Bsp5);

}

void loop() {


}

output:



2
2



edit: sollte durchgestrichen sein, aber Durchstreichfunktion existiert hier nicht: da es alles Konstanten sind, wird die "Berechnung" evtl vom Preprozessor erledigt, ohne Compiler

Aber auch mit volatile int kommt erwartungsgemäß dasselbe Ergebnis.


void setup() {
Serial.begin(115200);
delay(1000);
Serial.println();


volatile int x=3;
volatile int y=2;
volatile int z=2;

volatile int Bsp5 = 3/2*2;
Serial.println(Bsp5);

Bsp5 = x/y*z;
Serial.println(Bsp5);

}

void loop() {


}

Sisor
09.05.2020, 18:36
Der Präprozessor rechnet nicht. Er bearbeitet Anweisungen zum Einfügen von Quelltext (#include), zum Ersetzen von Makros (#define), und bedingter Übersetzung (#if).

#define CELSIUS_ZU_FAHRENHEIT( t ) ( ( t ) * 1.8 + 32 )

Das Makro CELSIUS_ZU_FAHRENHEIT beschreibt die Umrechnung einer Temperatur (angegeben als Parameter t) aus der Celsius- in die Fahrenheit-Skala. Auch ein Makro mit Parametern wird im Quelltext ersetzt:


int fahrenheit, celsius = 10;
fahrenheit = CELSIUS_ZU_FAHRENHEIT( celsius + 5 );

wird durch den C-Präprozessor ersetzt zu:

int fahrenheit, celsius = 10;
fahrenheit = ( ( celsius + 5 ) * 1.8 + 32 );
Quelle (https://de.wikipedia.org/wiki/C-Pr%C3%A4prozessor)
Danach darf der Compiler gemäß der spezifizierten Rechenregeln weitermachen.

HaWe
09.05.2020, 18:50
Der Präprozessor rechnet nicht.
danke, da war ich einem Irrtum aufgesessen, ich dachte wirklich dass er bereits bei konstanten Ausdrücken wie
3/2*2
quasi das konstante Zwischenergebnis einsetzt - tut also doch immer erst hinterher der Compiler.

Sisor
09.05.2020, 20:52
Vorsicht!
Die Auflösung des Literals wird vom Precompiler übernommen.
Für das Fragment:
int main (void)
{

int8_t test = 3.1415/2 *2;
if (test == 3)
SetLED2();

bekomme ich vom Compiler im AVR-GCC weder eine Warnung, noch einen Fehler. Im Listing sehe ich aber:

000028b6 <main>:
static inline void SetLED2()
{
PORTD.OUTSET = 0x02; }
28b6: c0 e6 ldi r28, 0x60 ; 96
28b8: d6 e0 ldi r29, 0x06 ; 6
28ba: 12 e0 ldi r17, 0x02 ; 2
28bc: 1d 83 std Y+5, r17 ; 0x05

Da wird also weder eine Variable angelegt, noch im ganzzahligen Bereich, wie auf der eigentlichen Maschine, gerechnet.
Insofern sind die oben angegebenen Beispielzeilen nicht wirklich tauglich, die Sache zu durchschauen.

Besser (so, wie erwartet) wird's mit:
int main (void)
{
int8_t test = 3.1415;
test/=2;
test*=2;
if (test == 3)
SetLED2();

Hier castet sich der fraktale Anteil im Precompiler weg:
test = 3.1415 ->3
test /= 2 ->1
test *= 2 ->2

Es wird vom Compiler im Listing zwar immer noch keine Variable angelegt, aber der Aufruf von SetLED2() kommt auch nicht mehr.

Wenn du mit Precompiler Präprozessor meinst, der rechnet nicht. GCC hat keinen Precompiler für C/C++. Die Auswertung der Literale erfolgt im Compiler.
Bei int t = 3.1415/2 *2 ist 3.1415 ein float und 2 ein int, damit ist das Ergebnis aus 3.1415/2 ein float, damit ist das 3.1415/2 * 2 auch ein float. Das wird dann zum int (3) gecastet.
Bei int u = 3/2 *2 sind 3 und 2 int, damit ist 3/2 ein int (1), damit ist 1*2 ein int (2). Das braucht nicht mehr gecastet werden.

Regeln-fuer-Konvertierungen-und-Casts (https://www.heise.de/developer/artikel/C-Core-Guidelines-Regeln-fuer-Konvertierungen-und-Casts-3965134.html)

Moppi
12.05.2020, 08:47
Datentypen, ein spannendes und wirklich wichtiges Thema.
So dumm war meine obige Frage gar nicht...
Das es je nach Plattform unterschiedliche Ausprägungen gibt, beantwortet auch, warum so wenig dieser Grenzüberschreitungen im Netz steht.

Im aktuellen Fall habe ich die Unsicherheit mit typecast beseitigt.


...
float PWMschritt = PWMbereich / FadeZeit;
int PWMakt = (int)(AktZeit * PWMschritt + 0.5);
...

Vielleicht kehre ich aber auch wieder zu <stdint.h> bzw. <inttypes.h> Datentypen zurück.
Fand ich als Anfänger aber etwas unübersichtlicher (unleserlicher).

DANKE für eure Inspirationen!!!

Man findet schon was zum Thema Datentypen. Überlauf, Bitbreite etc. sind erste Grundlagen der Programmierung. Wer in Assembler bzw. mit Maschinensprache anfängt MUSS die Grundlagen lernen. Inkrementierung und Dekrementierung geht i.R. mit einem Überlauf eines Registers einher, das eine bestimmte Bitbreite hat. Ein 8Bit-Register hat 8 Bit. Das sind 256 mögliche Werte, von 0 bis 255. Das bedeutet, wenn in einem Register alle Bits gesetzt sind und eine Inkrementierung auf das Register stattfindet (80x86 bspw.: INC AL), dass das Register auf "0" springt, weil ein Überlauf stattfindet. Aber das ist nicht alles. Dazu kommt, dass beim Überlauf ein Überlauf-Bit (Carry-Flag) gesetzt wird. Es gibt weitere Befehle, die dieses Bit verrechnen können. ADC (80x86) steht zum Beispiel für Addition mit Carry. Auf diese Weise lässt sich, mit wenigen Bit eines Registers, mit riesigen Zahlen rechnen, die niemals in dieses Register vollständig hinein passen würden. Später gab es mathematische Coprozzessoren mit Registern die eine wesentlich größere Bitbreite hatten.

In einer Hochsprache ist das aber eher unwichtig. Hier geht es mehr darum, die Grenzwerte der Datentypen zu kennen und sich innerhalb dieser zu bewegen. Für eine Berechnung eines Wertes brauche ich also den passenden Datentyp, wenn es keine Überraschungen geben soll.



MfG

frabe
12.05.2020, 10:53
Wertes brauche ich also den passenden Datentyp, wenn es keine Überraschungen geben soll.

DAS ist das A und O bei allen Deklarationen und Zwischenberechnungen - das Fazit - niemals zulassen das ungereimtheiten auftreten können.
Nur ist es nict immer so leichte, besonders wenn man bestehende Codes anpasst oder anch langer Zeit mal wieder anderweitig verwenden möchte.

Klebwax
12.05.2020, 12:55
Nur ist es nict immer so leichte, besonders wenn man bestehende Codes anpasst oder anch langer Zeit mal wieder anderweitig verwenden möchte.

Mit ein wenig Disziplin geht das schon. Das fängt damit an, daß man unsigned Typen vermeidet. Mathematisch machen sie keinen Sinn, eigentlich sind sie Bitfelder wie SFRs oder Bitsequenzen eines Übertragungsprotokolls. Darauf sollte man keine arithmetischen Operationen sondern nur logische Operatoren anwenden. Wenn man Werte daraus im Programm weiter verwenden will, sollte man sie so schnell wie möglich in Zahlen umwandeln. Man sollte generell mit Objekten, die keine Zahlen sind, nicht rechnen. Daher ist es auch unerheblich ob chars signed oder unsigned sind. 'a' + 'b' macht keinen Sinn, es mag zwar in C ein Ergebnis geben, in anderen Sprachen eher nicht. Die einzige arithmetische Operation die auf chars Sinn macht ist '0' + [0..9]. Sie liefert das Zeichen für die Ziffern 0 bis 9 und das unabhängig davon, ob chars signed oder unsigned sind oder der Zeichensatz ASCII oder EBCDIC ist. Wenn man aus Platz oder Geschwindigkeitsproblemen 8-Bit Variable für nötig hält, verwendet man int8_t. Will man auf allen Architekturen schnell sein, sollte man int_fast8_t verwenden.

Ansonsten ist ein int immer mindestens 16 Bit. Wenn man portabel programmiert, verlässt man sich nicht auf das Verhalten bei einem Über oder Unterlauf. Man sollte also nicht davon ausgehen, daß 32767 + 1 = -32768 ist. Wenn man das so will, sollte man es explizit so programmieren. Dann läuft das Programm auch, auf einem System mit 32 oder 64 Bit Integer. Wenn man so programmiert, kommt man sogar mit den klassischen C-Typen int, long int (32 Bit) oder long long (64 Bit) aus. Besser lesbar sind sicher die Typen aus stdint.h wie int32_t und int64_t.

Daß das geht, sieht man an vielen Programmen. Beim Umstieg von 32 auf 64 Bit mussten sie nur neu kompiliert und gegen die 64 Bit Library gelinkt werden. Wer aber trickst, Überläufe explizit einkalkuliert, Shifts verwendet statt zu teilen, oder zu addieren statt bitweise zu odern, hat da eher Schwierigkeiten.

MfG Klebwax