ポインタ④(バイト単位の処理) | Programming Place Plus C言語編 第34章

トップページC言語編

このページの概要

以下は目次です。


voidポインタ

ここまでに登場したポインタは、指し示す先の型に応じて、int* だとか char* といった型名で表現されていました。たとえば「int*」というポインタは、指し示す先に int型の値があることを期待しているという訳です。

一方で、指し示す先にある値が、どんな型でも構わないという特殊なポインタもあります。これは、void* と表記し、voidポインタ (void pointer) と呼ばれます。

int i = 0;
char c = 'a';

void* p1 = &i;  // int型を指し示せる
void* p2 = &c;  // char型を指し示せる

voidポインタなどというものがあるのなら、いつもそれを使えばいいのではないかと思われるかもしれません。しかし、voidポインタは、指し示す先の型の情報が欠如しているため、間接参照ができません。そのため、何でも指し示せるものの、その先にある値を操作できないのです。役に立つのやら立たないのやらという印象ですが、もちろん適切な使い方があります。

まず、voidポインタは、汎用でないポインタへ暗黙的に型変換できます。そのため、指し示す先の本当の型に合わせて、型変換を行えば、以降は普通に間接参照できます。

【C++ プログラマー】C++ では、voidポインタから通常のポインタ型への暗黙の型変換は行われず、明示的にキャストを行う必要があります(C++編【言語解説】第2章)。

#include <stdio.h>

int main(void)
{
    int i = 0;
    char c = 'a';

    void* p1 = &i;
    void* p2 = &c;

    int* pi = p1;
    char* pc = p2;

    *pi = 100;
    *pc = 'x';
    printf("%d %c\n", *pi, *pc);
}

実行結果:

100 x

このサンプルプログラムは使い方を示しているだけで、何も便利さはありません。voidポインタの代表的な使い道について、この先の項と、次の章とで取り上げます。

メモリ範囲を特定の値で埋める (memset関数)

memset関数を使うと、メモリ上のある範囲内のバイト列を、特定の値で埋められます。memset関数は、<string.h> に以下のように宣言されています。

void* memset(void* s, int c, size_t size);

s が指し示しているメモリ範囲を先頭として、そこから sizeバイト分の範囲の各バイトに、c の値を埋めます。戻り値は、s と同じものが返されます。

バイト単位で値を埋めるのに対して、引数 c が int型であることに注意してください。実際に行われていることは、c の値を unsigned char型にキャストした値を、各バイトに入れていくことです。

s の型が void* であることに注目してください。memset関数は、voidポインタの典型的な使い方の例になっています。つまり、メモリ範囲に置かれる値の型がどうであろうと、memset関数を使えます。たとえば、次のサンプルプログラムを見てください。

#include <stdio.h>
#include <string.h>

int main(void)
{
    struct Point {
        int x, y;
    };

    int a[5];
    struct Point pos;
    int v;

    memset(a, 0, sizeof(a));
    memset(&pos, 0, sizeof(pos));
    memset(&v, 0xff, sizeof(v));

    for (int i = 0; i < 5; ++i) {
        printf("%d ", a[i]);
    }
    printf("\n");

    printf("%d %d\n", pos.x, pos.y);
    printf("%d\n", v);
}

実行結果:

0 0 0 0 0
0 0
-1

int型の配列、構造体変数、int型の変数のそれぞれに memset関数を適用しています。このように、対象物が何であっても使用できるのが、void* の効力です。その代わり、対象物の型に関する情報がないため、どれだけの範囲を埋めればよいのかわからないので、第3引数に大きさを指定してやる必要があるのです。

memset関数は、指定されたメモリの範囲に値を埋めているだけですから、そこに変数があるかどうかは問題ではありません。たとえば、対象が構造体の場合に、パディングがあれば、その部分も埋められます。

また、ポインタ型や、浮動小数点型の変数をクリアする目的で memset関数を使う場合、バイトを 0 で埋めることが、必ずしもヌルポインタや 0.0 でクリアすることと同じではない点に注意してください。

