• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Kolejność bajtów

Object Storage Arubacloud
+2 głosów
3,230 wizyt
pytanie zadane 1 sierpnia 2017 w C i C++ przez niezalogowany
Postanowiłem sobie stworzyć własne rozszerzenie pliku, gdzie dane zapisuję binarnie i stworzyć program do odczytywania tych plików. Przeprowadziłem kilka zapisów i odczytów i wszystko jest ok, ale dowiedziałem się o czymś takim jak Big Endian i Little Endian. Mimo googlowania nie potrafię tego ogarnąć bo różne funkcje są jak dla mnie zbyt skomplikowane i nie rozumiem tych wszystkich uint32, uint64 i innych dziwnych rzeczy. Nie wiem gdzie i na rzecz czego mam te funkcje wywołać i z jakimi argumentami.

Znalazłem fajny artukuł, który w prosty sposób opisuje o co chodzi w zagadnieniu, może komuś się przyda

http://digitalforensics.pl/podstawy-analizy-danych/bajt/

Jednak nie ma tam wyjaśnione jak ustawić kolejność bajtów. Zależy mi na tym, żeby zapisywać i odczytywać dane binarnie (co potrafię), ale żeby mieć ustawioną kolejność bajtów, żeby mój program bez problemu działał na innych komputerach.

W artykule na cpp0x w części "zapis binarny"

http://cpp0x.pl/artykuly/?id=72

jest napisane, że można zapisać liczby w ściśle określonej kolejności bajtów. Tylko jak to zrobić? Przy okazji kolejność bajtów ma znaczenie tylko przy liczbach czy przy znakach i tekstach też?

3 odpowiedzi

+3 głosów
odpowiedź 1 sierpnia 2017 przez mokrowski Mędrzec (155,460 p.)

Kolejność bajtów jest dla danych binarnych determinowana przez architekturę procesora którego używasz i tryb jego pracy. W przypadku rodziny x86 jest to ściśle określone jako Little Endian bo ten procesor nie ma możliwości przestawienia jego trybu pracy. Oznacza to że bajty w zapisie liczby reprezentowanej jako hex np. 0x1122334455667788 (64-bitowej) są umieszczone w pamięci jako 0x88 0x77 0x66 0x55 0x44 0x33 0x22 0x11. A dane 32-bitowe np. 0x11223344 umieszczone także w porządku odwrotnym. Nie jest tak jednak w każdym przypadku. Architektura ARM lub np. Sparc, ma możliwość dowolnego ustawienia kolejności tych bajtów.

#include <iostream>
#include <cstdint> // Uwaga: Dopiero C++11 określa bezpieczne użycie..

using endianess1_t = union {
    uint64_t data;
    uint8_t bytes[8];
};

using endianess2_t = union {
    struct {
        uint32_t data1;
        uint32_t data2;
    } two_value;
    uint8_t bytes[8];
};

int main() {
    endianess1_t value1;
    value1.data = 0x1122334455667788;
    for(const auto val: value1.bytes) {
        std::cout << std::hex << static_cast<unsigned>(val) << ' ';
    }
    std::cout << std::endl;

    endianess2_t value2;
    value2.two_value.data1 = 0x11223344;
    value2.two_value.data2 = 0x11223344;
    for(const auto val: value2.bytes) {
        std::cout << std::hex << static_cast<unsigned>(val) << ' ';
    }
    std::cout << std::endl;
}

To jak dane są układane przez dany procesor możesz odkryć badając makra__BYTE_ORDER__ przyrównując je do __ORDER_BIG_ENDIAN__ , __ORDER_LITTLE_ENDIAN__, __ORDER_PDP_ENDIAN__ .

#include <iostream>
#include <string>

int main() {
    std::string val;
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    val = "big";
#elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    val = "little";
#elif __BYTE_ORDER__ == __ORDER_PDP_ENDIAN__
    val = "pdp";
#else
    val = "undetected";
#endif
    std::cout << val << std::endl;
}

Sprawdzone dla gcc i clang.

