配列とポインタ | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、前のページに続いて「配列」に関する話題を取り上げます。前のページでは深入りしなかった、配列からポインタへの変換についての話題を通して、ポインタについてもより詳しく学ぶことにします。配列をバイトの集まりとみなして 1バイトごとにアクセスする方法や、安全で分かりやすいプログラムを書く助けになる const についても触れています。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



配列とポインタ

配列」のページで説明したとおり、一部の例外的な場面を除いて、式の中で配列はポインタに変換されます。そのため、配列どうしを等価演算子で比較するとうまくいきませんし関数に配列を渡したり、関数から配列を返したりすることもできません

配列がポインタに変換された結果は、その配列の先頭要素のメモリアドレスです。その型は、配列の要素の型に、ポインタ型であることをあらわす * を付加したものです。int型の配列は int* になりますし、double型の配列なら double* になるので、そのつもりで変数を初期化したり、代入したりできます。auto による型推論では、配列型ではなくポインタ型に推論されます。

int array[] {1, 2, 3, 4, 5};

// いずれも OK
int* p {array};
p = array;

// 以下は int* に推論される
auto a = array;
auto* a = array;  // 明確にするため auto* としても、同じ結果になる

配列がポインタに変換されたあとでも、添字演算子を使って要素にアクセスできます。parray の先頭の要素のメモリアドレスを保持している状況では、array[2]p[2] はまったく同じことです。

#include <iostream>

int main()
{
    int array[] {1, 2, 3, 4, 5};
    int* p {array};

    p[2] = 100;
    std::cout << p[2] << "\n";

    // array側からアクセスしても同じ
    std::cout << array[2] << "\n";
}

実行結果:

100
100

const

文字列リテラルの正体は char型の配列ですが、より正確には、const修飾された配列です。たとえば、文字列リテラルの "Hello" の型は const char[6] と表現されます。

【C言語プログラマー】C言語の文字列リテラルは const修飾されていません。しかし、内容を変更しようとする行為は未定義動作です。

この const は、型に新たな意味を付け加える(型を修飾する)、型修飾子 (type specifiers) を表しており、同じ型修飾子の一種である volatile と合わせて、CV修飾子 (cv-qualifiers) に分類されます。const が付加された型を「const修飾された型」「const修飾型」のように表現することがあります。また、「const な型」「const な変数」「const なオブジェクト」といった言い方をすることもあります。また、CV修飾子を付加された型を CV修飾型 (cv-qualified types) と呼びます。

volatile はここでは解説しません。

const修飾型の変数は次のように定義します。

const 型名 変数名 初期化子;
型名 const 変数名 初期化子;

const修飾型の変数は、必ず初期値が与えられなければならず、あとから値を書き換えることもできません。いずれかに違反するコードは通常、コンパイラによって検出されてエラーになります。未初期化な状態がないことや、値が変化しないことが保証されるので、プログラムを分かりやすく安全にする効果があります。

int main()
{
    const int value1 {};
    const int value2 {10};
    const int value3;  // エラー。初期化しなければならない

    value1 = 20;  // エラー。値を変更できない
    value2++;     // エラー。値を変更できない
}

型推論では const の有無は無視されますが、明示的に付加することはできます。

const int value {100};
auto v1 = value;        // int
const auto v2 = value;  // const int

文字列リテラルは const修飾された配列なので、次のコードはエラーになります。

"Hello"[2] = 'x';  // エラー。const修飾型の値は変更できない

そもそも少し変なコードに見えるかもしれませんが、文字列リテラルに添字演算子を適用することは問題ありません。c = "Hello"[2] のようにして、文字を読み取ることができます。

const修飾型と、その constされていないバージョンの型との違いは、値が書き換えられるかどうかだけです。表現できる値の範囲や、型の大きさ、アラインメントなどは同じです。


const修飾型の変数には2つの捉え方があります。1つは、「後から値を変更できない、読み取り専用の変数」と捉えることです。変数なので、初期値をほかの変数の値を使って決められる点で、constexpr変数よりも柔軟です。たとえば、何度も繰り返し同じ計算をおこなうとき、その計算結果を変数に取っておくと、計算を1回で済ませられますが、その計算済みの値は変更する必要がありませんから、const修飾型の変数に保存しておくことができます。

