ポインタ虎の巻

malloc() されたポインタの使い方

さて、メモリを確保する有用なライブラリ関数に malloc(3) がある。これ自体は、決してOSの機能として実装されているシステムコールではないが、必要に応じてメモリを確保するシステムコール sbrk(2) を呼び出す。malloc(3) は、動的にメモリを確保するために使われる。つまり、実行時にしかサイズが決まらない配列を確保するのに使われる。たとえば、エディタなどを考えてみれば、その文書をメモリ内に保存するためにメモリが確保されるのだが、そのサイズはその文書の大きさによってだけ決まる。ここで静的に配列で確保すると、特定のサイズ以上のファイルは読み込めないことになる。これは困る。だから、こういう場合には、malloc(3) を使って、動的に必要なだけのメモリを確保するのである。

malloc(3) は void 型のポインタを返す。どんなサイズの構造体でも malloc(3) で確保できるのだが、malloc(3) が返すポインタは void 型なので、キャストの必要がある。

struct Point {
    int x;
    int y;
};   
/* ここではまだ、構造体の構造を宣言しただけで、
実体は確保されていない。 */

struct Point *p;  
/* ここではまだ、ポインタしかなく、実体は確保
されていない。*/
p->x = 10;     
/* 実体がないので、セグメンテーションフォルト
などを起こす */
p = (struct Point *)malloc( sizeof(struct Point) );
/* ここでようやく、実体が確保される。*/
p->y = 10;     
/* ちゃんと確保された実体に値が入る */
free( p );
/* 使い終ったら、メモリは返す。

どこでもポインタを宣言することは可能であるが、そのポインタに対する実体の確保は別物である。ポインタの宣言による確保に対して、メモリを伴った実体を与えるのが malloc(3) の役割である。free(3) は malloc(3) によって確保されたメモリが「もう使わない」状態になったときに、再利用のために返却する。

malloc(3) の唯一の引数は、確保されるメモリのサイズである。構造体メモリを確保する場合には、普通 sizeof 演算子が使われる。sizeof 演算子は、構造体宣言からその構造体のサイズを自動的に計算する。これは手動で計算するよりも良い。なぜなら、構造体には「パディング問題」があり、コンパイラが最適化のために、勝手に構造体の中に「詰め物」を入れてしまう可能性があるからである。

ということは、構造体の配列も確保できる。配列名はポインタにほかならないから、次のような確保方法で良い。

struct Point *points = 
    (struct Point *)malloc( sizeof(struct Point) * 10 );
if( points == NULL ) {
        printf( "memory cannot alloc!\n" );
        exit( 1 );
}
for( i = 0; i < 10; i++ ) {
        points[i].x = i;
        points[i].y = i;
}

malloc(3) は失敗する可能性の高いライブラリ関数である。なぜなら、メモリが確保できない場合には、malloc(3) は黙って NULL ポインタを返す。つまり、malloc(3) を使う場合には、戻り値のチェック(NULLでないこと)が必須である。

この時、読者の多くは「何でアクセスが points[i]->x でないんだろう?」という疑問を持ったかも知れない。これは次の理由による。

points はポインタであり、確保されたメモリの先頭を指している。これを配列参照すると、それ自体はちゃんと確保された構造体になっており、断じて points[] はポインタではなく、そこからメモリが確保された実体である。ちょうど、

int *ints = (int *)malloc( sizeof(int) * 10 );
if( ints == NULL ) {
        printf( "memory cannot alloc!\n" );
}
ints[5] = 10; 
/* ints[5] はポインタではない */

であるのと同様である。間違え易いので注意されたい。

malloc(3) はシステムコールではなくて、ライブラリ関数である。実際にはシステムコールである sbrk(2) を内部で呼び出してメモリ管理を行っている。この malloc(3) の実装は、K&R に解説があるので参照すると良いだろう。

malloc(3) の実装を見ると、意外なことに気づく。それは free(3) の実装の部分である。つまり、free(3) は何をしているのかというと、不要になったメモリを、再利用のために「空きメモリリスト」に付け加えているのである。だから free(3) はメモリを解放しない。UNIXなどの仮想メモリ環境では、これは致命的な問題ではない。つまり、使われなくなったメモリ(ページ)は、そのうちにメインメモリから追い出されてしまい、現在使用中のより有効なデータの入ったページがメインメモリに置かれるのである。このロジックは仮想メモリ環境以外では問題がある場合が多い。かつて MS-DOS の時代には、メモリ制限がキビシイので、malloc(3),free(3) によるメモリ管理を回避して、MS-DOS のシステムコールでメモリを確保し管理するのが普通だった。つまり、不要になったメモリを手動で解放していたのである。この知識が有用になることもなくもないだろうから、注記しておく。

