ポインタを使う
「前に聞いた電話番号メモった紙、どこに置いたっけなあ……」
データを正しく保存しておく為には、データ自身を記録しておくのはもちろん、その記録をどこに置いたかを覚えておかなければなりません。C言語では、整数型、浮動小数点型の変数の他に、データの置き場所を格納する、ポインタ型という変数を使うことが出来ます。
データが格納されるしくみ
C言語では、変数や構造体を簡単に作成して、値を代入して使用することができますが、そもそも、変数はどこに作られて、どのように管理されているのでしょう。
データが格納されるしくみを中心に、コンピュータのしくみについてお話しましょう。Fig.1:コンピュータのしくみ
ハードウェアアーキテクチャー
皆さんが普段使っているコンピュータは何ですか? AT互換機、Mac、SparcStation、PIC、スーパーコンピュータ、等々、人それぞれに、さまざまなコンピュータを使っていると思います。これらの機種間には、異なる点が多々あります。曰く、載せられるOSが違う、使えるアプリケーションソフトウェアが違う、価格帯が違う、云々。しかし、これらは同じコンピュータというモノなので、最低限コンピュータと呼ばれるに値する共通の特徴は備えています。では、その共通の特徴とは何なのでしょう?
脊椎動物が、脊椎という身体的構成要素を必ず持っているように、今現在の我々の周囲にあるコンピュータにも、共通の身体的構成(ハードウェアアーキテクチャー)が存在します。それを図示したものがFig.1です。
コンピュータと呼ばれるものは、必ず3つの構成要素を備えています。その3つとは、CPU、メモリ、I/Oです。大雑把に言うと、CPUは計算を、メモリは記憶を、I/Oは入出力をつかさどります。「1+1を計算しろ」という仕事が与えられた場合、命令を忘れないように記憶しておいたり、命令の結果を記憶するのがメモリ、実際に1+1を計算するのがCPU、計算結果を画面に表示するのがI/Oから指示を受けたVGAといった具合に、それぞれが役割分担を果たします。
普通、プログラムの実行形式は、ファイルの形でHDDなどの上に保存されています。これがどのように実行されるかというと、
1:実行形式ファイルをI/Oからメモリ上に命令コードとして読み込む。
2:プログラムに必要なデータ領域をメモリ上に確保する。
3:読み込んだプログラム(命令コード)をCPUが解釈しながら動作する。
という手順で行われます。プログラム内で使用されている変数などのデータは、確保されたメモリ上に格納されるわけです。Fig.2:メモリのしくみ
メモリ上のデータ領域
ここまで、メモリと呼んでいるのは、皆さんがコンピュータショップで「メモリ、128MB下さい」と言って買ってくる、まさにあのメモリなのですが、そもそもメモリとは、どのようなモノなんでしょうか?
128MB、64MBなどと呼んでいますが、こうした数値はメモリ容量を表す数値で、数値が大きいほど、実際の動作時に、大きなデータ領域を確保できたりします。また、その単位であるMB(メガバイト)とは、以前お話した、0から255までを表現できる1バイトの1000倍の1000倍のことです。メモリとは、容量分のバイトが、順番に並んでいるものだと言えます。
メモリ上には、命令コードやデータなどを置くことができますが、「とにかく空いてるとこに、やみくもに置いていけ」では、置いたものを後で見つけることができません。置いたものを後で自由に見れるようにするためには、置いた場所を覚えておかなければなりません。このためメモリでは、場所を特定できるように、メモリ上の全ての場所に住所が与えられています。メモリ上の住所は、全て数値で与えられているので、住所というよりは番地になっています。番地は、1バイト毎に割り振られ、メモリ上のバイトの並びに対応して連番が与えられます。(Fig.2参照)Fig.3:変数とメモリ
C言語のchar型変数とデータ領域
C言語上で使用される変数や構造体は、その中にデータを入れておけて、後で参照できるという特徴を持ちますが、これは、メモリ上の特定の区画を変数や構造体として利用しているに他なりません。
例えば、
char ch;
として、char型変数chを作成した場合、このchはメモリ上のある特定の番地と結び付けられます。ここでは0x0104番地と結び付けられたとしましょう。(Fig.3参照)
この時、
ch = 18;
という変数chへの代入操作を行うと、代入された値は実際にはメモリ上のchの実体である0x0104番地に格納されます。
また、
printf("%d", ch);
などによって、chの中身を見たくなった時も、chの実体は0x0104番地の中身だと参照し、値を取り出します。
変数は値を入れておく名前の付いた箱だと考えると、メモリはその箱を並べておく机だと考えることが出来ます。机の上に新しい箱を置くとき、箱を置けるスペースを探して箱を置き、その置いた場所を覚えておいて、箱の名前を言われたら、その箱の場所をすぐに思い出せるようにしておくわけです。
変数がどのような要素から成り立っているのかを考えると、型、名前、値に加えて、メモリ上の番地も重要な要素だと考えられます。これを構造体風に表現すると、
struct 変数 {
型
名前
値
メモリ上の番地
}
といった具合になるでしょう。Fig.4:変数、配列とメモリ
C言語の変数、配列、構造体とデータ領域
char型変数は、その大きさが、ちょうど1バイトなので、メモリ上の番地と1対1の対応が結べますが、それ以外の変数や、配列、構造体は、その大きさが2バイト以上なので、メモリ上の番地と1対1対応になりません。
例えば、int型の大きさが4バイトの場合、どうなるのか考えてみましょう。(Fig.4参照)
メモリ上に4バイトの連続した領域を確保して、int型変数iを作成したとします。このとき、0x0204の1バイトだけを取り出しても意味のあるデータにはなりませんし、0x0205、0x0206、0x0207の1バイトだけを取り出しても意味のあるデータにはなりません。これらは、4バイトまとまって意味のあるint型というデータになるのです。こういう場合、C言語内部では、連続領域の先頭である0x0204だけを記憶します。つまり、変数iのメモリ上の実体は、0x0204という番地で記憶されます。この記憶した領域の先頭番地と、int型は4バイトという知識から、この連続する4バイトを1つの塊として処理を行うようになっています。
次に、char型の配列について考えてみましょう。配列とは、変数が順番に並んだものなので、メモリ上でも変数が順番に並んだ状態で領域が確保されます。char型配列str[4]を作成すると、4バイトの連続領域が確保されます。このとき、0x0304の内容はstr[0]、0x0305の内容はstr[1]になり、int型の時とは違って、先頭番地以外の番地のデータを意味のあるデータとして扱うことができます。それでも、配列と結び付けられて記憶される番地は、先頭の0x0304だけで、配列内の各要素は、配列が格納されている領域の先頭番地、配列の型、要素番号から計算で求められる番地のデータを取り出して使うようになっています。
構造体も、配列のシステムを拡張して処理されます。つまり構造体を格納する領域の先頭番地と、構造体のそれぞれのメンバーが先頭から何バイト目から格納されているかの知識をもとに計算して、メモリ上の場所を特定しています。List.1:list1.c
#include <stdio.h> #include <string.h> typedef struct { char name[32]; int length, weight, test[5]; } member; int main() { char ch; int i = 10000, j; char str[4] = "ABC"; member jan; printf("この処理系におけるポインタのデータ長: %dバイト\n\n", sizeof(char *)); printf("chのサイズ: %dバイト\n", sizeof(ch)); printf("chのポインタ: 0x%X\n\n", (unsigned long)&ch); printf("iのサイズ: %dバイト\n", sizeof(i)); printf("iのポインタ: 0x%X\n\n", (unsigned long)&i); printf("strのサイズ: %dバイト\n", sizeof(str)); printf("strのポインタ: 0x%X\n", (unsigned long)str); for(j = 0; j < 4; j++) printf("str[%d]のポインタ: 0x%X\n", j, (unsigned long)&(str[j])); printf("\njanのサイズ: %dバイト\n", sizeof(jan)); printf("janのポインタ: 0x%X\n", (unsigned long)&jan); printf("jan.weightのポインタ: 0x%X\n", (unsigned long)&(jan.weight)); return 0; }
Fig.5:Windowsでの実行例
Fig.6:Linuxでの実行例
ポインタ
これまでの連載では、変数とはC言語という世界の中の、いわば抽象的なものだったわけですが、実際には、メモリというコンピュータの部品上の特定の領域に表現される情報だというわけです。特定のアドレスに置かれたデータが、C言語からは変数として見えていたわけです。
メモリ上の番地のことを一般的に「アドレス」と呼んでいますが、このアドレスは、C言語においては、特定の型のデータの実体を指し示しているという意味で、「ポインタ」と呼ばれています。最近のOSでは、メモリを管理する複雑な機構が実装されているので、メモリ上の実際のアドレスを直接操作することが一般的には禁止されています。このため、「ポインタ」の値も、実際のアドレスではなく、アドレスに見立てた識別番号になっています。しかし、データの位置を特定する為にポインタが必要なのであって、特定の番地のデータを直接操作することが目的でないと考えれば、不都合はありません。
List.1をコンパイル、実行してみてください。実行結果の例が、Fig.5、Fig.6になります。
変数のポインタを得るためには、単項演算子「&」を使います。また配列のポインタに関しては、[]のつかない配列名のみの変数が、その配列のポインタを格納した変数になっています。構造体のポインタを得るためには、変数のポインタを得るとき同様に、単項演算子「&」を使います。
これをみても分かるように、実際にそこに現れるポインタの値は、環境によって異なる値となります。これは、ハードウェア構成や、OSや、実行時のメモリの状態によって、ポインタの値が異なることを示しています。
このプログラムでは、ポインタの存在を例示する便宜上、ポインタの値を表示していますが、実際のプログラミングでは、これらの値を保存しておいて利用することで、今まで名前でしかアクセスできなかったデータに、ポインタによってアクセスするという新しいデータアクセスの手法を導入することが可能になります。List.2:list2.c
#include <stdio.h> #include <string.h> int main() { char ch1 = 'A', ch2 = 'B', *chp; int j, *ip; chp = &ch1; printf("%c\n", *chp); chp = &ch2; ch1 = ch2; printf("%c, %c\n", *chp, ch1); ch2 = 'C'; printf("%c, %c\n", *chp, ch1); for(j = 0, ip = &j; j < 5; j++) printf("%d\n", *ip); return 0; }
Fig.7:Windowsでの実行例
ポインタ型変数
変数をどの位置に配置し、そのデータへのポインタがどのような値になるかなどは、基本的にC言語が自動的に行うので、プログラマが気にする必要はありません。しかし、データに対して、名前でなく、ポインタでアクセスしたい場合には、ポインタを柔軟に扱う為に、ポインタを記憶しておく変数が必要になります。
ポインタを入れるための変数型をポインタ型変数といいます。ポインタ型変数は、どんなポインタでも入れられるわけではなく、int型データのポインタを入れるための型や、char型データのポインタを入れるための型といった具合に、特定のデータ型のポインタを専門に扱うようにバリエーションが存在します。
ポインタ型変数の作成(宣言)は、その型の変数のポインタを作成するという表現で、
int *ip;
というように行います。これによって、int型変数専用のポインタ変数ipを作成できます。
int j = 2;
ip = &j;
このipに&で求めたポインタを代入すれば、そのポインタを保存しておくことができ、
printf("%d", *ip);
そのポインタで指し示されるデータの内容には、単項演算子「*」でアクセスすることができます。
List.2をコンパイル、実行してみてください。実行結果がFig.7になります。
まず理解しなければならない第1の点は、ch1と*chpが同じchar型であり、&ch1とchpが同じchar *(char型変数専用ポインタ)型だということです。同じ型同士の変数間では、演算や代入が行えますから、chpに&ch1を代入することが可能になっています。
次に理解しなければならないのは、値をコピーしたものを参照することと、ポインタによって指し示されたものを参照することの違いです。List.2の13行では、ch2のポインタをchpに取得し、14行では、ch2の値をch1にコピーしています。この時点でのch1の値と*chpの値は、実行例から同じ値になっていることが分かります。その後17行で、ch2の値を変えると、*chpの値はその変更に追随しますが、ch1は追随しませんので、実行例の表示が異なります。演習:List.2の22行の時点で、ipと同じ値を示す表現を答えよ。また、この時の*ipの値を答えよ。
List.3:list3.c
#include <stdio.h> #include <string.h> int main() { char str1[64], str2[64], str3[64]; int i; strcpy(str1, "ABCDEF"); strcpy(str2, "UVWXYZ"); str3[0] = '\0'; printf("%s\n", str1); printf("%s\n", str2); printf("%s\n\n", str3); strcpy(str3, str1); printf("%s\n", str1); printf("%s\n", str2); printf("%s\n\n", str3); strcpy(str3 + 3, str2); printf("%s\n", str1); printf("%s\n", str2); printf("%s\n", str3); return 0; }
Fig.8:Windowsでの実行例
文字列とポインタ
C言語においては、文字列を表現する為に、char型の配列を用いています。これをもう少し厳密にいうと、文字列の先頭を表すchar型変数へのポインタと、そのポインタから順番にデータを見ていって、最初に現れる'\0'(通常は0)の間のデータが文字列として扱われます。このしくみによって、文字列を表現する為に、文字列を構成する全ての文字データを示す必要はなく、char型変数へのポインタ1つによって表現可能となっているのです。
List.3をコンパイル、実行してみてください。実行結果がFig.8になります。
まず、10行、11行でのstrcpy()と、18行のstrcpy()を見比べてください。文字列を第2引数として与えているのと同じように、char型配列を表すchar型変数へのポインタを与えています。つまり、この2つは同じ型だとして扱われているわけです。
次に、24行のstrcpy()をみると、char型変数へのポインタに3を足しています。ポインタに対しては加減算を行うことができますが、この時、加減される値の単位は「個分」となり、+3で、ポインタを「そのポインタの示す型3個分進める」といった操作を行います。str3が示すのは&(str3[0])ですが、str3+3によって示されるのは&(str3[3])になります。演習:List.3の29行以降に書き加えて、str1とstr2とstrcpy()のみを用いて、str3を"UVWABCXYZDE"とせよ。
List.4:list4.c
#include <stdio.h> #include <string.h> int *liner_interpolation(int start, int end, int count, int *array); int main() { int i, ar[10], *ret; ret = liner_interpolation(0, 10, 10, ar); for(i = 0; i < 10; i++) printf("%d, ", ret[i]); printf("\n"); ret = liner_interpolation(100, 0, 10, ar); for(i = 0; i < 10; i++) printf("%d, ", ret[i]); printf("\n"); ret = liner_interpolation(-10, 10, 10, ar); for(i = 0; i < 10; i++) printf("%d, ", ret[i]); printf("\n"); return 0; } int *liner_interpolation(int start, int end, int count, int *array) { int i; for(i = 0; i < count; i++) array[i] = start + (double)(end - start) * i / (count - 1); return array; }
Fig.9:Windowsでの実行例
関数の引数、戻り値とポインタ
配列や構造体などのように、多くの要素(変数)から構成されているものでも、ポインタによって表現すれば、ポインタ型変数1つで表現できます。この性質を上手く使ってやれば、関数呼び出し時の引数の数を減らしたり、一つの値しか返せない戻り値をポインタにすることで、意味的には複数の値を返しているのと同じにすることができます。極端に言うと、関数に引数を与えるための構造体を用意して、その構造体へのポインタを引数に与えるようにすれば、全ての関数の引数を1つにすることができます。
List.4をコンパイル、実行してみてください。実行結果がFig.9になります。
liner_interpolation()では、関数の処理の結果として、count数分の数値を作成しますが、戻り値で返せる値は1つです。そこで処理結果を収めた配列へのポインタを返すことで、意味的にcount数分の値を返すようにしています。演習:List.4のliner_interpolation()を
#define AR_MAX 10
typedef struct {
int start, end, count, array[AR_MAX];
} li_params;
li_params *liner_interpolation(li_params *lip);
として書き直せ。List.5:list5.c
#include <stdlib.h> #include <stdio.h> #include <string.h> int *liner_interpolation(int start, int end, int count); int main() { int i, j, *ret[3]; ret[0] = liner_interpolation(0, 10, 10); ret[1] = liner_interpolation(100, 0, 10); ret[2] = liner_interpolation(-10, 10, 10); for(i = 0; i < 3; i++) { for(j = 0; j < 10; j++) printf("%d, ", ret[i][j]); printf("\n"); } free(ret[1]); free(ret[2]); free(ret[3]); return 0; } int *liner_interpolation(int start, int end, int count) { int i, *ret; ret = (int *)malloc(sizeof(int) * count); for(i = 0; i < count; i++) ret[i] = start + (double)(end - start) * i / (count - 1); return ret; }
動的メモリ確保とポインタ
前回、プログラム中の如何なる場所からも参照可能なグローバル変数を紹介しましたが、グローバル変数はプログラム開始時に作成されるので、配列をグローバルで作成する場合、配列の要素数として、想定される最大数で作成しなければなりません。また、プログラムの進行具合に合わせて、必要なときに必要なだけグローバルな変数を使いたい場合もあります。
C言語では、動的メモリ確保という手法を使って、必要なときに必要なだけ、グローバル変数と同じ扱いのデータ格納領域を作成することが出来ます。
List.5をコンパイル、実行してみてください。実行結果はFig.9と同様になります。
malloc()を使って動的メモリ確保を行いますが、このとき確保される領域には、通常の変数作成などとは異なり、名前が与えられません。このため、作成した領域のポインタをポインタ型変数に保持しておき、利用しなければなりません。
また、malloc()によって確保された領域は、使い終わったらfree()によって明示的に解放してやる必要があります。
for(i = 0; i < MAX; i++)
p = malloc(sizeof(int) * MAX);
free(p);
free()のタイミングを正しく考えてやらないと、確保した領域を開放する前に、その領域へのポインタを無くしてしまい、メモリが無駄遣いされてしまいます。上記のように書いてしまうと、sizeof(int)*MAX*(MAX-1)バイトのメモリが解放できなくなってしまいます。演習:List.5でfree()は3行に記述があるが、free()の使用を1行のみにし、なおかつ正しくメモリの解放を行えるようにするためには、List.5をどのように書き換えればよいだろう?
まとめ
今回は、C言語の文法解説の最終回として、ポインタとその周辺について解説しました。
ポインタ+malloc()などポインタ周辺の記述手法は、C言語においてもっともバグを含みやすい部分といえます。しかし、これを避けては、C言語でプログラミングするメリットを十分に得られません。
私個人は、初心者のうちはなるべくポインタを使わないようなプログラミングを心がけ、必要に迫られたときだけ使うようにした方が良いと思います。少しずつ慣れるようにして使えば、使い方のコツも徐々に身に付くでしょう。
さて、来月からは気分も一新して、より具体的なプログラミングを見ていきたいと思います。それでは。
コラム
連載後半の番組のお知らせ
来月から連載後半が始まります。
後半の6回では、実際にC言語を用いて、実践的なプログラミングに関する解説を行っていこうと考えています。
以下のテーマを予定しています。
第7回「入出力」
処理が対話形式で進むプログラムや、起動時にコマンドライン引数を与えるプログラム、ファイル入出力を行うプログラムなどの作成を解説。
第8回「アルゴリズム@ソート・探索」
配列の要素を選択ソートで、連結リストをバブルソートで、関数の再帰呼び出しを用いたクイックソートで、それぞれ並べ替えを行うプログラムを作成、解説。
配列内の要素から、条件に合うものを探し出す、探索手法について、線形(逐次)探索、2分探索を解説。
第9回「アルゴリズムA圧縮・ビン詰め問題・巡回セールスマン問題」
データの意味を失わせずに大きさを小さくする圧縮手法について、連長圧縮、スライド辞書圧縮を解説。
実世界の問題解決をコンピュータを用いて行う考え方を、ビン詰め問題、巡回セールスマン問題を例に解説。
第10回「アルゴリズムBTicTacToe」
私が子供の時には「まるぺけ」と呼んでいた、どうも世界中の子供達が砂場で遊んでいるらしい「TicTacToe」を、いろんな角度で切ってみる。
第11回「ゲームプログラミング超入門」
簡単なテキストアドベンチャーの作成の実際。
第12回「プログラミングTips」
連載中に取り上げられなかった話題を中心に、より実際的なプログラミングに必要なことを考えてみる。前回の演習問題の答え
前回の演習問題は、いずれもソースファイルを記述する問題でしたので、今月号CD-ROMに収録されている、答えとなるソースファイルを見て下さい。今回出てきた標準関数一覧
sizeof(型名 | 実体名)
関数ではなく単項演算子扱いであるsizeofは、与えられた型や、変数、配列、構造体の実体が、メモリ上で何バイトの大きさであるかを求めます。
char ch;
である時、
sizeof(char);
sizeof(ch);
ともに1の値を持ちます。
今回の例題では、メモリ上での占有領域を求めるために使用したり、動的にメモリを確保する場合に要求するメモリ容量を算出するのに使用したりしています。
#include <stdlib.h>
void *malloc(size_t size);
malloc()は動的にメモリを確保します。確保されたメモリは、グローバル変数などと同等の扱いになりますが、変数などの宣言とは異なり、名前が付かないので、ポインタ型変数にポインタを保存しておいて利用します。
戻り値となる、確保されるメモリ領域を示すポインタは、void *型と呼ばれる、型が不定のポインタ値となるので、通常はキャストという手法で、利用したい型に合わせるようにします。List.5では、int *型に合わせるようにしています。
引数の型であるsize_t型は、unsignedである整数型で、処理系によって異なります。大抵の場合は、4バイト以上の長さがあります。この型で表現できる大きさまで、メモリを確保できるということですが、4バイト符号なしでも4GBまで表現できます。普通の人の使っているコンピュータには、まだそこまでメモリが載っていないと思います……
#include <stdlib.h>
void free(void *ptr);
malloc()によって確保したメモリを解放します。通常の処理系では、free()を用いずとも、プログラム終了時に解放が行われます。しかし、長時間動作し続けるプログラムなどの内部で、解放されないメモリの無駄遣いが蓄積していくと、そのうちプログラムは動作を続けられなくなります。正しくfree()を使うように心がけましょう。
戻り値のvoid型は、値が存在しないことを示す特別な型です。関数が値を返さない場合や、関数に引数がいらない場合に使用されます。かなり古いCコンパイラでは、void型の利用できない可能性がありますが、恐らくそのような特殊な環境を持っておられる方は初心者じゃないので、ここで注意を喚起するまでもないでしょう(^^;