const auto tmp = a * b * c;  // 計算結果を取っておく
result = x * tmp + y / tmp + z / tmp;

もう1つは、定数であるとする捉え方です。整数型か列挙型の const修飾型の変数が、定数式で初期化されている場合は、その変数を定数式として使用できます1

【C言語プログラマー】C言語の const は定数として使うことはできません。

int a {3};
const int size {1000};  // 整数型であり、定数式で初期化している
int array[size] {};     // OK

const int x {3};  // 整数型であり、定数式で初期化している
switch (a) {
case x:  // OK
    break;
}

constexpr auto z = size * 10;  // OK

整数型や列挙型の const修飾型であっても、定数式で初期化されていない場合には、定数として使うことはできません。

int a {3};
const int size {1000 * a};  // 定数式で初期化していない
int array[size] {};         // コンパイルエラー

const int x {a};  // 定数式で初期化していない
switch (a) {
case x:  // コンパイルエラー
    break;
}

constexpr auto z = size * 10;  // コンパイルエラー

定数として使う const は constexpr に置き換えられますし、その方が明確です。整数型や列挙型である必要もなくなります。

constポインタ

const修飾型の変数のメモリアドレスを記憶するには、constポインタ (const pointer) を使う必要があります。

constポインタ型の変数を次のように宣言できます。const* よりも手前側に記述することに注意してください。

const int array[] {1, 2, 3};
const int* p {value};  // OK
int const* p {value};  // これも同じ意味

const でないポインタ型に、constポインタを与えることはできません。

const int array[] {1, 2, 3};
const int* pc {value};
int* p {pc};  // エラー

【上級】const_cast やC言語形式のキャストを使ってコンパイルを通すことは可能ですが、元々 const であるオブジェクトを書き換えられる保証はありません。

const修飾型でない変数のメモリアドレスを、constポインタに受け取ることはできます。

int value {100};
const int* p {&value};  // OK

constイテレータと同じ考え方で(「イテレータ」のページを参照)、constポインタは、そのメモリアドレスにある値を書き換えることができません。そのため、書き換えるべきでないことや、書き換えていないことを明確にするために constポインタを使うのは有効な方法です。

int array[] {1, 2, 3};
const int* p {array};
p[0] = 10;   // エラー。constポインタを使った書き換えは不可

int array2[] {10, 20, 30};
p = array2;  // p の値を変更することはできる

「constポインタ」と、「ポインタ変数が const修飾型であること」は異なります。次のコードの p は、constポインタではありません。const を記述する位置に注意してください。

int array[] {1, 2, 3};
int* const p {array};  // p は const修飾型の変数だが、constポインタではない
p[0] = 10;   // OK。constポインタではないので、この書き換えは可能

int array2[] {10, 20, 30};
p = array2;  // エラー。p は const修飾型なので、p 自体を書き換えることはできない

int* const p のように * の後ろ側に const を置いたときは、p 自体が書き換えられないということになります。const int* pint const* p のように * の手前側に const があるのは constポインタです。

constポインタをあらわす const と、const修飾型であることをあらわす const が両方現れることもあります。

int value {100};
const int* const p {&value};  // constポインタであり、p 自体が const
p[0] = 10;   // エラー

int array2[] {10, 20, 30};
p = array2;  // エラー

const修飾されている配列がポインタに変換されるとき、その結果は constポインタです。そのため、文字列リテラルをポインタで受け取るときには、constポインタでなければなりません。

const char* s {"Hello"};  // OK. const char[6] から const char* へ変換できる
char* s {"Hello"};        // エラー。const char[6] から char* へは変換できない

Visual Studio 2015/2017 では2行目のコードもコンパイルできてしまいますが、Visual Studio 2019 からはコンパイルエラーになります。

【C言語プログラマー】C言語では、const な配列を 非const のポインタに変換できましたが、C++ では認められません。C言語に対する互換性を失う仕様変更ですが、const の意味からいって、これは正当な仕様修正といえるでしょう。

前に書いたとおり、型推論では const修飾は無視されますが、constポインタであることは推論されます。

const int value {100};
const int* p {&value};
auto p1 = p;         // const int*
const auto p2 = p;   // const int* const