また、free(3) は戻り値を返さない(void free(void *ptr); で宣言されている)。しかし、malloc(3) はライブラリ関数である。つまり、これは次のことを意味する。

  1. malloc(3), free(3) はユーザメモリ空間で実行される。
  2. つまり、malloc(3) が利用するグローバルな管理データは、ユーザメモリ空間に存在する。
  3. それはどこにあるのかといえば、ソースレベルではライブラリの中にあり、これは実プログラムにリンクすると、そのプログラムのデータセグメントにまとめられる。
  4. ということは、malloc(3) が利用するグローバルな管理データは、ユーザ・プログラムによって破壊される可能性がある。

お分かりかな? 結果として、malloc(3) が利用するグローバルな管理データ(それらは確保されたメモリへリストのかたちで参照を持っている)が破壊された時には、free(3) を呼び出した時に、セグメンテーション・フォルトが起きるのである。この free(3) でセグメンテーション・フォルトが起きるバグは、その原因を突き止めるのがなかなか難しいバグである。デバッガによってでは、原因が判らないケースがほとんどであり、注意深くソースを検討するか、Electric Fence のようなデバッグ用の malloc(3) ライブラリを使って原因を解明する必要がある。Electric Fence ライブラリは、malloc(3) と free(3) を置き換えるライブラリであり、通常の malloc(3), free(3) よりも厳格なライブラリ利用をする。つまり、連続にメモリを確保せずに、飛び飛びにメモリを確保して、確保したサイズを越えたアクセスがあった時点で、確実にセグメンテーション・フォルトが生成されるようにする。だから、この時にデバッガを使ってやれば、その原因となるアクセスを特定できるのである。有効なライブラリであるので、使ってみると良いだろう。

この malloc(3) によるメモリ確保には、もっと高度な使い方がある。それは realloc(3) を使うやり方である。これは、一旦確保されたメモリのサイズを伸ばしたり縮めたりするために使う。たとえば、エディタであるファイルを読み込んで処理が終ったあと、別なサイズの異なるファイルを読み込むとすれば、文書を保存していたメモリを再利用したくなる。この時、realloc(3) は、確保されたメモリのサイズを調整してくれる。これは非常に重要な使い方であり、中・上級ではメモリ確保についてのほぼ定型的な手法になる。

realloc(3) は2つの引数を取る。最初のものは、現在のサイズが変更されるべきポインタであり、第2の引数は変更後のサイズである。最初の引数は NULL でも構わない。その時は、malloc(3) と同様の動作をする。

int Points_Alloced = 0;
struct Point *alloc_mem( struct Point *buff, int size ) {
        /* もしサイズが異なれば */
        if( size != Points_Alloced ) {  
                void *tmp = realloc( buff, size );
                /* エラーチェック */
                if( tmp == NULL ) {    
                        fprintf( stderr, "cannot realloc memory" );
                        /* 確保できなければ、オリジナルを返す
                           のが正しい場合も多い */
            exit( 1 );
                }
                /* 確保サイズを更新 */
                Points_Alloced = size;   
                return (struct Point *)tmp;
        } else {
       /* サイズが同じならば、オリジナルのまま */
                return buff;  
        }
}

void main() 
{
        struct Point *Points;
        Points = alloc_mem( NULL, 10 );
        ..............
        Points = alloc_mem( Points, 20 );
        ..............
        Points = alloc_mem( Points, 5 );
}

この例では、サイズが縮小する時も、実配列サイズを縮小しているが、一般には拡大する時だけ変更すれば良いだろう。この realloc(3) で確保されたメモリを使う時に、一つ注意が必要である。

void main( )
{
    struct Point *Points = NULL;
    struct Point buff;
    int num = 0;
    int alloced = 0;
    while( fread( buff, sizeof(buff), 1, stdin ) ) {
        if( ++num > alloced ) {
            Points = alloc_mem( Points, alloced + 10 );
            alloced += 10;
        }
        Points[num].x = buff.x;
        Points[num].y = buff.y;
    }
}