Z problemem będziesz miał więc do czynienia w przypadku reprezentacji przekraczających 8-bitów. Ten problem nie występuje przy reprezentacjach 8-bitów czyli np. przy "tekstach ASCII". Tu (o ile kodowanie nie wybiega poza 8-bitów), nie ma dyskusji. Dane są wymieniane w kolejności "takiej jak zapis".  Jeśli kodowanie znaków wybiega poza 16-bitów (np UTF), z problemem kolejności będziesz się borykał. Dlatego w plikach o wielobajtowych kodowaniach, stosuje się znacznik kodowania w pierwszych 2 bajtach pliku (tzw. BOM https://pl.wikipedia.org/wiki/BOM_(informatyka)  )

W praktyce:

  1. Przyjmujesz arbitralnie sposób zapisu danych do pliku (najczęściej "naturalny" Big endian czyli "tak jak to widzi człowiek")
  2. Konwertujesz dane do formatu poprawnie interpretowanego przez procesor w swoim programie.

W konwersji z danych BIG, pomogą Ci makra funkcje hton*() (to mogą być makra w danym kompilatorze...) http://beej.us/guide/bgnet/output/html/multipage/htonsman.html

 

+1 głos
odpowiedź 1 sierpnia 2017 przez draghan VIP (106,230 p.)

Przeprowadziłem kilka zapisów i odczytów i wszystko jest ok, ale dowiedziałem się o czymś takim jak Big Endian i Little Endian. Mimo googlowania nie potrafię tego ogarnąć bo różne funkcje są jak dla mnie zbyt skomplikowane i nie rozumiem tych wszystkich uint32, uint64 i innych dziwnych rzeczy. Nie wiem gdzie i na rzecz czego mam te funkcje wywołać i z jakimi argumentami.

Te endianowości to wbrew pozorom bardzo prosta sprawa. Mając dziesiętnie dwie liczby: 0 i 17 zapisane binarnie jako 2 bajty danych (16 bitów) naturalnie otrzymamy 00000000 00010001. To jest zapis tzw. Big Endian. Ale te same liczby można zapisać kolejno bajtami "od tyłu": 00010001 00000000 - wtedy to jest Little Endian.

uint32 i uint64 to po prostu typ danych - unsigned int 32 lub 64 bitowy. Większość operacji "binarnych" (czytanie/zapis/manipulacje bezpośredniej reprezentacji zmiennej) wykonywana jest na typie unsigned, żeby nie zaprzątać sobie głowy kodowaniem minusa, zaś dodatkowa wiedza o długości takiej zmiennej jest bardzo pomocna.
O jakich funkcjach tutaj mówisz?

Jednak nie ma tam wyjaśnione jak ustawić kolejność bajtów.

W większości przypadków nie możesz tego "ustawić" - to cecha procesora, na którym uruchamiasz program.

Zależy mi na tym, żeby zapisywać i odczytywać dane binarnie (co potrafię), ale żeby mieć ustawioną kolejność bajtów, żeby mój program bez problemu działał na innych komputerach.

Napisz funkcję, która sprawdza z którym typem kodowania masz do czynienia. To nie takie trudne.

jest napisane, że można zapisać liczby w ściśle określonej kolejności bajtów.

Nie chce mi się czytać całego artykułu, wybacz. Gdzie dokładnie to jest napisane?

komentarz 1 sierpnia 2017 przez niezalogowany
We fragmencie zatytułowanym "Zapis binarny" pod koniec. Zaczyna się nad niebieskim linkiem.
+1 głos
odpowiedź 1 sierpnia 2017 przez PoetaKodu Stary wyjadacz (10,990 p.)
edycja 2 sierpnia 2017 przez PoetaKodu

Wyobraź sobie pamięć jako ciąg następujących po sobie bitów, np. pamięć o pojemności dokładnie 8GiB (GiB = Gibibajt = 1024 MiB = 1024 * 1024 KiB = 1024 * 1024 * 1024B) zawiera 8 [GiB] * 1024 [MiB] * 1024 [KiB] * 1024 [B] * 8 bitów. Pojedyncze bity, jako że mogą przechowywać tylko jeden z dwóch stanów - 0 lub 1 na niewiele się przydają komputerowi. Przy użyciu 8 bitów (czyli jednego bajta) możemy już zapisać więcej informacji bo pozwala to na zapisanie 256 różnych stanów - od 0 do 255 włącznie. To już może się przydać, więc pamięć zorganizowana jako bardzo długi ciąg bajtów obsługuje adresowanie przynajmniej bajtu - nie możemy wziąć adresu poszczególnego bitu. Adres jest po prostu jego numerem licząc od zera. Teraz kiedy tworzysz zmienną, np. typu int to system operacyjny (to uproszczona wersja) przydziela Ci jakiś adres i począwszy od tego zakresu Twoja zmienna ma do dyspozycji 4 bajty (zazwyczaj, bo rozmiar typu int zależy od platformy). Oto rozmiary, które najpopularniejsze typy zazwyczaj (w przeważającej większości przypadków) przyjmują:

  • int - 4 bajty = 32 bity
  • short - 2 bajty = 16 bitów
  • float - 4 bajty = 32 bity
  • double - 8 bajtów = 64 bity
  • char - 1 bajt = 8 bitów (akurat rozmiar typu char jest zagwarantowany - nie ma opcji, żeby miał inny rozmiar niż 8 bitów)
  • long - 4 bajty = 32 bity (wbrew pozorom long bardzo często ma taki sam rozmiar co int)
  • long long - 8 bajtów = 64 bity

I teraz zależnie od tego jakiego typu stworzysz zmienną tyle system operacyjnych Ci przyzna kolejnych bajtów. I zależnie od platformy kolejność zapisu tych bajtów będzie różna. Załóżmy, że stworzymy zmienną typu int (4 bajty), której nadamy wartość 0x12AB34CD (zapis 16-stkowy bo jest łatwiej - 2 kolejne znaki odpowiadają jednemu bajtowi). Możemy teraz te dane zapisać w pamięci na dwa sposoby - załóżmy, że zaczynamy od adresu 0, kolejne adresy 1, 2, 3 itd to numery bajtów:

Liczba do zapisania: 0x12AB34CD
Numer bajtu:
   0   |    1   |    2   |    3  |
-------------------------------------
Little endian:
  CD   |   34   |   AB   |   12  |
Big endian:
   12  |   AB   |   34   |   CD  |

I teraz zależnie od platformy zostaną one zapisane w jednym z tych sposobów - kolejność bitów poszczególnych bajtów się nie zmienia, tylko kolejność całych bajtów.

Wracamy do sposobu zapisu danych w pamięci.

Bez względu na to, czy zapisujesz znak, czy tekst, czy liczbę, wszystko jest reprezentowane przez ciąg bitów.
Wbrew temu, co często się mówi char jest tak samo jak short czy int typem liczb całkowitych - jedynie ze względu na rozmiar dokładnie jednego bajta używa się go często do zapisania znaku z tablicy ASCII (tablicy, która odpowiednim liczbom całkowitym przyporządkowuje odpowiednie znaki graficzne). Każdy znak ASCII jest po prostu liczbą, tylko std::cout wyświetla je jako znak, nie jako liczbę. Przykład jak to wygląda po zamianie na int:
http://ideone.com/9ba97U <- link do kompilatora online z przykładem.

String (ciąg znaków) to po prostu tablica następujących po sobie znaków, czyli tablica następujących po sobie liczb, które też umieszcza się w pamięci.

Ze względu na to, by uniezależnić rozmiar int, float, long itp od platformy czy kompilatora (bo on też na to wpływa) stworzono specjalne typy danych jak np:
int8_t, int16_t, int32_t, int64_t
uint8_t, uint16_t, uint32_t, uint64_t

Dodatkowo, skracają one zapis. Żeby ich używać, wystarczy dodać:

#include <cstdint>

Zwykłe typy int8_t, int16_t itd... oznaczają, że jest to liczba całkowita ze znakiem (dodatnie i ujemne), która ma rozmiar dokładnie N bitów, przy czym N jest podane przy "int", czyli:
int8_t ma dokładnie 8 bitów
int16_t ma dokładnie 16 bitów itd itd...

Do tego dochodzą typy bez znaku (tylko liczby dodatnie i zero - czyli po prostu unsigned) - uint8_t, uint16_t, uint32_t itd...
Bardzo wygodne jest korzystanie z nich, bo jeśli chcesz zapisać do pliku dokładną ilość bajtów to możesz śmiało tego użyć.

Problem zapisu danych.

Teraz to o co konkretnie pytałeś - niektóre systemy zapisują w inny sposób dane i trzeba się z tym liczyć. Raz zapisując dane będziesz miał kolejność big endian, raz little endian. Jeśli chcesz sprawdzić jakiego sposobu używa Twój procesor najlepiej gdybyś stworzył dwubajtową liczbę (std::int16_t lub std::uint16_t). Następnie zapisz do niej na dwóch bajtach konkretną liczbę, np:

0x1234 <--- teraz pierwszy bajt przyjmie wartość 0x12 a drugi 0x34.
Ale tylko w systemie BigEndian, w systemie LittleEndian kolejność bajtów będzie odwrotna, więc teraz wystarczy to sprawdzić castując wskaźnik na tą liczbę na wskaźnik na char (bo jest jednego bajtu) i wyłuskać wartość.

Potem zależnie od tego, czy zmienił się system tego zapisu musisz zadecydować, czy zmieniać kolejność bajtów czy nie (o tym na końcu). Najważniejsze jest także to, żebyś zwracał uwagę na strukturę pliku, jeśli zapisujesz w ten sposób:
| Liczba - 4 bajty | Liczba - 2 bajty | Liczba 2 bajty | Znak - 1 bajt | Liczba - 4 bajty |
To musisz w ten sposób też te dane wczytać, jeśli wczytasz 4 bajty tam, gdzie zapisywałeś 2 to zepsujesz sobie te dane.

Zapis i odczyt danych z pliku

Standardowo pliki otwierane przy użyciu std::fstream są otwierane w trybie ASCII, nie binarnym. Oznacza to, że jeśli zapiszesz fizycznie w notatniku do pliku dane:

123456

I spróbujesz wczytać je tak z pliku:

std::ifstream file("plik.txt");
int liczba = 0;
file >> liczba;

To liczba przechowa wartość "123456". Tego nie chcemy - dlaczego? Bo tak naprawdę ta liczba jest zapisana jako następujące po sobie znaki ASCII '1', '2', '3', '4', '5', '6' i zajmuje teraz 6 bajtów. Jeśli zapiszesz w trybie ASCII liczbę, która ma 9 cyfr to zajmie ona 9 bajtów! To pozwala się uchronić przed konsekwencją zmiany systemu Endianness, ale za to dostajesz o wiele większe rozmiary plików i jest to nieprofesjonalne. Nie chcemy tego. Żeby poprawnie zapisać dane binarne w pliku otworzymy go w trybie binarnym:

std::ofstream plik("plik.bin", std::ios::binary);

std::uint32_t liczba = 123456; // upewniamy sie, ze zajmie ona dokladnie 4 bajty

plik.write(reinterpret_cast<char*>(&liczba), sizeof(liczba));

Na pierwszy rzut oka wygląda to dosyć skomplikowanie ale jest to proste.
Metoda plik.write, zapisuje pojedyncze ciąg pojedynczych znaków, bo tak jest najłatwiej - każdy znak zajmuje dokładnie 8 bitów. Pierwszy argument to const char*, który oznacza tablicę następujących po sobie bajtów, których ilość z kolei określa drugi parametr.

Bierzemy adres tej liczby i konwertujemy go reinterpret_cast-em tak, żeby pasował do przyjmowanego typu, teraz ta liczba będzie brana po prostu jako ciąg czterech bajtów w pamięci. W drugim argumencie używamy operatora sizeof(liczba), który wskaże ile bajtów ma liczba (chociaż w tym przypadku śmiało możemy sami wstawić tam po prostu czwórkę, bo uint32_t gwarantuje nam taki rozmiar).

Jeśli teraz chcemy poprawnie wczytać te dane to użyjemy:
 

std::ifstream plik("plik.bin", std::ios::binary);

std::uint32_t liczba;

plik.read(reinterpret_cast<char*>(&liczba), sizeof(liczba));

Czyli robimy to w ten sam sposób, tylko metodą read. Po wczytaniu uzyskamy taką samą liczbę jak zapisaliśmy.
Chyba że...

Zamiana systemów Little Endian i Big Endian.

Jest to banalnie proste. Przytoczę tutaj kod z stackoverflowa, który dobrze do tego podchodzi. Zakładam, że nie chcesz się w to sam bawić bo kto chciałby od nowa wynaleźć koło:
 

#include <climits>

template <typename T>
T swap_endian(T u)
{
    static_assert (CHAR_BIT == 8, "CHAR_BIT != 8");

    union
    {
        T u;
        unsigned char u8[sizeof(T)];
    } source, dest;

    source.u = u;

    for (size_t k = 0; k < sizeof(T); k++)
        dest.u8[k] = source.u8[sizeof(T) - k - 1];

    return dest.u;
}

Jeśli chcesz zmienić system z Big Endian na Little Endian lub na odwrót to tu masz przykład:

uint32_t little = 123;
uint32_t big = swap_endian<uint32_t>(little);

Ufff... dużo tego jest, mam nadzieję, że pomogłem.
Tutaj źródła:

Koniec :)

