std.conv — Zahlensysteme und Bitoperationen

Zurück zur Unit-Übersicht

Zwei Themen: Zahlen zwischen Textform und Wert wandeln (hexadezimal, binär, oktal, mit Tausendertrennzeichen) und auf einzelnen Bits arbeiten — Bits setzen und lesen, Bitfelder ausschneiden, Nullbits zählen, Byte-Reihenfolge tauschen, Vorzeichen erweitern, Adressen ausrichten.

Alles rechnet auf int64. Die Unit ist der Werkzeugkasten für hardwarenahe Arbeit: Protokollfelder, Register, Binärformate, Fehlersuche.

Autor: Andreas Röne
Copyright: 2024–2025 Andreas Röne
Quelle: std/conv.lyx

<WRAP alert> RotateLeft32 maskiert nicht auf 32 Bit — 0x80000000 «< 1 ergibt 4294967297 statt 1 (#1495). RotateRight32 arbeitet korrekt.

ParseHex, ParseBin und ParseOct melden ungültige Eingaben nicht und behandeln Störzeichen uneinheitlich (#1500). Details unter Fallstricke. </WRAP>

Import

import std.conv;


Die Operatoren der Sprache

Für die Grundrechenarten auf Bits braucht es keine Unit — Lyx hat sie eingebaut:

Operator Bedeutung Beispiel
& UND 12 & 10 = 8
| ODER 12 | 10 = 14
&#94; XOR 12 &#94; 10 = 6
~ Komplement ~12 = −13
&lt;&lt; Linksschieben 1 &lt;&lt; 10 = 1024
>> Rechtsschieben 1024 >> 3 = 128
|~ NOR 12 |~ 10 = −15

Zwei Eigenheiten, die man kennen muss:

>> schiebt arithmetisch. Das Vorzeichenbit wird nachgezogen: −16 >> 2 ergibt −4, −1 >> 1 bleibt −1. Für ein logisches Rechtsschieben — bei dem Nullen nachrücken — muss der Wert vorher umgedeutet werden:

var logisch: int64 := (wert as uint64) >> n;

Genau so macht es diese Unit intern in SwapEndian* und RotateRight32.

Schiebeweiten werden modulo 64 genommen. 1 &lt;&lt; 64 ergibt 1, 1 &lt;&lt; 65 ergibt 2, 1 &lt;&lt; −1 ergibt das Vorzeichenbit. Das entspricht dem Verhalten der Prozessorbefehle, wird aber leicht übersehen, wenn die Schiebeweite aus einer Rechnung stammt. Wer sie nicht selbst in der Hand hat, prüft sie vorher gegen 0…63.


Zahlensysteme

Ausgeben

Signatur Beschreibung
IntToHex(val, digits: int64): pchar Hexadezimal mit fester Stellenzahl, Großbuchstaben
IntToHex8 · IntToHex16 · IntToHex32 · IntToHex64 2, 4, 8 bzw. 16 Stellen
IntToBin(val, bits: int64): pchar Binär mit fester Stellenzahl
IntToBin8 · IntToBin16 · IntToBin32 · IntToBin64 8, 16, 32 bzw. 64 Stellen
IntToOct(val, digits: int64): pchar Oktal mit fester Stellenzahl

Negative Werte erscheinen im Zweierkomplement: IntToHex64(−1) ergibt FFFFFFFFFFFFFFFF, IntToBin8(−1) ergibt 11111111.

Reicht die Stellenzahl nicht, werden die höherwertigen Stellen abgeschnittenIntToHex(255, 1) ergibt F. Es gibt keine Meldung, die Zahl passt also stillschweigend nicht mehr.

Einlesen

Signatur Beschreibung Status
ParseHex(s: pchar): int64 Hexadezimal; erkennt 0x-Präfix, Groß- und Kleinschreibung Fehler nicht meldbar (#1500)
ParseBin(s: pchar): int64 Binär; erkennt 0b-Präfix dito
ParseOct(s: pchar): int64 Oktal; führende 0 zulässig dito

Der Rundlauf funktioniert: ParseHex(IntToHex32(x)) und ParseBin(IntToBin16(x)) liefern den Ausgangswert zurück.

Lesbare Zahlen

Signatur Beschreibung
IntToStrWithSeparator(n, sep: int64): pchar Dreiergruppen mit frei wählbarem Trennzeichen (Zeichencode)
IntToStrWithComma(n: int64): pchar 1,234,567
IntToStrWithUnderscore(n: int64): pchar 1_234_567

Für die deutsche Schreibweise IntToStrWithSeparator(n, 46) — 46 ist der Punkt. Negative Zahlen und Werte unter 1000 werden korrekt behandelt.


Bitoperationen

Einzelne Bits

Signatur Beschreibung
GetBit(val, bit_pos: int64): bool Ist das Bit gesetzt?
SetBit(val, bit_pos: int64): int64 Bit setzen
ClearBit(val, bit_pos: int64): int64 Bit löschen
ToggleBit(val, bit_pos: int64): int64 Bit umschalten

Alle vier prüfen die Position gegen 0…63. Außerhalb liefert GetBit false, die übrigen geben den Wert unverändert zurück — ein Tippfehler bei der Position bleibt damit folgenlos, aber auch unbemerkt.

Bitfelder

Signatur Beschreibung
ExtractBits(val, start, count: int64): int64 count Bits ab Position start herausziehen
InsertBits(val, bits, start, count: int64): int64 count Bits ab start ersetzen

Beide prüfen, dass start + count innerhalb von 64 Bit bleibt; sonst kommt 0 bzw. der unveränderte Wert zurück.

Zählen

Signatur Beschreibung
CountLeadingZeros(val: int64): int64 Führende Nullbits; für 0 sind es 64
CountTrailingZeros(val: int64): int64 Nullbits am unteren Ende; für 0 sind es 64

Ein PopCount — die Zahl der gesetzten Bits — gibt es hier nicht, sondern in std.math; dort ist es allerdings für negative Werte fehlerhaft (#1448).

Bytes und Byte-Reihenfolge

Signatur Beschreibung
SwapEndian16(val: int64): int64 Zwei Bytes tauschen
SwapEndian32(val: int64): int64 Vier Bytes umdrehen
SwapEndian64(val: int64): int64 Acht Bytes umdrehen
GetByte(val, byte_pos: int64): int64 Einzelnes Byte, Position 0 ist das niederwertigste
SetByte(val, byte_pos, byte_val: int64): int64 Einzelnes Byte ersetzen

Rotation

Signatur Beschreibung Status
RotateLeft32(val, n: int64): int64 Linksrotation über 32 Bit überschreitet 32 Bit (#1495)
RotateRight32(val, n: int64): int64 Rechtsrotation über 32 Bit ok

64-Bit-Rotationen gibt es nicht.

Breiten und Ausrichtung

Signatur Beschreibung
SignExtend8 · SignExtend16 · SignExtend32 Kleinere Breite vorzeichenrichtig auf 64 Bit bringen: SignExtend8(255) = −1
ZeroExtend8 · ZeroExtend16 · ZeroExtend32 Nur die unteren Bits behalten: ZeroExtend8(−1) = 255
ClampUint8 · ClampUint16 · ClampUint32 Auf den vorzeichenlosen Bereich begrenzen statt abzuschneiden
AlignUp(addr, alignment: int64): int64 Nächstes Vielfaches nach oben
AlignDown(addr, alignment: int64): int64 Nächstes Vielfaches nach unten
IsAligned(addr, alignment: int64): bool Bereits ausgerichtet?

Der Unterschied zwischen ZeroExtend8 und ClampUint8 ist wesentlich: Ersteres schneidet ab (300 wird zu 44), Letzteres begrenzt (300 wird zu 255).


Beispiel

import std.conv;

fn main(): int64 {
    PrintLn("-- Zahlensysteme:");
    PrintLn("IntToHex8(255)        = ", IntToHex8(255));
    PrintLn("IntToHex32(305419896) = ", IntToHex32(305419896));
    PrintLn("IntToBin8(5)          = ", IntToBin8(5));
    PrintLn("IntToOct(511, 3)      = ", IntToOct(511, 3));
    PrintLn("IntToHex64(-1)        = ", IntToHex64(-1), "   (Zweierkomplement)");

    PrintLn("-- und zurueck:");
    PrintLn("ParseHex('DEADBEEF') = ", IntToStr(ParseHex("DEADBEEF")));
    PrintLn("ParseHex('0xFF')     = ", IntToStr(ParseHex("0xFF")));
    PrintLn("ParseBin('0b1010')   = ", IntToStr(ParseBin("0b1010")));
    PrintLn("ParseOct('0777')     = ", IntToStr(ParseOct("0777")));

    PrintLn("-- Einzelne Bits:");
    var flags: int64 := 0;
    flags := SetBit(flags, 0);
    flags := SetBit(flags, 3);
    PrintLn("nach SetBit(0) und SetBit(3): ", IntToBin8(flags), " = ", IntToStr(flags));
    PrintLn("GetBit(3)   = ", IntToStr(GetBit(flags, 3) as int64));
    flags := ClearBit(flags, 0);
    PrintLn("nach ClearBit(0): ", IntToBin8(flags));
    flags := ToggleBit(flags, 7);
    PrintLn("nach ToggleBit(7): ", IntToBin8(flags));

    PrintLn("-- Bitfelder lesen und schreiben:");
    var wort: int64 := 0;
    wort := InsertBits(wort, 5, 4, 4);      // 4 Bits ab Position 4
    wort := InsertBits(wort, 3, 0, 4);      // 4 Bits ab Position 0
    PrintLn("zusammengesetzt: ", IntToBin8(wort), " = ", IntToStr(wort));
    PrintLn("oberes Nibble = ", IntToStr(ExtractBits(wort, 4, 4)));
    PrintLn("unteres Nibble = ", IntToStr(ExtractBits(wort, 0, 4)));

    PrintLn("-- Bits zaehlen:");
    PrintLn("CountLeadingZeros(255)  = ", IntToStr(CountLeadingZeros(255)));
    PrintLn("CountTrailingZeros(8)   = ", IntToStr(CountTrailingZeros(8)));

    PrintLn("-- Byte-Reihenfolge und Bytes:");
    PrintLn("SwapEndian32(0x12345678) = ", IntToHex32(SwapEndian32(305419896)));
    PrintLn("GetByte(0x12345678, 0)   = ", IntToHex8(GetByte(305419896, 0)));
    PrintLn("GetByte(0x12345678, 3)   = ", IntToHex8(GetByte(305419896, 3)));

    PrintLn("-- Breiten und Ausrichtung:");
    PrintLn("SignExtend8(255)  = ", IntToStr(SignExtend8(255)));
    PrintLn("ZeroExtend8(-1)   = ", IntToStr(ZeroExtend8(-1)));
    PrintLn("ClampUint8(300)   = ", IntToStr(ClampUint8(300)));
    PrintLn("AlignUp(100, 16)  = ", IntToStr(AlignUp(100, 16)));
    PrintLn("IsAligned(96, 16) = ", IntToStr(IsAligned(96, 16) as int64));

    PrintLn("-- Lesbare Zahlen:");
    PrintLn("IntToStrWithComma(1234567)      = ", IntToStrWithComma(1234567));
    PrintLn("IntToStrWithSeparator(1234567, 46) = ", IntToStrWithSeparator(1234567, 46));
    return 0;
}

Ausgabe:

-- Zahlensysteme:
IntToHex8(255)        = FF
IntToHex32(305419896) = 12345678
IntToBin8(5)          = 00000101
IntToOct(511, 3)      = 777
IntToHex64(-1)        = FFFFFFFFFFFFFFFF   (Zweierkomplement)
-- und zurueck:
ParseHex('DEADBEEF') = 3735928559
ParseHex('0xFF')     = 255
ParseBin('0b1010')   = 10
ParseOct('0777')     = 511
-- Einzelne Bits:
nach SetBit(0) und SetBit(3): 00001001 = 9
GetBit(3)   = 1
nach ClearBit(0): 00001000
nach ToggleBit(7): 10001000
-- Bitfelder lesen und schreiben:
zusammengesetzt: 01010011 = 83
oberes Nibble = 5
unteres Nibble = 3
-- Bits zaehlen:
CountLeadingZeros(255)  = 56
CountTrailingZeros(8)   = 3
-- Byte-Reihenfolge und Bytes:
SwapEndian32(0x12345678) = 78563412
GetByte(0x12345678, 0)   = 78
GetByte(0x12345678, 3)   = 12
-- Breiten und Ausrichtung:
SignExtend8(255)  = -1
ZeroExtend8(-1)   = 255
ClampUint8(300)   = 255
AlignUp(100, 16)  = 112
IsAligned(96, 16) = 1
-- Lesbare Zahlen:
IntToStrWithComma(1234567)      = 1,234,567
IntToStrWithSeparator(1234567, 46) = 1.234.567

Das Zusammenspiel von InsertBits und IntToBin8 ist beim Bauen von Protokollfeldern hilfreich: man sieht das Bitmuster unmittelbar.


Fallstricke

Geprüft mit lyxc 1.0.21A.

RotateLeft32 verlässt den 32-Bit-Bereich (#1495)

Aufruf geliefert erwartet
0x80000000 «< 1 4294967297 1
0xFFFFFFFF «< 4 68719476735 4294967295
0x12345678 «< 8 78187493394 878082066

Die herausgeschobenen Bits fallen nicht weg, sondern bleiben im int64 stehen. RotateRight32 ist davon nicht betroffen. Das trifft insbesondere Hashverfahren wie MD5, SHA-1 oder ChaCha, deren Kern die 32-Bit-Linksrotation ist. Bis zum Fix selbst maskieren:

var rotiert: int64 := RotateLeft32(wert, n) & 4294967295;

Die Parser melden keine Fehler (#1500)

Eingabe Ergebnis Verhalten
ParseHex(„FFxy“) 255 bricht bei x ab
ParseHex(„12 34“) 4660 überspringt das Leerzeichen und liest 1234
ParseHex(„-FF“) 255 Minuszeichen wird ignoriert
ParseBin(„1010abc“) 10 bricht ab
ParseOct(„7778“) 511 bricht bei der 8 ab
ParseHex(„ZZ“) 0 nichts gelesen
ParseHex(„0“) 0 gültige Eingabe — nicht unterscheidbar

Zwei Dinge fallen zusammen: eine fehlgeschlagene Umwandlung ist am Rückgabewert nicht erkennbar, und Störzeichen führen mal zum Abbruch, mal werden sie übersprungen. Wer fremde Eingaben verarbeitet, prüft die Zeichen deshalb vorher selbst.

Zu wenige Stellen schneiden ab

IntToHex(255, 1) ergibt F, IntToBin(255, 4) ergibt 1111. Die höherwertigen Stellen entfallen ohne Hinweis. Die festen Varianten (IntToHex8 und Verwandte) sind hier die sicherere Wahl, weil die Breite zum Wertebereich passt.

Positionsprüfungen schlucken Tippfehler

SetBit(wert, 99) gibt den Wert unverändert zurück, GetBit(wert, 64) liefert false. Das verhindert Speicherfehler, verbirgt aber einen falschen Index — die Rückgabe sieht wie ein gültiges Ergebnis aus.


Verwandte Units

  • std.mathPopCount, IsPowerOfTwo, NextPowerOfTwo
  • std.buffer — Hex-Umwandlung ganzer Puffer statt einzelner Zahlen
  • std.pack — Zahlen mit fester Breite in Byte-Puffer schreiben
  • Syntax — die Operatoren der Sprache im Überblick