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

先頭へ戻る

この章の概要

この章の概要です。

汎用ポインタ (voidポインタ)

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

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

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

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

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

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

C++ では、汎用ポインタから通常のポインタ型への暗黙的な型変換はできず、明示的にキャストを行う必要があります(Modern 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 );
    
    return 0;
}

実行結果:

100 x

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

メモリ範囲を特定の値で埋める (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関数は、汎用ポインタの典型的な使い方の例になっています。つまり、メモリ範囲に置かれる値の型がどうであろうと、memset関数を使うことができます。例えば、次のサンプルプログラムを見て下さい。

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

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

    int a[5];
    struct Point pos;
    int v;
    int i;
    
    memset( a, 0, sizeof(a) );
    memset( &pos, 0, sizeof(pos) );
    memset( &v, 0xff, sizeof(v) );
    
    for( i = 0; i < 5; ++i ){
        printf( "%d ", a[i] );
    }
    printf( "\n" );
    
    printf( "%d %d\n", pos.x, pos.y );
    printf( "%d\n", v );
    
    return 0;
}

実行結果:

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関数は、実際の表現形式とは無関係に、メモリ上のバイトを埋めるだけです。

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

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

int main(void)
{
    int a[5];
    int i;
    
    /* a の各要素に 100 を入れる? */
    memset( a, 100, sizeof(a) );
    
    for( i = 0; i < 5; ++i ){
        printf( "%d ", a[i] );
    }
    printf( "\n" );
    
    return 0;
}

実行結果:

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( 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* s1, const void* s2, size_t size);
void* memmove(void* s1, const void* s2, size_t size);

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

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

これらの関数は、コピー元とコピー先を指定するために汎用ポインタを使用しています。文字列をコピーする 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 printArray(const int* array, size_t size)
{
    size_t i;

    for( 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] へコピー */
    printArray( a, sizeof(a) / sizeof(a[0]) );
    
    memmove( a, &a[2], sizeof(int) * 4 ); /* a[2]~a[5] を a[0]~a[3] へコピー */
    printArray( a, sizeof(a) / sizeof(a[0]) );
    
    return 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)) );
    
    return 0;
}

実行結果:

-1
0

memcmp関数の使い方で注意が必要なのは、構造体の比較を行うときです。構造体のメンバ間や末尾には、パディングが入ることがありますから、memcmp関数を使って構造体の大きさ分の比較を行うと、パディングの部分も比較対象に含まれることになります。memset関数でクリアするなどして、確実に値が入っている状態にしていない限り、パディング部分の状態は不定ですから、正しい比較が行えない可能性があります。

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

int equalData(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;

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

実行結果:

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

また、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)
{
    size_t i;
    const unsigned char* p = s;  /* 汎用ポインタを unsigned char のポインタに。
                                     const は無暗に外さない。*/

    for( i = 0; i < n; ++i, ++p ){
    
        /* 比較は unsigned char型として行う (memchr関数の仕様) */
        if( *p == (unsigned char)c ){
        
            /* 見つかったバイト位置を const なしの汎用ポインタとして返す */
            return (void*)p;
        }
    }

    return NULL;
}

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

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

実行結果:

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];
    int i;
    
    memset( a, 0xff, sizeof(a) );
    
    for( i = 0; i < 5; ++i ){
        printf( "%u ", a[i] );
    }
    printf( "\n" );
    
    return 0;
}

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

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


解答ページはこちら

参考リンク

更新履歴

'2018/6/1 第38章から練習問題⑬を移動してきて、練習問題③とした。

'2018/5/29 この章にあったものを第35章へ移動して統合。事実上、この章の内容はほぼ新規のものになった。
章のタイトルを変更(「ポインタ④(動的なメモリ割り当て)」->「ポインタ④(バイト単位の処理)」)

'2018/5/14 章のタイトルを変更(「ポインタ④ 動的なメモリ割り当て①」->「ポインタ④ 動的なメモリ割り当て」)

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/3/10 全面的に文章を見直し、修正を行った。
章のタイトルを変更(「動的なメモリ」->「動的なメモリ割り当て」)
memset関数」の項を追加。

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





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

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

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS