C言語基礎|バイト順序とエンディアン

同じ数なのにメモリの並びが違う!?バイト順序(エンディアン)を知ると、ポインタとデータの見え方がつながる。

C言語でポインタを学び始めると、アドレスを表示したり、メモリの中身をバイト単位で覗いたりする機会が増えます。
そのときに必ず出会うのが バイト順序、いわゆる エンディアン(endianness) です。

同じ 0x12345678 という数値でも、メモリ上に 12 34 56 78 の順で並ぶとは限りません。
環境によっては 78 56 34 12 の順に入ります。

これを知らないと、ポインタでバイト配列として読み取ったときに
「えっ、値が逆に見える!」って混乱しやすいんです。
ここを先に押さえておくと、ポインタ・配列・構造体・通信・ファイル保存が一気に理解しやすくなりますよ。

バイト順序とは(複数バイトの値がどう並ぶか)

char は 1バイトですが、int や long、double などは 複数バイトで表現されます。
その複数バイトをメモリ(低いアドレス→高いアドレス)に並べる順番が バイト順序 です。

1バイトと複数バイトの違い

型の例典型的な大きさメモリ上の見え方
char1バイト並び順の問題が起きにくい
int4バイト(多くの環境)4つのバイトの並びが問題になる
double8バイト(多くの環境)8つのバイトの並びが問題になる

※正確なサイズは処理系で変わるので、sizeof で確認できます。

エンディアン(little / big)を図でつかむ

ここでは説明を分かりやすくするため、32ビット値(4バイト)を例にします。
値は 0x12345678 とします。

図:同じ値でも並びが2種類ある

リトルエンディアン(little endian)
下位バイト(78)が低アドレス側に来ます。

ビッグエンディアン(big endian)
上位バイト(12)が低アドレス側に来ます。

この図の読み方(大事)

  • 「低アドレス→高アドレス」は、メモリを左から右に並べたイメージです。
  • 同じ 0x12345678 でも、先頭(最小アドレス)のバイトが変わります。
  • 先頭アドレスを指すポインタで 1バイトを読むと、見える値が変わることがあります。

なぜエンディアンが重要?(ポインタでバイトを見ると差が出る)

ポインタは「オブジェクトの先頭アドレス」を指します。
つまり、unsigned char * のように バイト単位で覗くと、先頭バイトが何になるかはエンディアンに依存します。

先頭バイトがどう見えるか

32ビット値リトルエンディアンで先頭バイトビッグエンディアンで先頭バイト
0x123456780x780x12

この違いが、バイナリ解析・通信・ファイル形式の読み書きで効いてきます。

ポインタの基本(& と * をやさしく整理)

ここからは、エンディアンの話とつながる「ポインタで覗く」ための基本です。

アドレス演算子 &

  • 役割:オブジェクトのアドレス(場所)を取り出す
  • 例:&n は n の先頭アドレス

間接演算子 *

  • 役割:ポインタが指す先の値にアクセスする
  • 例:p が n を指すなら、*p は n と同じものとして扱える(読み書きできる)

& と * の対応

記号呼び方意味
&アドレス演算子(単項)住所を取り出す&n
*間接演算子(単項)指している先を読む/書く*p

※ビットANDの &(2項)と混同しないように、1つに付く & はアドレス演算子と覚えるのがコツです。

サンプルプログラム

目的はシンプルです。
1つの 32ビット値を用意して、ポインタで先頭から4バイトを表示し、エンディアンの違いが見えるようにします。

プロジェクト名:chap10-3-1 ソースファイル名:chap10-3-1.c

#include <stdio.h>

int main(void)
{
    unsigned int v = 0x12345678;

    puts("4バイトの並びを先頭から確認します。");

    unsigned char *p = (unsigned char *)&v;

    printf("値 v          : 0x%08X\n", v);
    printf("v のアドレス  : %p\n", (void *)&v);

    printf("先頭バイト p[0]: 0x%02X\n", p[0]);
    printf("次のバイト p[1]: 0x%02X\n", p[1]);
    printf("次のバイト p[2]: 0x%02X\n", p[2]);
    printf("次のバイト p[3]: 0x%02X\n", p[3]);

    return 0;
}

実行結果のイメージ(例)

  • リトルエンディアンなら、先頭から 78 56 34 12 のように出やすい
  • ビッグエンディアンなら、先頭から 12 34 56 78 のように出やすい

※実際の結果は環境で変わります(ここがまさにエンディアンの話です)。

このサンプルで何をしているか

v をバイト列として覗いているイメージ

v(unsigned int): 0x12345678

p(unsigned char*)は v の先頭アドレスを指す
p[0], p[1], p[2], p[3] で 1バイトずつ読む

ここでのポイント(重要)

  • v は 4バイトの塊(多くの環境)としてメモリに置かれる。
  • (unsigned char *)&v によって「1バイト単位の視点」に切り替えている。
  • p[0] は「先頭アドレスの1バイト」なので、エンディアンで中身が変わる。

登場する命令(関数)と書式・何をする?

このサンプルで使った「命令」は主に puts と printf です。加えて、キャストや配列アクセスも重要なので合わせて整理します。

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示して改行も出します。
  • 今回の役割:説明文を1行出す。

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に従って値を表示します。
  • 今回の役割:16進表示やアドレス表示、バイト表示

(void *) へのキャスト(アドレス表示の作法)

  • 例:(void *)&v
  • 何をする?:ポインタの型を void * に合わせる(%p に渡すときの作法)
  • ねらい:環境差による警告を避け、読みやすくする。

(unsigned char *) へのキャスト(バイト単位で覗く)

  • 例:unsigned char *p = (unsigned char *)&v;
  • 何をする?:v の先頭アドレスを「1バイト単位で扱うポインタ」に変換する。
  • ねらい:p[0] などで、メモリの生バイトを確認する。

printf の変換指定の意味(今回よく使う)

変換指定表示内容
%08X8桁・0埋めの16進(大文字)0x12345678 をそろえて表示
%02X2桁・0埋めの16進(バイト向け)0x7A など
%pポインタ(アドレス)(void *)&v

ちょい注意:エンディアンはどこで困る?

エンディアンは「普段のCの計算」では意識しなくても動くことが多いです。困るのは主にこういう場面です。

表:困りやすい場面

場面何が起きる?対策の考え方
ネットワーク通信バイト順が規定されていることが多い送受信時に変換する(プロトコル仕様に従う)
バイナリファイル別環境で読むと値が逆に見えることがあるファイル形式のエンディアンに合わせる
生メモリ解析先頭バイトの意味が変わるエンディアンを前提に読む

使った表や図の説明(読み方のコツ)

  • エンディアンの図は「低アドレス→高アドレス」の方向に、1バイト箱を並べています
    先頭箱が p[0] だと思って見ると、ポインタと直結して理解できます。
  • サンプル分解図は「型の視点」を切り替えている点がポイントです。
    unsigned int は 4バイトの塊、unsigned char * は 1バイト刻みで覗く視点、という感じです。
  • 変換指定の表は、表示が崩れやすい箇所(%p、%02X、%08X)をまとめています。
    実験コードの見た目が整うと、理解も一気に進みます。