// * は明示してもいい
auto* p1 = p;        // const int*
const auto* p2 = p;  // const int* const

auto による型推論の結果は決して参照型にはならない(「関数から値を返す」のページを参照)ので、ポインタ型の場合とは挙動が異なっています。

配列を関数に渡す

関数に配列そのものを渡すことはできませんが、ポインタに変換されたうえでならば渡せます。仮引数はポインタ型として宣言します。関数内で要素を書き換えないのであれば、constポインタを使えます。

void f1(int* array);
void f2(const int* array);

int array[] {1, 2, 3, 4, 5};
f1(array);
f2(array);

関数が受け取ったものはもはや配列ではなく、先頭要素のメモリアドレスに過ぎないことに注意が必要です。たとえば「配列」のページで作った SIZE_OF_ARRAYマクロを使って、配列の要素数を得ることはできません。

#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

void f(const int* array)
{
    // 正しくない
    for (std::size_t i = 0; i < SIZE_OF_ARRAY(array); ++i) {
        std::cout << array[i] << "\n";
    }
}

int main()
{
    int array[] {1, 2, 3, 4, 5};
    f(array);
}

実行結果:

1

SIZE_OF_ARRAY(array) を置換した結果は (sizeof(array) / sizeof(array[0])) です。ここで sizeof(array) は、配列全体の大きさではなく、ポインタ型の大きさを調べていることになるので、計算結果は欲しかったものとは違います。

関数内で配列の要素数が必要になるときには、呼び出し側で要素数を計算して渡す方法がよく使われます。

#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

void f(const int* array, std::size_t size)
{
    for (std::size_t i = 0; i < size; ++i) {
        std::cout << array[i] << "\n";
    }
}

int main()
{
    int array[] {1, 2, 3, 4, 5};
    f(array, SIZE_OF_ARRAY(array));
}

実行結果:

1
2
3
4
5

配列を関数から返す

ここまでの話と同じ理屈で、関数から配列を返すには、ポインタ型として返すことになります。しかし、関数を抜け出した時点で寿命が終わってしまう配列を返そうとしてはいけません。

#include <algorithm>
#include <iostream>

int* get_fill_array(int value)
{
    int array[1024];
    std::fill(std::begin(array), std::end(array), value);
    return array;  // 危険。直後に array の寿命が尽きる
}

int main()
{
    int* array {get_fill_array(100)};
    std::cout << array[0] << "\n";  // 未定義動作
}

静的ストレージ期間を持つ配列であれば、このような危険性はないですが、ローカルな配列のメモリアドレスを外にさらすことになります。ほかの関数内の変数にアクセスできることになるので、プログラムとしては分かりづらく管理しづらいものになります。

自動ストレージ期間を持つ配列のメモリアドレスを返していても、次のコードには問題がありません。

#include <algorithm>
#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

int* get_fill_array(int* array, std::size_t size, int value)
{
    std::fill_n(array, size, value);
    return array;  // OK。array の実体は呼び出し元のほうにあるので、ここで寿命が尽きるわけではない
}

int main()
{
    int array[1024] {};
    int* p {get_fill_array(array, SIZE_OF_ARRAY(array), 100)};
    std::cout << p[0] << "\n";  // OK
}

実行結果:

100

この場合、返している array は、仮引数の array です。仮引数の array は元々、main関数側にある array をポインタに変換したものですから、結局のところ、main関数の array のメモリアドレスが get_fill_array関数を経由して返ってきているだけです。main関数の array も、仮引数の array も自動ストレージ期間を持ちますが、main関数の array は、main関数内にいるかぎり寿命が尽きないので、p[0] にアクセスしても問題ありません。

std::string の内部にある文字列へのポインタ

std::string は、char型の生の配列を内部に隠して、安全で便利に使えるようにしたものですが、まれに std::string のまま取り扱うことができず、内部の生の文字列が必要なことがあります。典型的なのは、文字列を渡す関数の仮引数が std::string になっておらず、const char* になっている場合です。

#include <iostream>
#include <string>

// s に c が含まれているか?
bool contains(const char* s, char c)
{
    // ...
}

int main()
{
    std::string s {"abcde"};
    std::cout << std::boolalpha << contains(s, 'c') << "\n";  // エラー
}