これは安全であり、10個単位ごとにメモリを確保するために効率も良い。これがなぜ安全であるかと考えると、Points に対して配列アクセスをしているからである。つまり、次のプログラムは安全ではない。

void main( )
{
    struct Point *Points;
    struct Point *at;
    struct Point buff;
    int num = 0;
    int alloced = 0;

    Points = alloc_mem( NULL, 10 );
    alloced = 10;
    at = Points;
    while( fread( buff, sizeof(buff), 1, stdin ) ) {
        if( ++num > alloced ) {
            Points = alloc_mem( Points, alloced + 10 );
            alloced += 10;
        }
        *at->x = buff.x;
        *at->y = buff.y;
        at++;
    }
}

これは判りやすい例として挙げている。at は、最初に確保された Points を示しているが、配列が拡大されると Points の内容(確保されているメモリ)は移動する可能性がある。ポインタである at はその動作とはまったく連動しない。だから realloc(3) によって無効となったポインタを at は保持し続ける可能性がある。これは一見してマズイと感じることであろう。しかし、これを見つけにくい表現に書き換えることもできる。

int Alloced = 0;
int NowUsed = 0;
struct Points *Points = NULL;

struct Points *newPoint( void )
{
    if( NowUsed + 1 > Alloced ) {
        Points = alloc_mem( Points, 10 );
        Alloced += 10;
    }
    return &Points[NowUsed++];
}


void main( )
{
    struct Point *at, *prev;
    struct Point buff;

    prev = NULL;
    while( fread( buff, sizeof(buff), 1, stdin ) ) {
        at = newPoint();
        if( prev != NULL ) {
            at->x = buff.x + prev->x;
            at->y = buff.y + prev->y;
        } else {
            at->x = buff.x;
            at->y = buff.y;
        }
        prev = at;
    }
}

この時 prev は、それがセットされた段階では有効だったが、realloc(3) によって無効になったポインタを保持する可能性がある。これは見つけにくいバグを作り出す。だからこれは次のように書き直さなくてはならない。

int Alloced = 0;
int NowUsed = 0;
struct Points *Points = NULL;

int newPoint( void )
{
    if( NowUsed + 1 > Alloced ) {
        Points = alloc_mem( Points, 10 );
        Alloced += 10;
    }
    return NowUsed++;
}


void main( )
{
    int at, prev;
    struct Point buff;

    prev = -1;
    while( fread( buff, sizeof(buff), 1, stdin ) ) {
        at = newPoint();
        if( prev != -1 ) {
            Points[at].x = buff.x + Points[prev].x;
            Points[at].y = buff.y + Points[prev].y;
        } else {
            Points[at].x = buff.x;
            Points[at].y = buff.y;
        }
        prev = at;
    }
}

これは若干皮肉なことであるが、ポインタのやや高度なイディオムである realloc(3) を使うためには、配列によるアクセスにしなくてはならない。この理由は今まで見てきた通りである。ポインタでアクセスすると、この時にポインタは「配列ベース+添字」の役割をしてこれを保存する。配列のベースが下請け関数内で変更された時に、ポインタ自体は更新されるわけではないことが、バグを作りだす。もし配列でアクセスをするのならば、コンパイラはこう考える。

  1. 下請け関数を呼び出している。
  2. グローバル変数 Points[] は安全ではない。
  3. よって参照 Points[at] は再計算する(p = Points + at * sizeof(Points))必要がある。

というような流れによって、危険な最適化は抑制される。しかし明示的ではないかたちで Points[] の再割り当てがありうる場合もないわけではない。典型的なケースはスレッドを使っている場合である。このような場合には、処理に対して排他制御(MUTEX)を注意深くつけるなどの配慮が必要である。

この realloc(3) の技は、アプリケーションで勝手に定められた、ハードコーディングされた「最大**数」による制限を越えて、メモリが利用可能な範囲で最大の「**数」を実現する。これは大変有効な技であり、あなたのプログラムでも是非採用すべき技である。さらにより高度なメモリ管理の技である、ガベージコレクタ(GC)も、この realloc(3) の技の上に構築されるのが常識である。

課題

  1. リスト構造を、realloc(3) を使って動的な配列確保で行ってみよう。これは、中・上級プログラムでは、ほぼ定型的な処理であり、実際のプログラムでは嫌というほど体験する。


copyright by K.Sugiura, 1996-2006