おさえておきたいプログラミングの基本
ポインタは配列とセットで使用すると実力を発揮します。
前回、ポインタが他の変数に「なりすます」ことにより、一つのポインタで様々な値を設定したり取得したりすることを学びました。実はこの特性は、配列変数に適用すると、より効果が発揮できるのです。そのことについて説明する前に、まずは配列とポインタの関係性についてまなんでみましょう。
listex3-1:main.c#include <stdio.h>
#define SIZE 5
int main(int argc,char** argv){
// サイズ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));
}
return 0;
}
実行結果
ポインタについて説明する前に、まずは3行目に出てくる#defineマクロについて説明しましょう。ここで
#defineマクロで定数を定義となっていますが、これはSIZEという文字列を、5という数値に置き換えるという指定です。C言語では、こういった方法で定数を定義することができます。したがって、13行目、21行目で出てくるfor文は、実質的に
13行目、21行目のfor文の実質とするのと一緒です。この方法の良い点は、#defineマクロでSIZEの数値を変えると、プログラムの他の部分のSIZEも一斉に変わるため、プログラムの変更を 最低限にしても良いという点です。プログラムで同じ値を用いる定数などはこのようにマクロで定義して使いまわすといろいろと便利なのです。
次に、18,19行目のポインタの値の代入をみてみましょう。
ポインタへのアドレスの代入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つ分移動することになるのです。
このように、ポインタには、整数を足してアドレスを移動することができますが、それは、配列の番号を変えるのと等しいことなのです。例えば、
ポインタへのアドレスの代入とすると、pとaの対応は、以下のようになります。(表3-2)
表3-2.ポインタと配列変数の関係性②配列 | a[0] | a[1] | a[2] | a[3] | a[4] |
---|---|---|---|---|---|
該当するポインタ | p-2 | p-1 | p | p+1 | p+2 |
ポインタ変数の値 | *(p-2) | *(p-1) | *p | *(p+1) | *(p+2) |
このように、ポインタと配列変数は相性が良いと言う事がわかったと思います。実は配列変数と言うのは、ポインタのある特殊な形だと言う事が出来るのです。その証拠として以下のプログラムを実行してみてください。
listex3-2:main.c#include <stdio.h>
#include <stdlib.h>
#define SIZE 3
int main(int argc,char** argv){
double d[3] = { 0.2 , 0.4 , 0.6 };
double *p1 = NULL,*p2 = NULL;
int i;
p1 = d; // p1にdのアドレスを代入
p2 = d; // p2にdのアドレスを代入
for( i = 0 ; i < 3; i++){
printf("%f %f %f\n",*(d+i),p1[i],*p2); // p1を配列のように扱う
p2++; // p2のアドレスをインクリメント
}
return 0;
}
実行結果
まず注目してほしいのは、7,8行目で行われている、ポインタへのアドレスの代入です。
ポインタへのアドレスの代入実はこれは、配列変数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()関数を学んだ時、数値を入力するときと文字列を入力するときに書式が違うことを不思議に思った人も多いのではないのでしょうか。実はこの謎の答えは、配列とポインタの関係性にあるのです。数値の入力の時は、
キーボードから数値の入力待ち(nは整数型変数)とし、これに対し、文字列変数の場合は、
キーボードから数値の入力待ち(sは文字列)と記述しました。もうすでにお分かりの通り、文字列はchar型の配列であることから、sは、char型の配列の先頭アドレスになるわけです。このことから、表記の仕方は違うものの、scanf()には、第二引数に変数のアドレスを入れるということは一貫していると言う事がわかります。
今までは、ポインタを使ってすでにある変数のアドレスを取得するという方法を使ってきました。しかし、ポインタを使えば必要な個所で動的にメモリを確保し、必要がなくなったときに破棄すると言う事が出来るようになります。以下のサンプルを入力してみてください。
listex3-3:main.c#include <stdio.h>
#include <stdlib.h>
#define SIZE 3
int main(int argc, char** argv){
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);
return 0;
}
実行結果
11,12行目に出てきたmalloc()(マロック)は、メモリを動的に確保する処理を行う関数です。()内に確保したいメモリの大きさをバイト数で指定します。それに対して、メモリを解放する関数が、free()フリー関数です。()内には、確保したメモリのアドレスを持つポインタを引数として与えます。実際の利用方法は、以下のようになります。まずは、mallocから見てみましょう。
malloc()関数によるメモリの確保。(pをintのポインタ変数と仮定)int型のサイズは、sizeof(int)で取得できますから、ここではそれに更に10をかけ、int10個分、つまり、10の成分の配列変数としてメモリが確保できたわけです。ここで忘れてはいけないのは、mallocの先頭に、指定するポインタへのキャストを入れることです。pはint型のポインタなので、(int*)でキャストします。
そして、このようにして確保したメモリは、以下のようにして開放します。
動的に確保したメモリの開放メモリ解放時には、特にキャストのように型の違いによる特別な処理をする必要がありません。
なお、malloc()~free()を使うためには、stdlib.hをインクルードする必要があります。(listex2-3:2行目)これがないと、mallocおよびfreeはコンパイルエラーになります。また、C言語で動的にメモリを生成するには複数の関数があります(表3-3)。ただ現在、実際にPC上で動作するアプリを作る場合は、malloc()を用いる場合がほとんどなので、以後、ここでは全てmalloc()に統一するものとします。
表3-3.C言語で用いられるメモリ生成のための関数関数名 | 読み方 | 説明 |
---|---|---|
malloc | マロック | 引数で指定したバイト数だけ動的にメモリを確保する。 |
calloc | キャロック | mallocと基本は一緒だが、生成された領域は全て0で初期化されている。 |
realloc | リアロック | 一度確保したメモリを違うサイズで確保し直す。 |
ポインタを使って動的にメモリを確保する処理のメリットは何でしょうか?それはなんといっても、必要な時に確保して、必要が無くなったら破棄できるという点です。例えば、画像のファイルなどは容量が大きく、メモリを圧迫することから、必要な時だけ保持し、不必要になったら破棄することが出来ればメモリの節約をすることが可能です。
最後に、コンピュータのメモリ領域について説明します。コンピュータのメモリには、大きく分けて4つあり、以下の表のようになっています。(表3-4)
表3-4.メモリの4領域番号 | 名前 | 説明 |
---|---|---|
① | プログラム領域 | プログラム(マシン語)が格納される場所。 |
② | 静的領域 | グローバル変数やstatic変数が置かれる領域。 |
③ | ヒープ領域 | 動的に確保されたメモリを置く領域。 |
④ | スタック領域 | ローカル変数などが置かれる領域。 |
②および④は、C言語のプログラムを作成した時にすでに固定されています。それに対し、③はプログラム実行中に自在に必要なだけ確保できるのです。
また、ポインタに関しては、さらに高度な技術が存在します。それが「ポインタのポインタ」及び、「関数ポインタ」に関する知識です。ここでの説明は省略しますが、以下で詳しく説明してありますので、興味がある方は一度見てみることをお勧めします。(表3-5.)
表3-5.高度なポインタに関する知識名前 | 内容 |
---|---|
ポインタのポインタ | ポインタを入れることのできるポインタ。 |
関数ポインタ | 関数を入れることのできるポインタ。 |
練習問題 : 問題3.
一週間でわかるC言語・C++言語がオンライン講座になりました!動画と音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。
Read →本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!
Read →