Unix C プログラミング入門資料(高崎金久)   9.ポインターとメモリー管理 目次 1.ポインターとメモリー管理 2.ポインター変数の基礎知識 3.メモリー管理への応用 -------------------------------------------------------------------------- 1.ポインターとメモリー管理 1)C言語とポインター C言語の特徴の一つはポインター操作を積極的に支援していることににあります. 通常の変数は数値や文字などのデータを格納するためにありますが,ポインター変 数は他の変数(場合によっては関数)のアドレスを格納するために用いられます. ポインターは処理の高速化やメモリーの有効利用に力を発揮しますが,反面,プロ グラムのエラーを招きやすいという問題もあります. ポインターはこれまでの話の中にも隠れた形ですでに現れています.例えば scanf で入力を変数に格納する時には変数名にアドレス演算子& を付けましたが,これは scanf の引数がポインター型だからです.gets, fgets で文字列を入力するときに は文字配列変数を指定するだけで,特にアドレス演算子を必要としませんでしたが, これは「C言語では配列変数が内部的にはポインターとして扱われる」という事情 によるものです.コマンド引数を参照する argv もポインター型です.さらに,フ ァイルストリームはデータ型 FILE * のポインターです. このような隠れた使い方に限らず,C言語ではポインターをさまざまな場面で利用 しますが,以下ではメモリー管理への利用法に焦点を絞って説明します. 2)メモリー管理 C言語では通常の変数は宣言によって領域が確保されますが,それでは都合の悪い 場合もあります.(例:限られたメモリーのもとで大型の配列を使う場合,あらか じめ必要になる領域の大きさがわからない場合,実行時に領域を新たに確保したい 場合,など.)C言語ではプログラムの実行時にシステムからメモリー領域の配分 を受け,使い終れば返す,という機能が利用できます.このようなメモリー管理を 処理系が自動的に行なってくれるプログラミング言語もありますが,C言語ではメ モリー管理もプログラマーに任されています. システムに要求して配分されたメモリー領域はポインター変数で参照します.この 場合,ポインター変数は配列変数における配列名と同じ役割を果たします.その意 味でC言語におけるメモリー管理とポインター変数は密接な関連があります. 2.ポインター変数の基礎知識 1)ポインター変数の宣言,代入,参照 ポインター変数も変数の一種なので使用に先立って宣言が必要です.ポインター変 数を単独で利用することは無意味で,かならず他の変数と組み合わせて用いられま す.ポインター変数 p が変数 x の番地を格納している,という状況は次のように イメージ化できます. p x ------ ---------- | *---|----------->| | ------ ---------- ポインター変数を宣言するときにはポイント先の変数のデータ型を指定します.ポ イント先のデータの型を一般的に TYPE と書くことにすると TYPE 型のデータを指 すポインター変数 p の宣言は次のようになります: 【ポインター変数の宣言】 TYPE *p; 宣言は同じ型の他の変数の宣言と併せて行うことができます.TYPE型変数x ととも に宣言するときには TYPE *p, x; とすればよいわけです. ポインターと変数を結び付けるのはアドレスの代入操作です.TYPE型ポインター変 数 p に TYPE型変数 x のアドレスを代入するには,アドレス演算子 & を使って p = &x; とします.これで p,x が上のイメージのように結び付きました. ポインターの指している変数の内容はポインター演算子 * を付けることで参照でき ます.例えば上の状況で p の指している変数(つまり x)の内容を別のTYPE型変数 y に代入するには y = *p; とします. 2)ポインターと配列の関係 C言語では配列とポインターが密接な関係にあります.いま一般的なデータ型TYPE に対して配列とポインターがあるとしましょう. TYPE *p, x[N]; 配列というのはメモリーの上に連続に並んだ TYPE 型データの領域です.上のよう に p, x が宣言されると,メモリーの上にはそれぞれの領域が確保されます. p x ------ ------------------------------- | | | | | | | | | ... ------ ------------------------------- ここで p = x; という代入文をおくと,p が x を指す,つまり p x ------ ------------------------------- | *--|--------->| | | | | | | ... ------ ------------------------------- という状況になります.ここではアドレス演算子を付けていないことに注意して下 さい.これは「C言語では配列名は配列の先頭アドレスと解釈される」という事情 によるものです. 上の状況で配列の i 番目の要素を参照するには次のような複数の方法があります. 1.i 番目の要素を p[i] で参照する. 2.i 番目の要素を *(p+i) で参照する. 3.p を『p += i;』などで i だけ増やして *p で参照する. 1番目の方法は配列とまったく同じです.2番目の方法は p を基準点(オフセッ ト)から右へ『i 単位』だけずれた場所の内容を参照するというものです.ここで 『単位』といっているのは,データ型 TYPE によってメモリー上に占める領域の大 きさが違うためです.TYPE のデータ領域の大きさを1単位としてその i 倍ずらす というのが p+i の意味です.3番目の方法は p 自体の値を変えてポイント先を参 照するというものです. 3)関数の引数や返戻値としてのポインター 関数の引数としてポインター型のものを指定するには関数プロトタイプや関数定義 の中で該当する変数を TYPE *x というように修飾します.例えば2つの変数のア ドレスを受け取ってその内容を入れ換える関数は次のように定義されます. void swap(int *x, int *y) { int temp; temp = *x; *x = *y; *y = temp; } 呼び出すときには swap(&s, &t) というようにアドレス演算子を付けます.このよ うに,ポインターを引数とする関数は変数の内容そのものを変更するために用いら れます.(配列を引数とする場合と同じ.) 関数の返り値としてポインター型を指定するときも同様です.一般に TYPE 型のポ インターを返す関数は TYPE *function_name(...){ ... } という形で定義します.プロトタイプ宣言も同様です. 4)ポインターのポインター コマンド引数を利用する時の main 関数の定義は (1) main (int argc, char *argv[]){ ... } あるいは (2) main(int argc, char **argv){ ... } という形になります.(1) は argv が char * 型変数の配列であることを意味し ます.(2) はこれとまったく同じことなのですが,配列とポインターの関係を反 映したもので,argv が char * 型データへのポインター(つまりポインター型 データへのポインタ)であることを示しています.図式的に言うと,次のような 状況になっています. char ** char * char ----------- ------------------------------------ argv --> | arvg[0] | ---> | | | | ... | | |---------| ------------------------------------- | argv[1] | ---> | | | | ... | | |---------| ------------------------------------- | | ---> | | | | ... | | : : : i 番目の引数 argv[i] の j 番目の文字は argv[i][j] と参照できますが,この ことから,2次元配列を参照するポインターもポインターポインターであること がわかります.例えば,あまり意味はないですが, char **pp; を用意して pp = argv; と代入すれば pp は argv と同じように使えます. 3.メモリー管理への応用 1)C言語におけるメモリー管理 プログラムをコマンドとして呼び出すと,CPU自体によって実行される機械語命 令群や,命令を実行するために必要なデータや変数領域がメモリーの上に配置され, プログラムの実行が始まります.詳細はシステムや言語処理系によって異なります が,変数の配置場所には大きく分けて次の3通りがあります. 1.大域変数や静的(static宣言された)局所変数がおかれる領域.変数の 場所はプログラム実行中を通じて変化しない. 2.関数の引数や動的(static宣言されていない)局所変数がおかれるスタ ック領域.関数が呼び出されている間だけ存在する. 3.システムに要求して利用できるメモリーの空き領域(ヒープ領域).必 要なくなれば解放してシステムに返すことができる. ヒープ領域は * 画像データや数値計算などのための巨大な配列 * プログラムの実行時に初めて大きさが決まる配列 * リスト・木構造など動的なデータ構造 などを配置するために用いられます.これらは実用的なプログラミングには欠かせ ないものですが,C言語はヒープ領域の取扱いに関して最低限の手段しか提供して いません.あとはすべてプログラマーが自分で記述しなければなりません. 2)メモリー管理のための関数 calloc, malloc, free C言語ではヒープから領域を確保するための関数 calloc, malloc と不要になった 領域を解放する(システムに返す)関数 free が用意されています.領域を確保す るためによく用いられるのは汎用性の高い malloc です.free は使わなくなった 領域を空けて有効利用するために使います.特に,数値計算やグラフィックス処理 などのために巨大な配列用領域を使う場合には,malloc で領域を確保し,不要に なれば free で解放する,というサイクルで処理を進めます. 【メモリー管理用の基本的関数】 ● void *calloc(int n, int s) --- 要素が s バイト,長さが n の配列用 の領域を確保し,その先頭アドレスを返す.確保に失敗すると NULL を返 す.各バイトは00に初期化される. ● void *malloc(int n) --- 長さ n バイトの領域を確保し,その先頭アドレ スを返す.確保に失敗すると NULL を返す.領域は初期化されない. ● void free(void *p) --- ポインター p が指すヒープ領域を解放してシス テムに返す. 【注意】これらを用いるにはファイルの先頭に #include を挿入する. 【使い方】領域に格納するデータの型を一般的に TYPE とする. TYPE *p; /* 領域を参照するためのポインターを用意する */ ... p = (TYPE *)malloc(sizeof(TYPE) * N); /* 長さ N のTYPE 型領域をヒープに確保して p にその先頭アドレスをセットする */ ... free(p); /* 不要になったので p の指す領域を解放する */ ここで (TYPE *) はキャスト演算子と呼ばれるもので,データの型を強制的に変 換します.malloc が返すのは void * という特別なポインター型(どのような データにも対応するポインタ型で,汎用の関数を定義するときに返戻値として 用いられることが多い)です.これをキャスト演算子によって目的の TYPE * 型 に変換しています. また,sizeof は特定のデータ型や変数がメモリー上に占める大きさ(バイト数) を与える演算子です. * sizeof(データ型)または sizeof(変数) --- 指定したデータ型あるいは 変数がメモリー上に占める大きさをバイト数で与える. malloc や calloc はバイト単位で領域を確保しますが,実際に扱うデータにはさ まざまな型があります.TYPE 型データを N 個入れるには TYPE型変数がメモリー 上に占める大きさ(バイト数)x N の長さの領域が必要です.この長さを sizeof 演算子を使って自動的に調べています. 3)ヒープ上に1次元配列をつくる ヒープの上に配列を作る手順は次のようになります. 1.malloc で『要素のデータの大きさ』×『配列の長さ』だけのメモリー を確保する. 2.返ってきたアドレスを目的のデータ型にキャストしてポインター変数 (要素のデータ型に合わせる)に代入する. 3.必要ならば初期化を行う. 逆に解放するには free を呼んでアドレスを渡すだけです.こういう手順はあらか じめ関数として用意しておいて使うのが便利です. 【例】長さ N の double 型配列領域をヒープ上に確保および解放する関数 double *new_dvector(int N) { double *top; top = (double *)malloc(sizeof(double) * N); /* 必要ならここに初期化手続きを置く */ return top; } void free_dvector(double *top){ free(top); } 【使い方】 double *x; ... x = new_dvector(N); /* 配列の生成 */ for (i = 0; i < N; i++) x[i] = sin(M_PI*i/N); /* 普通の配列と同様に使える */ ... free_dvector(x); /* 不要になったら解放する */ 4)ヒープ上に2次元配列をつくる 今度はヒープ上に2次元配列をつくることを考えましょう.配列の大きさはM行N列 とします.配列本体はヒープ上の長さが M * N * sizeof(配列要素のデータ型) の 領域ですが,それ以外に,各行を参照するためのポインターの配列(これもヒープ 上に確保する)とそれらを参照するためのポインターポインター(これが2次元配 列にアクセスする最終的なポインターとなる)が必要です. 基本的な手順は次のようになります. 1.ヒープに長さ M のポインター配列用の領域を確保し,先頭アドレスをポ インターポインターに格納する.これが2次元配列へのポインターとなる. 2.ヒープに長さ M * N の配列用領域を確保し,先頭のアドレスを1のポイ ンター配列の最初の要素に代入する. 3.1のポインター配列の i 番目の要素に2の配列の i*N 番目の要素のアド レスをセットする.これが i 行へのポインターとなる. 4.必要ならば初期化をおこなう. これも関数にしておくのが便利です. 【例】 M 行 N 列 double 型2次元配列領域を確保および解放する関数 double **new_dmatrix(int M, int N){ double **top; int i; /* 第1段階 */ top = (double **)malloc(sizeof(double *) * M); /* 第2段階 */ top[0] = (double *)malloc(sizeof(double) * M * N); /* 第3段階 */ for (i = 1; i < M; i++) top[i] = top[i-1] + N; /* 第4段階 */ /* 必要ならここに初期化手続きを記述する */ return top; } void free_dmatrix(double **top){ free(top[0]); free(top); } 【使い方】 double **A; ... A = new_dmatrix(M, N); /* 配列の生成 */ ... for (i = 0; i < M; i++) for (j = 0; j < N; j++) A[i][j] = sin(a*i + b*j + c); /* 普通の2次元配列と同様に使える */ ... free_dmatrix(A); /* 不要になったら解放する */