ソースコード上では、0 というヌルポインタ定数によってヌルポインタを表現しますし、0.0 は当然 0.0 を意味していますが、メモリ上のビットの並びとして 0 が並んでいるかどうかとは別の話です。メモリ上での表現は、実行環境ごとの表現形式によって決まることです。memset関数は、実際の表現形式とは無関係に、メモリ上のバイトを埋めるだけです。

memset関数を使って、0 によるクリアを行う場面はよくありますが、そもそも、変数を定義した時点でクリアできるのなら、次の方法を使ったほうがいいです。

int a[5] = {0};
struct Point pos = {0};

効率面でも、安全面においても、この方法が一番優秀です。配列に明示的に与えた要素数よりも、初期値の個数のほうが少ない場合、残りの要素は自動的にデフォルト値で初期化されます(第25章)。構造体でも同様です(第26章)。そのため、このサンプルプログラムのように、先頭の要素に与える初期値だけを記述しておけば、配列全体、構造体全体がデフォルト値でクリアされた状態にできます。

なお、すでに作られている構造体変数をクリアしたいときには、複合リテラル(第26章)を使うという手段もあります。

pos = (struct Point){0};

構造体はそのまま代入可能ですから、メンバすべてが初期化された構造体を複合リテラルで作って代入することによりクリア処理を実現しています。

ところで、次のサンプルプログラムは、恐らく意図した結果になりません。

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[5];

    // a の各要素に 100 を入れる?
    memset(a, 100, sizeof(a));

    for (int i = 0; i < 5; ++i) {
        printf("%d ", a[i]);
    }
    printf("\n");
}

実行結果:

1684300900 1684300900 1684300900 1684300900 1684300900

int型の配列の各要素に 100 を入れるつもりで memset関数を使っています。しかし、実行結果はまるで違う値を出力しています。

memset関数は、1バイト単位で値を埋めることを思い出してください。つまり、a[0]、a[1]、a[2]、a[3]、a[4] のそれぞれに 100 を入れているのではなく、a が使っている範囲の各バイトに 100 を入れています。(sizeof(int) * 5) が 20バイトだとすれば、100 を 20個入れます。

printf関数のところで、“%x” を使って出力するように変えてみると、状況が見えやすいかもしれません。

    for (int i = 0; i < 5; ++i) {
        printf("%x ", a[i]);
    }
    printf("\n");

実行結果:

64646464 64646464 64646464 64646464 64646464

16進数の「64」は、10進数の「100」ですから、確かに 100 が 20個並んでいるようです。

あるいは、後の項で取り上げる方法を使って、バイト単位で値を確認してみても良いでしょう。

メモリ範囲をコピーする (memcpy関数、memmove関数)

メモリ上のある範囲の内容を、別の範囲へコピーするために、memcpy関数、あるいは memmove関数を使用できます。memmove関数も、名前は「move」ですが、コピーします。

memcpy関数と memmove関数は、<string.h> で以下のように宣言されています。

void* memcpy(void* restrict s1, const void* restrict s2, size_t size);
void* memmove(void* s1, const void* s2, size_t size);

どちらの関数も、s2 で指定したメモリアドレスを起点にして、size で指定したバイト数の分だけ、メモリの内容をコピーします。コピー先の先頭のメモリアドレスを s1 で指定します。戻り値は、s1 がそのまま返されます。

memcpy関数と memmove関数の違いは、コピー元とコピー先とで、領域の一部が重なり合っていたときに現れます。memcpy関数では、そのような状況を未定義の動作としていますが、memmove関数は安全にコピーできます。その代わりに、memmove関数の方が実行効率が劣るかもしれません。

仮引数の restrict が、この仕様の違いを端的に表しています。restrict については、第57章で取り上げます。

これらの関数は、コピー元とコピー先を指定するために voidポインタを使用しています。文字列をコピーする strcpy関数というものがありましたが、これの汎用版であると考えられます。文字列専用の strcpy関数は、終端文字(‘\0’) を見つけてコピーを打ち切りますが、memcpy関数、memmove関数は終端文字を特別扱いしません。

これらの関数の使い道としては、配列内の複数の要素をまとめてコピーすることが挙げられます。配列から配列へは代入できませんから、memcpy関数や memmove関数が役立ちます。