対応方法の1つは、&s[0] のようにして、先頭要素を指すポインタを取得することですが、s.length() == 0 の場合に未定義動作にならないように注意が必要です。

もう1つのより良い方法に、c_strメンバ関数8あるいは dataメンバ関数9があります。どちらの関数を使っても同じで、std::string の内部にある文字列への constポインタを返します。s.length() == 0 の場合でも、ヌル文字だけが含まれた文字列を指すポインタを得られます。

【C++98/03 経験者】C++03 までの dataメンバ関数は、返された文字列の終端にヌル文字がありませんでした。C++11 で仕様が変更され、ヌル文字で終端されるようになったため、c_strメンバ関数とまったく同じ意味の関数になっています。

#include <iostream>
#include <string>

// s に c が含まれているか?
bool contains(const char* s, char c)
{
    for (const char* p {s}; p != '\0'; ++p) {
        if (*p == c) {
            return true;
        }
    }
    return false;
}

int main()
{
    std::string s {"abcde"};
    std::cout << std::boolalpha << contains(s.c_str(), 'c') << "\n";
}

実行結果:

true

constポインタでないポインタが欲しい場合は、(C++14 では)&s[0] の方法を使うしかありません。

【C++17】dataメンバ関数に、constポインタでないポインタを返すタイプが追加されました9。この追加は c_strメンバ関数には行われていないので、相互に置換可能なまったく同じ関数というわけでもなくなっています。

ただし、constポインタを使っていないということは、ポインタを経由した書き換えが行われるかもしれないということです。std::string が管理している内部の文字列を直接書き換えられると危険です。たとえば、+=演算子を使うなどの正当な方法を使わずに文字を追加すると、おかしなことになります。

#include <iostream>
#include <string>

// 末尾に1文字追加する
void add_char(char* s, char c)
{
    char* p {s};
    while (*p != '\0') {
        ++p;
    }
    *p = c;
    *(p + 1) = '\0';
}

int main()
{
    std::string s {"abcde"};
    add_char(&s[0], 'f');  // 危険
    std::cout << s.length() << "\n";
    std::cout << s << "\n";
}

実行結果:

5
abcde

意図どおりなら、1文字追加後の文字列は “abcdef” ですし、文字数は 6 であるはずですが、実行結果では変化が起きていません(Visual Studio 2015 で確認)。

ポインタ演算

ここまでポインタ型に対して、添字演算子以外の演算子を使うことはありませんでしたが、ほかにもいくつかの演算子が適用可能です。

間接参照

乗算演算子と同じ * を使って、間接参照(逆参照) (indirection) という操作を行えます。乗算と違って、ポインタの手前側に * を付けるように書きます。つまりこの場合の * はオペランドが1つの単項演算子です。

*ポインタ

間接参照は、ポインタが保持しているメモリアドレスにアクセスする操作で、そこにある値を読み書きします。constポインタの場合は読むことしか許されません。

#include <iostream>

int main()
{
    int array[] {0, 1, 2, 3, 4, 5};

    int* p {array};
    std::cout << *p << "\n";

    p = &array[3];
    std::cout << *p << "\n";

    *p = 100;
    std::cout << *p << "\n";
}

実行結果:

0
3
100

これはイテレータでいうと、デリファレンスのことです(「イテレータ」のページを参照)。イテレータはデータ構造内の要素を「指し示す」という表現で解説しましたが、ポインタも要素を「指し示す」ものと捉えられます。ポインタは、なにかを「指し示す」ことを、メモリアドレスを使って実現しています。

加算

++=++ を使って加算が行えます。

算術型に対する加算とはちがって、型に応じた距離だけメモリアドレスを増加させます。たとえば、int型のポインタを +1 すると、int型1個の大きさ分(sizeof(int))だけ値が増加します。したがって、+1 の加算を繰り返し行けば、配列の要素を1つずつ順番にアクセスできます。

#include <iostream>

int main()
{
    int array[] {0, 1, 2, 3, 4, 5};
    int* p {array};

    std::cout << p << "\n";
    std::cout << p + 1 << "\n";
    std::cout << p + 2 << "\n";

    ++p;
    std::cout << p << "\n";

    p += 3;
    std::cout << p << "\n";
}

