配列とポインタ

サンプルプログラム

前回、ポインタが他の変数に「なりすます」ことにより、一つのポインタで様々な値を設定したり取得したりすることを学びました。実はこの特性は、配列変数に適用すると、より効果が発揮できるのです。そのことについて説明する前に、まずは配列とポインタの関係性についてまなんでみましょう。

まずは、以下のプログラムを作成してみましょう。

listex3-1:main.c
#include <stdio.h>

#define SIZE    5
  
void main(){
    //  サイズSIZEの配列を用意する。
    int ar1[SIZE];
    char ar2[SIZE];
    int i;
    int* p1 = NULL;
    char* p2 = NULL;
    //  値を代入
    for(i = 0; i < SIZE; i++){
        ar1[i] = i;
        ar2[i] = 'A'+i;
    }
    //  ポインタにアドレスを代入
    p1 = &ar1[0];
    p2 = &ar2[0];
    //  値を出力
    for(i = 0; i < SIZE; i++){
        printf("ar1[%d]=%d *(p1+%d)=%d ",i,ar1[i],i,*(p1+i));
        printf("ar2[%d]=%c *(p2+%d)=%c¥n",i,ar2[i],i,*(p2+i));
    }
}
実行結果
ar[0]=0 *(p1+0)=0 ar[0]=A *(p2+0)=A
ar[1]=1 *(p1+1)=1 ar[1]=B *(p2+1)=B
ar[2]=2 *(p1+2)=2 ar[2]=C *(p2+2)=C
ar[3]=3 *(p1+3)=3 ar[3]=D *(p2+3)=D
ar[4]=4 *(p1+4)=4 ar[4]=E *(p2+4)=E

ポインタについて説明する前に、まずは3行目に出てくる#defineマクロについて説明しましょう。ここで

#defineマクロで定数を定義
#define SIZE    5

となっていますが、これはSIZEという文字列を、5という数値に置き換えるという指定です。C言語では、こういった方法で定数を定義することができます。 したがって、13行目、21行目で出てくるfor文は、実質的に

13行目、21行目のfor文の実質
for(i = 0; i < 5; i++)

とするのと一緒です。この方法の良い点は、#defineマクロでSIZEの数値を変えると、プログラムの他の部分のSIZEも一斉に変わるため、プログラムの変更を 最低限にしても良いという点です。プログラムで同じ値を用いる定数などはこのようにマクロで定義して使いまわすといろいろと便利なのです。

次に、18,19行目のポインタの値の代入をみてみましょう。

ポインタへのアドレスの代入
p1 = &ar1[0];
p2 = &ar2[0];
p1,p2はそれぞれ、int,charのポインタです。ar1,ar2は配列変数なので、&ar1[0]と&ar2[0]はそれぞれ、その配列の先頭の変数のアドレスなのです。このとき、 p1+1は、p1の次のアドレス、つまり、配列でいうと、&ar1[1]に該当し、p2+1も同様に、&ar2[1]と等しくなるのです。p1を例に表にすると、以下のようになります。(表3-1)

表3-1.ポインタと配列変数の関係性①(listex1-1のar1,p1の場合)
配列変数配列変数のアドレス該当するポインタポインタ変数の値
ar1[0]&ar1[0]p1*p1
ar1[1]&ar1[1]p1+1*(p1+1)
ar1[2]&ar1[2]p1+2*(p1+2)
ar1[3]&ar1[3]p1+3*(p1+3)
ar1[4]&ar1[4]p1+4*(p1+4)

例ではar1,p1を取り上げていますが、ar2,p2についても同様です。p1,p2はそれぞれint,charであることから、一つの変数のサイズは変わりませんが、 ポインタに1を足すと、その型のサイズ分後ろのアドレスに移行し、逆に1を引くと前に移行します。これは、配列で言うと、配列の番号1つ分移動することになるのです。

このように、ポインタには、整数を足してアドレスを移動することができますが、それは、配列の番号を変えるのと等しいことなのです。例えば、