この用途では、別の配列へコピーすることが確定的ならば、効率的だと思われる memcpy関数を選択すればいいのですが、そうでないのなら安全な memmove関数を選択するのが無難です。

たとえば、a[0]~a[4] を a[3]~a[7] へコピーするだとか、a[0]~a[9] を a[0]~a[9] へ、つまり自分自身へコピーするといったことがあると、memcpy関数は未定義の結果を生んでしまいます。

#include <stdio.h>
#include <string.h>

void print_array(const int* array, size_t size)
{
    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

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

    memcpy(a, &a[3], sizeof(int) * 3);  // a[3]~a[5] を a[0]~a[2] へコピー
    print_array(a, sizeof(a) / sizeof(a[0]));

    memmove(a, &a[2], sizeof(int) * 4); // a[2]~a[5] を a[0]~a[3] へコピー
    print_array(a, sizeof(a) / sizeof(a[0]));
}

実行結果:

3 4 5 3 4 5
5 3 4 5 4 5


メモリ範囲を比較する (memcmp関数)

memcmp関数は、あるメモリ上の範囲2つの内容を比較できます。memcmp関数は、<string.h> で以下のように宣言されています。

int memcmp(const void* s1, const void* s2, size_t size);

s1 と s2 には、比較したいメモリ範囲の先頭のメモリアドレスを指定します。size は比較する大きさです。

memcmp関数がすることは、文字列に特化した strcmp関数と同じです。違いは、終端文字(‘\0’) を特別扱いせず、比較する範囲を引数 size で指定することだけです。

戻り値についても strcmp関数と同じで、一致か不一致かだけではなく、どちらが大きいかを判断できるように返されますが、memcmp関数では一致か不一致かだけを知りたいことが多いと思われます。

以下は使用例です。

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a1[] = {0, 1, 2, 3, 4};
    int a2[] = {0, 1, 2, 4, 8};

    printf("%d\n", memcmp(a1, a2, sizeof(a1)));

    memcpy(a1, a2, sizeof(a1));
    printf("%d\n", memcmp(a1, a2, sizeof(a1)));
}

実行結果:

-1
0

memcmp関数の使い方で注意が必要なのは、構造体の比較を行うときです。構造体のメンバ間や末尾には、パディングが入ることがありますから、memcmp関数を使って構造体の大きさ分の比較を行うと、パディングの部分も比較対象に含まれます。

memset関数でクリアするなどして、確実に値が入っている状態にしていない限り、パディング部分の状態は不定ですから、正しい比較が行えない可能性があります。

構造体同士の比較が必要なら、メンバ同士を1つ1つ等価演算子(文字列なら strcmp関数)で比較する方法が確実です。比較することがよくあるのなら、比較するための関数を作っておくと良いでしょう。

int equal_data(const struct Data_tag* data1, const struct Data_tag* data2)
{
    return data1->a == data2->a
        && data1->b == data2->b
        && strcmp(data1->s, data2->s) == 0
        ;
}

メモリ範囲を探索する (memchr関数)

memchr関数は、メモリ範囲内から、特定のバイト値を探します。memchr関数は、<string.h> で以下のように宣言されています。

void* memchr(const void* s, int c, size_t n);

s にメモリ範囲の先頭のメモリアドレスを、n に範囲の大きさを指定します。探したい値を c に指定します。

仮引数 c の型は int型ですが、実際には unsigned char型にキャストされたうえで比較を行います。この関数はあくまでも「バイト」を探すことを意図したものなので、10000 のような大きな値を探すことはできません。

メモリ範囲内を先頭から順に探していき、c を unsigned char型にキャストした値と一致するバイトが見つかったら、その位置を指すポインタが返されます。見つからなかった場合は、ヌルポインタを返します。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ab\0cdecde";

    const char* p = memchr(s, 'c', sizeof(s));
    if (p == NULL) {
        puts("not found.");
    }
    else {
        puts(p);
    }
}

実行結果:

cdecde

s の途中には ‘\0’ がありますが、これを無視して探索が行えています。また、探したいものは ‘c’ であり、これは2つ含まれていますが、必ず先頭に近い方のものが見つかります。