komentarz 1 sierpnia 2017 przez PoetaKodu Stary wyjadacz (10,990 p.)
Mała notka:

Użyłem złego casta - static_castem nie przekonwertujesz int* na char*, musisz zrobić to reinterpret_castem. Zmiany już w oryginalnym poście zapisałem.
komentarz 1 sierpnia 2017 przez niezalogowany

Wow, naprawdę świetnie opisane! Szczegółowo, po kolei i zrozumiale. I chyba będę stosował od teraz ten sposób z reinterpret_cast bo jest krótszy, prostszy i jak podejrzewam bezpieczniejszy i ogólnie lepszy niż to rzutowanie w starym stylu z artykułu na cpp0x (no i Jurek Grębosz odradza to stare rzutowanie). Tak teraz wygląda mój kod testowy

#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <stdio.h>
#include <climits>
#include <cstdint>

using namespace std;

template <typename T>
T swap_endian(T u)
{
    static_assert (CHAR_BIT == 8, "CHAR_BIT != 8");

    union
    {
        T u;
        unsigned char u8[sizeof(T)];
    } source, dest;

    source.u = u;

    for (size_t k = 0; k < sizeof(T); k++)
        dest.u8[k] = source.u8[sizeof(T) - k - 1];

    return dest.u;
}

struct Struktura
{
    int a;
    int b;
    char c;
};