ポインタへのアドレスの代入
int a[5] = { 1, 2, 3, 4, 5 };
int *p = &a[2];

とすると、pとaの対応は、以下のようになります。(表3-2)

表3-2.ポインタと配列変数の関係性②
配列a[0]a[1]a[2]a[3]a[4]
該当するポインタp-2p-1pp+1p+2
ポインタ変数の値*(p-2)*(p-1)*p*(p+1)*(p+2)

ポインタとしての配列変数

このように、ポインタと配列変数は相性が良いと言う事がわかったと思います。実は、配列変数と言うのは、ポインタのある特殊な形だと言う事が出来るのです。 その証拠として、以下のプログラムを実行してみてください。

listex3-2:main.c
#include <stdio.h>

void main(){
    //  サイズSIZEの配列を用意する。
	double d[3] = { 0.2 , 0.4 , 0.6 };
    double *p1 = NULL,*p2 = NULL;
	int i;
	p1 = d;	//	p1にdのアドレスを入力
	p2 = d;
	for( i = 0 ; i < 3; i++){
		printf("%f %f %f¥n",*(d+i),p1[i],*p2);
		p2++;	//	p2のアドレスをインクリメント
	}
}
実行結果
0.200000 0.200000 0.200000
0.400000 0.400000 0.400000
0.600000 0.600000 0.600000

まず注目してほしいのは、8,9行目で行われている、ポインタへのアドレスの代入です。

ポインタへのアドレスの代入
p1 = d;
p2 = d;

実はこれは、配列変数dの先頭アドレス、つまり、&d[0]の値を代入するのと同じ処理なのです。つまり、このことからわかるとおり、配列変数は、 添字がなければ、ポインタ変数と同じようにあつかうことができるのです。

そのため、値も、d[0]は、*d、d[1]は*(d+1)…といった風に、ポインタ風にとることができます。つまり、配列変数というのは、ポインタ変数の特殊な形と言う事が出来るのです。

ただ、このように配列変数はポインタ変数のような方法で表記できるものの、一般のポインタとは違って他の変数のアドレスを取得することはできないので注意が必要です。

更に、変数p2は、値をインクリメントすることによってアドレスを変えています。最初に「p2 = d」とすることによって、p2は&d[0]と同じ値をとることになるますが、 「p2++;」とすると、&d[1]と同じ値、更にもう一度すると、&d[2]と同じ値…という具合になります。(図3-1)

図3-1.ポインタのインクリメント
ポインタのインクリメント

このように、ポインタのアドレスは、値を増減させることによってアドレスを移動することができます。

scanf関数とポインタ

基礎編で、scanf()関数を学んだ時、数値を入力するときと文字列を入力するときに書式が違うことを不思議に思った人も多いのではないのでしょうか。 じつは、この謎が、この配列とポインタの関係性に答えがあるのです。数値の入力の時は、

キーボードから数値の入力待ち(nは整数型変数)
scanf("%d",&n);

とし、これに対し、文字列変数の場合は、

キーボードから数値の入力待ち(sは文字列)
scanf("%s",s);

と記述しました。もうすでにお分かりの通り、文字列はchar型の配列であることから、sは、char型の配列の先頭アドレスになるわけです。 このことから、表記の仕方は違うものの、scanf()には、第二引数に変数のアドレスを入れるということは一貫していると言う事がわかります。

動的なメモリの生成

サンプルプログラム

今までは、ポインタを使ってすでにある変数のアドレスを取得するという方法を使ってきました。しかし、ポインタを使えば、 必要な個所で動的にメモリを確保し、必要がなくなったときに破棄すると言う事が出来るようになります。以下のサンプルを入力してみてください。

listex3-3:main.c
#include <stdio.h>
#include <stdlib.h>

#define SIZE	3