実行結果:

00D6FA10
00D6FA14
00D6FA18
00D6FA14
00D6FA20

p を 1 加算するごとに、出力されるメモリアドレスが 4 ずつ増加していることが分かります(この処理系では int型の大きさが 4 であるため)。

配列の最後の要素の1つ後ろを指し示すポインタを作ることが許されています。しかし、さらに1つ後ろを指したり、先頭の要素よりも手前を指したりするポインタを作ることは未定義動作です(指し示す先をアクセスしないとしても)。2

int array[] {0, 1, 2, 3, 4, 5};
int* p {&array[5]};

++p;  // OK. array の末尾の1つ後ろを指し示す
++p;  // 未定義動作

減算

--=-- がそれぞれ適用できます。もちろん加算の反対の動作になり、対象の型の大きさに応じてメモリアドレスが減算されます配列の先頭の要素よりも手前を指してしまった時点で未定義動作になります。

#include <iostream>

int main()
{
    int array[] {0, 1, 2, 3, 4, 5};
    int* p {&array[5]};

    std::cout << p << "\n";
    std::cout << p - 1 << "\n";
    std::cout << p - 2 << "\n";

    --p;
    std::cout << p << "\n";

    p -= 3;
    std::cout << p << "\n";
}

実行結果:

00AFFDC8
00AFFDC4
00AFFDC0
00AFFDC4
00AFFDB8

もう1つ特別な使い方があって、同じ配列内の要素を指し示している2つのポインタどうしで減算することができ、それぞれが指し示している要素間に、何要素あるかを計算します。

#include <iostream>

int main()
{
    int array[] {0, 1, 2, 3, 4, 5};
    int* p1 {&array[1]};
    int* p2 {&array[6]};

    std::cout << p2 - p1 << "\n";
}

実行結果:

5

加算」のところで触れたように、配列の末尾の要素の1つ後ろを指し示すポインタは許されるので、&array[6] は問題ありません。

ポインタどうしの減算の結果は、<cstddef> で定義されている std::ptrdiff_t という型です。符号付き整数型であることは決められていますが、具体的な大きさは処理系定義です3

バイト列

現在の目標であるバイナリエディタは、メモリの内容を確認するために用いるツールです。メモリはバイトが並んだものなので、各バイトにどんな値が入っているかを表示すれば、メモリの内容を可視化できます。

たとえば 4バイトの int型の値をそのまま表示するのではなく、1バイトずつに分割して、それぞれのバイトの値を表示するようにします。10億という値は、2進数では「00111011100110101100101000000000」という 32ビットで表現できますが、これを 1バイト(8ビット)ごとに区切ると、「00111011」「10011010」「11001010」「00000000」になります。一般的にバイナリエディタでは、これらを 16進数に変換して 2桁表記で表示します。すると、「3B」「9A」「CA」「00」となります。

このように、int型の整数値だったという事実は無視して、バイトの中身が何であるかだけを表示することを望みます。このような、本来の意味が無視された単なるバイトの並びをバイト列 (byte string) と呼びます。

バイト列は 1バイト単位の配列であると考えられます。そのため C++ では、大きさが 1バイトである unsigned char型の配列を使って表現します。符号にも用がないので、unsigned を使います。

【C++17】std::byte型という、より意味が明確になる型が追加されました4

バイナリエディタで確認したいメモリの範囲が unsigned char型の配列で表現できれば、あとはその要素を出力すればいいだけです(16進数に変換することも簡単にできます)。「ポインタ演算」の項で説明したように、ポインタに対する加算の演算は、ポインタの型に応じた距離だけ移動するという特性を持っていますから、int型の配列を unsigned char型のポインタで指し示してやれば、++ を繰り返すことで、1バイトずつアクセスできます。

とはいえ、int型の配列は int* にしか変換されませんし、int* から unsigned char* のような、異なるポインタ型への変換も自由には行えません。

int array[1024];  // 各バイトを確認したい配列

unsigned char* bytes {array};  // コンパイルエラー
bytes = array;                 // コンパイルエラー

これはキャストによって解決できます。ただし static_cast ではなく、以下の2つの方法のいずれかを使わなければなりません。