enum ByteOrderType
{
    BigEndian,
	LittleEndian,
};

inline ByteOrderType ByteOrderTest()
{
    volatile short int x = 0x1234;
	return *reinterpret_cast<volatile char*>(&x) == 0x12 ? BigEndian : LittleEndian;
}

struct dwaZnaki
{
    char a;
    char b;
};

int main()
{
    // I sposób zapisu danych binarnie z kursu na cpp0x

    ofstream plik1;

    plik1.open( "test.bin", ios::binary );

    Struktura s;
    s.a = 42;
    s.b = 667;
    s.c = 'X';

    int n1 = 78;

    plik1.write(( const char * ) & s, sizeof s );
    plik1.write(( const char * ) & n1, sizeof n1 );

    plik1.close();

    ifstream plik2;

    plik2.open("test.bin", ios::binary); // otwieramy plik do odczytu binarnego
    char* temp = new char[sizeof(s)]; // tymczasowy bufor na dane

    plik2.read(temp, sizeof(Struktura)); // wczytujemy dane do bufora
    Struktura* str = (Struktura*)(temp); // rzutujemy zawartosc bufora na typ Struktura
    cout << str->a <<  " " << str->b << " " << str->c << endl;
    delete str;

    char* temp2 = new char[sizeof(n1)]; // tymczasowy bufor na dane
    plik2.read(temp2, sizeof(n1)); // wczytujemy dane do bufora
    int *liczba = (int*)(temp2);
    cout << *liczba << endl;
    delete liczba;
    plik2.close();

    // II sposób zapisu danych binarnie (lepszy)

    int liczba1 = 14;
    ofstream plik3;
    plik3.open( "test2.bin", ios::binary );
    plik3.write(reinterpret_cast<char*>(&liczba1), sizeof(liczba1));
    plik3.close();

    int liczba2;
    ifstream plik4;
    plik4.open("test2.bin", ios::binary);
    plik4.read(reinterpret_cast<char*>(&liczba2), sizeof(liczba2));
    plik4.close();

    cout << "liczba2 = " << liczba2 << endl;

    // Sprawdzenie endianowości

    if(ByteOrderTest() == BigEndian)
        cout << "BigEndian" << endl;
    else
        cout << "LittleEndian" << endl;

    string val;
    #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    val = "big";
    #elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    val = "little";
    #elif __BYTE_ORDER__ == __ORDER_PDP_ENDIAN__
    val = "pdp";
    #else
    val = "undetected";
    #endif
    cout << val << endl;

    return 0;
}