memchr関数で注意すべき点は、仮引数 s が constポインタであるのに対し、戻り値は const でないポインタであることです。つまり、s に指定したものが文字列リテラルのような書き換えられないものであっても、戻り値は、それを指し示す const でないポインタを返してきます。うっかりすると、文字列リテラルを書き換えようとするプログラムを書いてしまう恐れがあります。これは、未定義の動作なので要注意です(第32章)。

char* p = memchr("abcde", 'c', 5);
if (p != NULL) {
    *p = 'x';  // 未定義の動作
}

戻り値を使って書き換えを行うつもりでないのなら、つねに constポインタで受け取るように癖を付けた方が良いと思います。むしろ、いかなる場面であっても、const を付けることを優先して考えるぐらいでいるのが良いでしょう。

const char* p = memchr("abcde", 'c', 5);
if (p != NULL) {
    *p = 'x';  // コンパイルエラー
}


バイト列としての操作

ところで、ここまでに登場した mem*** という名前の関数は、どのように実装できるでしょう。memchr関数を例にとって考えてみます。

mem*** 系の関数の実装を考える上で問題なのは、メモリの範囲を voidポインタ(と大きさ)で表現していることです。この章の冒頭で確認したとおり、voidポインタは間接参照できませんから、何らかの具体的なポインタ型に型変換しなければならないはずです。

また、memchr関数が実現する処理の内容を考えてみると、「メモリの範囲の先頭から順番に1バイトずつ調べる」という処理が必要なはずです。どうすれば、1バイトずつ調べることができるでしょうか。

このような、メモリの範囲を 1バイト単位で扱うという場面では、必ず 1バイトの大きさを持つ char型(および、signed char型、unsigned char型)のポインタを利用します。

memchr関数の場合は、仕様上、バイトの値を unsigned char型とみなすことになっているので、それに合わせて実装してみます。

#include <stdio.h>

void* my_memchr(const void* s, int c, size_t n)
{
    const unsigned char* p = s;  // voidポインタを unsigned char のポインタに。
                                 // const は無暗に外さない。

    for (size_t i = 0; i < n; ++i, ++p) {

        // 比較は unsigned char型として行う (memchr関数の仕様)
        if (*p == (unsigned char)c) {

            // 見つかったバイト位置を const なしの voidポインタとして返す
            return (void*)p;
        }
    }

    return NULL;
}

int main(void)
{
    char s[] = "ab\0cdecde";

    const char* p = my_memchr(s, 'c', sizeof(s));
    if (p == NULL) {
        puts("not found.");
    }
    else {
        puts(p);
    }
}

実行結果:

cdecde

このように、char型(あるいは、signed char、unsigned char)のポインタを使うことで、間接参照もできますし、ポインタの加減算で 1バイトずつ指し示す位置の移動もできます。

このサンプルプログラムでは、対象となっているメモリ範囲は char型の配列が置かれている場所なので、バイト単位での操作を行うために、char型のポインタを使うことに違和感はないと思います。もし対象が構造体であったり、int などの他の型の配列であったりしても、まったく同じ方法が使えることは理解しておくと良いでしょう。

たとえば、memcmp関数で、int型の配列同士を比較できていました。1バイト単位に分解して操作を行うのであれば、元がどんな型でも関係ありません。

ただし、書き換えを行うのであれば注意が必要です。たとえば、4バイトで1つの値を表現している int型の値があるとき、その一部のバイトだけを書き換えたらどういう結果になるのでしょうか。きちんと理解していない限りは避けておいた方が良いでしょう。


練習問題

問題① 次のプログラムの実行結果はどうなるでしょうか?

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[5];

    memset(a, 0xff, sizeof(a));

    for (int i = 0; i < 5; ++i) {
        printf("%u ", a[i]);
    }
    printf("\n");
}

問題② memcmp関数を自作してください。

問題③ memcpy関数を自作してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第33章 ポインタ③(引数や戻り値への利用))

次の章へ (第35章 ポインタ⑤(動的メモリ割り当て))

C言語編のトップページへ

Programming Place Plus のトップページへ



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