bytes = reinterpret_cast<unsigned char*>(array);
bytes = (unsigned char*)array;

1行目のほうは、reinterpret_cast を用いています。reinterpret_cast は、指し示す先の型が異なるポインタどうしでの変換や、ポインタ型と整数型のあいだでの変換といった、限られたケースでのみ許されるキャストです。reinterpret_cast が許可するこれらのキャストは、結果が保証される範囲が限定的であったり、型変換はできるものの、変換後の値の扱い方を間違えると未定義動作などの危険な結果になることが多く、使用には慎重にならなければなりません。

たとえば、アラインメント(「メモリとオブジェクト」のページを参照)の要求が厳しくなる方向への変換(char* から int* など)は、変換自体はできるものの危険性があります。char* のポインタは、どんなメモリアドレスでも指し示せますが、int* ではたとえば 4 の倍数のメモリアドレスを指し示すことしか保証できないためです。キャスト後の型で間接参照を行った途端に、異常な結果になるかもしれません。

【C++98/03 経験者】reinterpret_cast で異なる型のポインタにキャストすることには結果の保証がなかったため、static_cast<T*>(static_cast<void*>(p)) のように、static_cast を使って、void* を経由させるコードを書くことがありました。C++11 以降、アラインメントの要求が厳しくならないのであれば、reinterpret_cast<T*>(p) がまったく同じ意味になる保証があります5

2行目のほうは、C言語から受け継いだキャスト構文です。キャストの意図が分かりづらく、難解な型変換でも許されることがあり、C++ での利用は推奨されません6(こちらの方が入力量が少なくて、使いたくなりますが)。

C++ のキャストに static_cast や reinterpret_cast のように複数の種類が存在するのは(ほかに2つあります)、キャストの意図をプログラムが明確に示すことで、間違った使い方をコンパイラに発見してもらう意味があります。また、ソースコード上で目立つので、本来の型ではない型として、いわば例外的な扱いをしている箇所を見つけやすくなる利点もあります。

【上級】C言語のキャスト構文は、static_cast、reinterpret_cast(および const_cast)で行えることがすべて許され(異なるタイプのキャストを同時にまとめて行うことも含む)、さらに追加でいくつかの(多くは奇怪な)変換が可能になっています。7

次のプログラムは、int型の配列の中身を 1バイト単位で出力するものです。

#include <iostream>

int main()
{
    int array[] {7249, -53075, 547, 6082, 120};

    auto* bytes = reinterpret_cast<const unsigned char*>(array);
    for (std::size_t i = 0; i < sizeof(array); ++i) {
        std::cout << static_cast<unsigned int>(bytes[i]) << " ";
    }
    std::cout << "\n";
}

実行結果:

81 28 0 0 173 48 255 255 35 2 0 0 194 23 0 0 120 0 0 0

unsigned char型のポインタさえ得られれば、あとは末尾までループさせながら出力していくだけです。末尾の位置は、変換前の配列 (array) の大きさによって判断します。1バイト単位で取り扱うので、要素数ではなくて、sizeof で得られる大きさそのものを使います(i < sizeof(array))。なお、std::cout は unsigned char型を整数として出力してくれないので、unsigned int にキャストしてから出力させています。

マニピュレータ

出力を 16進数にしたければ、std::hex を使います。次のように、std::cout のところに挿入するだけです。

#include <iostream>

int main()
{
    int array[] {7249, -53075, 547, 6082, 120};

    auto* bytes = reinterpret_cast<const unsigned char*>(array);
    for (std::size_t i = 0; i < sizeof(array); ++i) {
        std::cout << std::hex << static_cast<unsigned int>(bytes[i]) << " ";
    }
    std::cout << "\n";
}

実行結果:

51 1c 0 0 ad 30 ff ff 23 2 0 0 c2 17 0 0 78 0 0 0

同様に、8進数なら std::oct を使います。10進数に戻すために std::dec もあります。残念ながら 2進数で出力するものはありません。

【上級】8進数を表すプリフィックス 0 や、16進数を表すプリフィックス 0x を付加したい場合は、std::showbase と併用します。

16進数の a~f を大文字にしたければ、std::uppercase を併用します。反対に小文字にする(戻す)には、std::nouppercase を使います(lowercase ではありません)。