Ten fragment znalazłem gdzieś w czeluściach internetu

enum ByteOrderType
{
    BigEndian,
	LittleEndian,
};

inline ByteOrderType ByteOrderTest()
{
    volatile short int x = 0x1234;
	return *reinterpret_cast<volatile char*>(&x) == 0x12 ? BigEndian : LittleEndian;
}

ale nie rozumiem kawałka od return do znaku zapytania. Mógłbyś mi to wyjaśnić, jak ta funkcja działa?

A to z literami B i L to jak mam zrobić? Bo pomyślałem, że zapiszę strukturę skoro mają być dwa znaki za jednym razem, a odczytam po jednym znaku, ale miałem normalną kolejność B i L. A testy mi wykazały, że mam przecież LittleEndian. Napisałbyś kod, który robi to sprawdzenie z dwoma znakami?

 

komentarz 2 sierpnia 2017 przez PoetaKodu Stary wyjadacz (10,990 p.)

Sam teraz zdałem sobie sprawę, że nie trzeba zapisywać tego do pliku, bo to wywnioskować można z tego jak do RAM zapisywane są liczby. Całkowicie zapomnij o tym co pisałem z sprawdzaniem w stylu "BL" czy coś takiego - to jest niepotrzebne. Dobrym bardzo pomysłem jest ta funkcja "ByteOrderType ByteOrderTest()".
Sposób zapisu (BigEndian lub LittleEndian) wpływa na sposób zapisania do RAMu, więc nie trzeba się bawić w zapisywanie do pliku.
Ten fragment kodu działa tak:

  • tworzy nową zmienną, która rozciąga się na dwa bajty (short int = short = zwykle 2 bajty) i przypisuje jej wartość korzystając z dwóch bajtów. Teraz w pamięci wygląda to tak:
    | byte 00 | byte 01 |
    ----------------------
    |  0x12   |  0x34   |

    ^ tak to wygląda w Big Endian. Ale jeśli procesor używa systemu Little Endian, to zapisze go w odwrotnej kolejności, więc pierwszy bajt będzie wynosił 0x34 a drugi 0x12, mimo że wartość liczby pozostanie taka sama.
  • Następnie bierzemy adres tej liczby, czyli adres pierwszego jej bajtu
    &x

    Teraz wystarczy sprawdzić, czy na pierwszym bajcie znajduje się 0x12 czy 0x34 i znamy system zapisu! W takim razie, musimy jakoś odczytać wartość tego jednego bajta, dlatego wskaźnik castujemy na wskaźnik to pojedynczego znaku:

    reinterpret_cast<volatile char*>(&x)

    Wystarczy teraz operatorem wyłuskania (* - gwiazdka przed) otrzymać wartość spod tego adresu i porównać czy jest to 0x12.
    Cała ta operacja jest rozwiązana w formie tzw. ternary operator (operator potrójny), który ma formę:

    return (warunek ? wartosc_jesli_true : wartosc_jesli_false);
    // Odpowiednik przy uzyciu if:
    if(warunek)
        return wartosc_jesli_true;
    else
        return wartosc_jesli_false;

    Czyli po prostu - jeśli pierwszy bajt to 0x12 to mamy do czynienia z BigEndian, jeśli nie to z LittleEndian.