void main(){
    int* p1 = NULL;
	double *p2 = NULL;
	int i;
	//	配列の生成
	p1 = (int*)malloc(sizeof(int)*SIZE);
	p2 = (double*)malloc(sizeof(double)*SIZE);
	//	値の代入
	for(i = 0; i < SIZE; i++){
		p1[i] = i;
		p2[i] = i / 10.0;
	}
	//	結果の出力
	for(i = 0; i < SIZE; i++){
		printf("p1[%d]=%d  p2[%d]=%f¥n",i,p1[i],i,p2[i]);
	}
	//	メモリの開放
	free(p1);
	free(p2);
}

実行結果
p1[0]=0 p2[0]=0.00000
p1[1]=1 p2[1]=0.10000
p1[2]=2 p2[2]=0.20000

11,12行目に出てきたmalloc()(マロック)は、メモリを動的に確保する処理を行う関数です。()内に確保したいメモリの大きさをバイト数で指定します。 それに対して、メモリを解放する関数が、free()フリー関数です。()内には、確保したメモリのアドレスを持つポインタを引数として与えます。

実際の利用方法は、以下のようになります。まずは、mallocから見てみましょう。

malloc()関数によるメモリの確保。(pをintのポインタ変数と仮定)
p= (int*)malloc(sizeof(int)*10);

int型のサイズは、sizeof(int)で取得できますから、ここではそれに更に10をかけ、int10個分、つまり、10の成分の配列変数としてメモリが確保できたわけです。 ここで忘れてはいけないのは、mallocの先頭に、指定するポインタへのキャストを入れることです。pはint型のポインタなので、(int*)でキャストします。

そして、このようにして確保したメモリは、以下のようにして開放します。

動的に確保したメモリの開放
free(p);

メモリ解放時には、特にキャストのように型の違いによる特別な処理をする必要がありません。

なお、malloc()free()を使うためには、stdlib.hをインクルードする必要があります。(listex2-3:2行目)これがないと、mallocおよびfreeはコンパイルエラーになります。 また、C言語で動的にメモリを生成するには複数の関数があります(表3-3)。ただ現在、実際にPC上で動作するアプリを作る場合は、malloc()を用いる場合がほとんどなので、以後、ここでは 全てmalloc()に統一するものとします。

表3-3.C言語で用いられるメモリ生成のための関数
関数名読み方説明
mallocマロック引数で指定したバイト数だけ動的にメモリを確保する。
callocキャロックmallocと基本は一緒だが、生成された領域は全て0で初期化されている。
reallocリアロック一度確保したメモリを違うサイズで確保し直す。

ポインタを使って動的にメモリを確保する処理のメリットは何でしょうか?それはなんといっても、必要な時に確保して、必要が無くなったら破棄できる という点です。例えば、画像のファイルなどは容量が大きく、メモリを圧迫することから、必要な時だけ保持し、不必要になったら破棄することが出来れば メモリの節約をすることが可能です。

メモリの4領域

最後に、コンピュータのメモリ領域について説明します。コンピュータのメモリには、大きく分けて4つあり、以下の表のようになっています。(表3-4)

表3-4.メモリの4領域
番号名前説明
プログラム領域プログラム(マシン語)が格納される場所。
静的領域グローバル変数やstatic変数が置かれる領域。
ヒープ領域動的に確保されたメモリを置く領域。
スタック領域ローカル変数などが置かれる領域。

②および④は、C言語のプログラムを作成した時にすでに固定されています。それに対し、③はプログラム実行中に自在に必要なだけ確保できるのです。

ポインタに関する高度な知識

高度なポインタの知識

また、ポインタに関しては、さらに高度な技術が存在します。それが「ポインタのポインタ」及び、「関数ポインタ」に関する知識です。ここでの説明は省略しますが、以下で詳しく説明してありますので、興味がある方は一度見てみることをお勧めします。(表3-5.)

表3-5.高度なポインタに関する知識
名前内容
ポインタのポインタポインタを入れることのできるポインタ。
関数ポインタ関数を入れることのできるポインタ。