#include <iostream>

int main()
{
    int array[] {7249, -53075, 547, 6082, 120};

    auto* bytes = reinterpret_cast<const unsigned char*>(array);
    for (std::size_t i = 0; i < sizeof(array); ++i) {
        std::cout << std::hex << std::uppercase << static_cast<unsigned int>(bytes[i]) << " ";
    }
    std::cout << "\n";
}

実行結果:

51 1C 0 0 AD 30 FF FF 23 2 0 0 C2 17 0 0 78 0 0 0

さらに、各バイトごとに 2桁ずつの出力になるように統一しましょう。これを実現するには、std::setwstd::setfill を使います。これらには実引数の指定が1つずつ必要で、std::setw のほうは出力時の最小幅(文字数)を指定し、std::setfill のほうは最小幅に満たないときに、空いてしまう桁を埋める文字を指定します。なお、<iomanip> という標準ヘッダのインクルードが必要です。

#include <iomanip>
#include <iostream>

int main()
{
    int array[] {7249, -53075, 547, 6082, 120};

    auto* bytes = reinterpret_cast<const unsigned char*>(array);
    for (std::size_t i = 0; i < sizeof(array); ++i) {
        std::cout << std::setw(2) << std::setfill('0') << std::hex << std::uppercase
            << static_cast<unsigned int>(bytes[i]) << " ";
    }
    std::cout << "\n";
}

実行結果:

51 1C 00 00 AD 30 FF FF 23 02 00 00 C2 17 00 00 78 00 00 00

std::hex、std::setw など、ストリームの動作を変更するような機能を総称して、マニピュレータ (manipulator) と呼びます。<iomanip> の「manip」はマニピュレータのことです。

エンディアン

さきほどのプログラムの出力結果は本当に正しいものなのでしょうか?

array の先頭から順番にバイトの中身を出力しているので、出力結果の先頭が array の先頭側のバイトということになります。この処理系では int型は 4バイトなので、先頭の 4バイトは 7249 を出力したものであるはずです。

7249 を 16進数に変換すると 1C51 になります。16進数の 1桁は 4ビットに相当するので、4桁では 16ビット=2バイトです。残りの 2バイトは 0 で埋められて 00001C51 ということになるでしょう。出力結果の先頭の 4バイトは 51 1C 00 00 となっているので、バイトが入れ替わったような状態になっていることが分かります。

2バイト以上で1つのデータを表現する場合に、各バイトをどのような順番で並べるかに関する決まりがあって、これをエンディアン (endian) あるいはバイトオーダー (byte order) と呼びます。エンディアンにはいくつか種類があって、代表的なものにリトルエンディアン(little endian) とビッグエンディアン (big endian) があります。

リトルエンディアンの場合は、下位のバイトが上位側に来るように並びます。00001C5151 1C 00 00 と並んだのはリトルエンディアンを採用している環境だからです。ビッグエンディアンの場合は、上位のバイトがそのまま上位側に来るように並びます。00001C5100 00 1C 51 と並ぶことになります。

リトルエンディアンは素直でないように見えるかもしれませんが、実際には数値の下位の桁ほど、若いアドレスに配置されているのですから、そういう視点で見ると素直な並びであるともいえます。実際このおかげで、4バイトの整数を 2バイトに切り詰めるような処理は効率的に実現できます(上位のアドレスにあるデータを単に無視するだけで良い)。この手の処理は、ビッグエンディアン方式の方が面倒になります。

メモリ上での表現におけるエンディアンは、CPU の種類によって異なることがあるため、移植性が要求されるプログラムで、1バイト単位の操作を行う場合は注意が必要です。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の各定義の違いを説明してください。

  1. char[] s = "abc";
  2. const char[] s = "abc";
  3. char* s = "abc";
  4. const char* s = "abc";
  5. const char* const s = "abc";

解答・解説

問題2 (基本★★)

大きさが 1バイト、2バイト、4バイト、8バイトの整数型の配列をバイト列として出力してみて、メモリ上にどのようにバイトが並ぶか確認してください。

解答・解説

問題3 (応用★★)

char型の配列の内容を逆順にする関数を作成してください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る