Kilka sprostowań:

  • słowo kluczowe volatile jest używane, by kompilator nie zoptymalizował kodu związanego z tą zmienną - jest to celowy zabieg bo tak naprawdę kompilator przy użyciu wysokiego poziomu optymalizacji mógłby stwierdzić, że ta funkcja nic ważnego nie robi, a pierwszym zapisanym bajtem tej liczby (mógłby przyjąć domyślny BigEndian) jest 0x12 więc nawet w przypadku LittleEndian zwróciłby, że używany jest BigEndian.
  • volatile ma znaczenie przy castowaniu, dlatego w reinterpret_cast mamy do czynienia z tym słówkiem
  • kod zawiera potencjalny błąd - jeśli wielkość short int nie będzie wynosiła 2 bajtów to kod nie zadziała, dlatego zamiast tego powinno się użyć uint16_t.
komentarz 2 sierpnia 2017 przez niezalogowany
edycja 2 sierpnia 2017

Napisałem następujący kod i rzeczywiście po użyciu funkcji swap_endian wyświetlają się takie liczby jak powinny. Ale jak będzie ze stringami? Przy innej endianowości będą czytane od tyłu? Zamiast "kot" będzie "tok"? I jak temu zaradzić?

#include <iostream>
#include <string>
#include <fstream>
#include <cstdint>
#include <cstdlib>

using namespace std;

template <typename T>
T swap_endian(T u)
{
    // sprawdzenie czy bajt składa się z 8 bitów
    static_assert (CHAR_BIT == 8, "CHAR_BIT != 8");

    union
    {
        T u;
        unsigned char u8[sizeof(T)];
    } source, dest;

    source.u = u;

    for (size_t k = 0; k < sizeof(T); k++)
    {
        dest.u8[k] = source.u8[sizeof(T) - k - 1];
    }

    return dest.u;
}

inline string ByteOrderTest()
{
    volatile uint16_t x = 0x1234;
	return *reinterpret_cast<volatile char*>(&x) == 0x12 ? "BigEndian" : "LittleEndian";
}

int main()
{
    cout << ByteOrderTest() << endl << endl;

    short a1 = 258;
    short a2 = 1;
    string napis = "napisik";

    ofstream plik;
    plik.open("plik.test", ios::binary);
    plik.write(reinterpret_cast<char*>(&a1), sizeof(a1));
    plik.write(reinterpret_cast<char*>(&a2), sizeof(a2));
    plik.write(napis.c_str(), napis.size()+1);
    //for(int i=0; i<napis.size(); i++)
    //    plik.write(reinterpret_cast<char*>(&napis[i]), sizeof(char));
    // dwie powyzsze linijki tez daja rade
    plik.close();

    short liczba1;
    short liczba2;
    string odczyt;

    ifstream plik2;
    plik2.open("plik.test", ios::binary);
    plik2.read(reinterpret_cast<char*>(&liczba1), sizeof(liczba1));
    plik2.read(reinterpret_cast<char*>(&liczba2), sizeof(liczba2));
    getline( plik2, odczyt, '\0' );
    plik2.close();

    cout << liczba1 << endl;
    cout << liczba2 << endl;
    cout << odczyt << endl << endl;

    cout << "Po zmianie endianowosci" << endl;
    cout << swap_endian<short>(liczba1) << endl;
    cout << swap_endian<short>(liczba2) << endl;
    /* cout << swap_endian<string>(odczyt) << endl; // nie zadziala, zapewne dlatego, ze
       string nie jest podstawowym typem danych z C */

    system("pause");

    return 0;
}

 

komentarz 2 sierpnia 2017 przez PoetaKodu Stary wyjadacz (10,990 p.)

Na stringi to nie ma znaczenia, bo string to ciąg znaków, które zajmują jeden bajt (jeśli mówimy o std::string). Kiedy zapisujesz do pamięci ciąg znaków:

char tab[] = { 'a', 'b', 'c', 'd', 'e' };

To są one poukładane zawsze tak samo, bo zajmują jeden bajt. Endianness nie ma wpływu na jedno-bajtowe dane, bo nie ma jak zmienić kolejności jednego bajta. Jak zapiszesz np. tablicę 2 bajtowych liczb:

std::int16_t tab[] = { 0x1234, 0x4321, 0x1324, 0x4422 };

To już zaczyna mieć znaczenie kolejność bajtów, ale tylko w obrębie pojedynczych elementów tablicy.

Chodzi o to, że nawet jeśli Twój procesor korzysta z systemu LittleEndian to nie oznacza to, że cała tablica zostanie od tyłu zapisana i nagle będzie to wyglądać tak:

0x22   0x44  |  0x24   0x13  |  0x21   0x43  |  0x34   0x12

Nie, pojedyncze dwubajtowe elementy tablicy zostaną na odwrót zapisane:

0x34   0x12  |  0x21   0x43  |  0x24   0x13  |  0x22   0x44

Dlatego, że adres tych zmiennych się nie zmienia, gdyby cała tablica była od tyłu zapisana, to oznaczałoby to, że pierwszy element byłby na jej końcu. Skoro kolejność elementów się nie zmienia, tylko kolejność bajtów w każdym z elementów, to na jednobajtowe znaki ASCII nie ma to najmniejszego znaczenia.
Możesz śmiało zrobić tak:

char str[64];
plik.read(str, sizeof(str));

 

komentarz 2 sierpnia 2017 przez niezalogowany
Pomyślałem sobie , że zapisywanie do pliku dużej tablicy znaków, podczas gdy string i tak będzie krótszy to trochę marnowanie pamięci. Wpadłem więc na pomysł żeby zapisywać binarnie długość stringa, a następnie string, i potem przy odczycie wczytać tą długość i dokładnie tyle znaków ile trzeba. Dzięki temu zużyje się chyba mniej pamięci bo chociaż dodatkowe dane liczbowe trochę zajmują to jest to bardzo mało miejsca (długość stringa można spokojnie przechowywać w zmiennej short mającej 2 bajty) podczas gdy tworzenie tablic charów to może być marnowanie kilkudziesięciu bajtów.

Jak myślisz, to dobry pomysł? Pytam bo czasem wpadam na dobre pomysły, a czasem tylko mi się wydaje i okazuje się, że mój kod może mieć niezdefiniowane zachowanie.

I zauważyłem, że tylko dla intów są te int32_t itd. Nie ma dla short, a ten jak mówiłeś może mieć różne rozmiary. Czy to może spowodować błędy? Mam korzystać zamiast tego z int8_t lub uint8_t?
komentarz 2 sierpnia 2017 przez PoetaKodu Stary wyjadacz (10,990 p.)

Co do zapisywania długości stringa - tak się praktycznie zawsze robi. Najpierw np. 2 bajty na długość a potem wczytujesz konkretnie tą ilość z pliku.

I zauważyłem, że tylko dla intów są te int32_t itd. Nie ma dla short, a ten jak mówiłeś może mieć różne rozmiary. Czy to może spowodować błędy? Mam korzystać zamiast tego z int8_t lub uint8_t?

Posłuchaj - short to też typ liczb całkowitych i technicznie rzecz biorąc "short" to skrót od "short int" czyli krótki int, który zwykle ma 2 bajty. Jeśli popatrzysz na plik stdint.h to zobaczysz to:

Te typy to zwykle właśnie takie typedefy. Możesz teraz pomyśleć, że to nic w sumie nie zmienia bo jeśli rozmiar int jakimś cudem wyniesie 16 bitów to int32_t skłamie.
Nie stanie się tak, bo wtedy taki kompilator miałby zmieniony ten zapis na:

typedef int uint16_t;

O to już się troszczą twórcy kompilatora. Ty się nie musisz tym przejmować. W ogóle jeśli zależy Ci na dokładnej liczbie bajtów to nie powinieneś używać tych podstawowych typów, tylko właśnie std::uint32_t itd...

Do tego manipulować rozmiarem typów można jeszcze w inny sposób:

#pragma pack(1)
struct int24_t
{
    std::int32_t data : 24; // Teraz to pole ma dokladnie 24 bity

    // tutaj zdefiniowane konwersje, operatory itp
};
#pragma pack(pop)

Używa się #pragma pack(1) ze względu na to, że kompilator czasem specjalnie dodaje paddingi (czyli puste miejsca o jakichś rozmiarach, które wyrównują cały rozmiar struktury/klasy do bardziej popularnej ilości bajtów - np. podzielnej przez 4). #pragma pack(1) sprawi, że cała struktura będzie wyrównana do jednego bajta, więc żaden padding nie zostanie dołozony.

Podobne pytania

0 głosów
1 odpowiedź 886 wizyt
pytanie zadane 21 maja 2017 w Java przez Koko$ Użytkownik (740 p.)
0 głosów
1 odpowiedź 228 wizyt
0 głosów
1 odpowiedź 364 wizyt

92,576 zapytań

141,426 odpowiedzi

319,652 komentarzy

61,961 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...