第4回 ポインタと構造体
構造体とは
前回まで説明した配列や,通常の変数を利用すれば,大抵のプログラムを記述することができるが,プログラムを分かり易く記述するために,データ構造を整理して記述するための機能がC言語には用意されている.それが構造体である.
構造体を使えば,関係している変数を一つにパックすることができたり,またデータ構造を構築するための要素を宣言することができる.複雑なデータ構造,例えば構造体を並べた配列や,次に説明するポインターを使って構造体を要素とする木構造やリスト構造を構築することができるようになる.
構造体の宣言
例えば,時刻を格納するためのデータ構造を考えよう.「04:37:46」という時刻を変数に記憶させるためには,そのまま「文字列」として記憶することもできるが,文字列では計算することができない.そこで,時,分,秒に分解して整数型の変数を宣言することになる.
int hour, minute, second;
この3つの変数は,組合わされて一つの時刻を表現することになる.もちろん,これでも時刻を扱うプログラムを十分書くことができる.しかし,時刻の内容を変更するようなときに,必ず3つの変数全部を更新しないといけないなど,使い方に注意が必要となる.また,時刻の配列を作成したいときには,次のようにそれぞれ別の配列として用意することになり,例えば4番の時刻を更新する場合には,必ず同じ番号の3つの配列変数を更新する必要があるなど,さらに使い方に制約が必要になる.
int hour[10], minute[10], second[10]; hour[4] = 04; minute[4] = 37; second[4] = 46;
これらを分かりやすくするために,次のように時刻を表す3つの変数を,一つの構造体としてパックする.
typedef struct { int hour; //時 int minute; //分 int second; //秒 } TIME; //新しい構造体型である「TIME」の宣言 TIME t; //TIME型の変数tを宣言
キーワードtypedefは,すでに存在している型に別名をつけるためのもので,この例では,typedefに続いている構造体宣言に,新しい型名としてTIMEという名前をつけて宣言している.構造体宣言は,structキーワードに続く{}の中に,パックしたい変数宣言を並べて記述したものである.構造体の中で宣言された変数をメンバ変数と呼ぶ.
typedefでは新しい型だけを宣言したこととなり,その次に,この新しい型(TIME型)の変数tを宣言する.
構造体型変数の使い方
構造体型の変数も,一つの変数であることに代わりはないため,同じ構造体型の変数ならばそのまま代入ができる.
TIME t1, t2; t1 = t2; //同じTIME型の変数なら代入可能
ただし,別の構造体型の変数はもちろん,整数や小数など別の型の値や変数を代入することはできない.
TIME t; //TIME型の変数 DATE d; //別の構造体DATE型の変数 t = d; //別の構造体なのでエラー t = 1; //整数なども別の型なのでエラー
構造体変数の使い方は,構造体の中に存在するメンバ変数ひとつひとつに値を代入したり,値を読み出すことが基本となる.例えばTIME型のtは,プログラム中では「t.メンバ変数」のように「.」を使用してtの中のメンバ変数アクセスすることができ,自由に読み書きできる.
TIME t1,t2; t1.hour = 04; t1.minute = 37; t1.second = 46; t2.hour = 17; t2.minute = 14; t2.second = 53;
また,構造体の配列もそのまま宣言することができる.
TIME ta[10]; t[3].hour = 04; t[3].minute = 37; t[3].second = 46; t[6].hour = 14; t[6].minute = 7; t[6].second = 6;
ポインター変数とは
C言語では,メモリへの「参照」を表現するためにポインター変数を使用する.
ポインター変数は,通常の変数とは異なり,整数や小数・文字などのデータを格納するものではない.ポインター変数は,別の変数の「場所」(通常はメモリのアドレス)を格納するものである.わかりやすく言い換えると,別の変数が「どこにあるか」を指し示すための変数である.道路における標識(案内板)や,Windowsにおけるショートカットアイコンのように,目的の「土地」や「ファイル」そのものではなく,それがどこにあるのかを示すための「ポインター」(方向指示)を意味している.
ポインター変数を利用すると,変数Aと変数Bをプログラム実行中に動的に切り替えたり,必要に応じて変数の数を後から増やせたり,配列や構造体と組み合わせて複雑なデータ構造を表現することができたりする.
このポインター変数は,C言語登場当時他のプログラミング言語には存在しなかったもので,従来は機械語でなければ記述できないようなハードウェアに直結するような部分のプログラムすら記述可能とした非常に強力な手段であった.このポインター変数のおかげで,C言語は,システムプログラムや組み込み系のプログラムでよく利用されることとなった.C言語がこれだけ普及したのも,強力なポインター変数を用いた記述能力が一つの要因と考えられる.
もっとも,ポインター変数は理解が難しいといわれ,プログラムミスを招きやすい.あまりに強力である半面,間違えばプログラムを簡単に暴走させることにもつながり,危険でもある.これらから,現代的なプログラミング言語からは排除されつつある.
ポインター変数の宣言と使用例
ポインタ変数の宣言と使用例は次のようなものである.
int *p; //ポインタ変数 int i; //普通の変数 p = &i; //ポインタ変数へ,普通の変数のアドレスを代入 (*p) = 5; //ポインタ変数
ポインタ変数は,宣言された後で値を正しく設定されないと使用することができない.宣言されただけの状態ではどこも「指していない」状態だからである.ショートカットアイコンのように,適当なファイルを指すように設定されて始めて使用できる.同様に,ポインタ変数には適当な変数の「場所(アドレス)」を設定する必要がある.変数のアドレスは,アドレス演算子「&」を使って適当な変数のアドレスを取得して,それを代入するようにして
ポインタ変数に対して「*」はそのポインタが指し示す先を「たぐる」演算を示す.これを「間接演算」と呼ぶ.これは掛け算の「*」とはまったく異なる.間接演算を行った演算結果は,たどったポインター先そのものと等価に扱われることとなり,ポインター先の変数の(ポインターを使って「間接」的に)読み書きをすることができるようになる.これはショートカットアイコンをダブルクリックするとリンク先のプログラムを実行できるのと同じような感覚で考えられる.
int *p; p = &y; // x = 1 * 2; //掛け算.演算子*の前後に数字や変数がある.(二項演算と呼ぶ) x = *p; //間接演算.演算子*の前には数字や変数がない.*に続くPの指している先(この例ではy)をたぐる.
ポインター変数は,intやdoubleといった通常の型の変数に対するだけでなく,当然構造体変数や配列変数にも使用することができる.
TIME t[2]; TIME *p1, *p2; p1 = &t[0]; //TIME型の変数のアドレスを代入すれば p2 = &t[1]; (*p1).hour = 04; //*を付けて構造体にアクセスすることができる. (*p2).hour = 06;
特に,構造体へのポインタ変数の場合には,いちいち(*変数名).メンバ名と書くことが大変なので,変数名->メンバ名と略記できることになっている.
p->hour = 04; //(*p1).hourと同じ p->hour = 06; //(*p2).hourと同じ
ポインタと配列
ポインター変数は,間接演算を使ってポインター先を操作するだけではなく,ポインターの内容,すなわちアドレスそのものを利用する場合もある.例えばポインター変数に対して加算と減算ができる.ちなみに乗算と除算はできない.
ポインター変数に対する加算減算は,アドレス演算とも呼ばれ,ポインターの指す位置を次の要素へずらすために行われる.そのため,加算と減算は,そのポインターが指している対象のサイズ単位で行われる.つまり,次の例のように配列と等価な処理を記述することができる.
int a[10]; int *p; p = &a[0]; *(p+3) = 4; //3つ分次の要素へポインターをずらす,つまりa[3]と等価
ここで注意することは,ポインターへの加算(アドレス演算)が単純にメモリのアドレスに対する加算ではない点である.単純にアドレスに+3をすればアドレスの値が3増えるだけとなる.しかしポインター変数への加算ではそうはならない.+3すると「int」の変数3つ分のサイズを増加させる.すなわちint(64bit=8byte)の3つ分として24byte増加される計算が,実際には行われる.
このアドレス演算*(p+3)は,p[3]とまったく等価と考えられ,実際にプログラムとしてポインタ変数に対して[]をつけて表記しても,コンパイラは正しく処理してくれる.
int a[10]; int *p; p = &a[0]; p[3] = 4; //a[3]と等価
メモリの動的管理
ポインター変数を使えば,動的なメモリ管理が可能となる.動的なメモリ管理とは,プログラム実行中に計算などに必要なメモリを後から必要なだけ確保して使用するために必要な機能である.動的なメモリ管理がなければ,プログラムでは最初に必要なメモリを例えば配列の形で用意しておかないといけなくなる.
例えば,電話帳プログラムを考えた場合,電話番号を何人分扱うかどうかは,データ次第である.動的なメモリ管理ができない場合には,「最大1000人分」と勝手に決めつけて,TEL[1000]などのように配列を確保して使用することになる.このプログラムではたまたま1000人以上の登録が必要になった時,処理することができない.また,電話帳に10人しか登録しないような時には,メモリが無駄になる.そこで,最初1人分だけのメモリを確保しておき,後から必要なだけ動的にメモリを確保できれば都合がよい.これを行うことがメモリの動的管理である.
メモリの動的管理のために,malloc()ライブラリ関数が用意されている.次のように使用する.
#include <stdlib.h> //mallocを使用するときには最初に必要 .....(略) int *p; .....(略) p = malloc( sizeof(int)*1000 ); .....(略) free(p);
最初のstdlib.hのインクルードは,mallocを使用するときには必要な宣言である.そして,動的に確保するメモリエリアの記憶するためのポインタ変数を用意しておく.その変数へmallocで必要な記憶サイズ分だけメモリを確保して,その確保したメモリの先頭アドレスをpに代入する.最後に,そのメモリエリアが必要なくなったら(使い終わったら)freeでメモリを返却する.
sizeof演算子は,型名や変数名に対してそれのメモリ上の大きさを計算する演算子である.この例ではintの1000個分のサイズを計算している.それを使ってmallocに必要なメモリサイズを伝えている.
配列を確保するときには,専用のcallocを使用することもある.callocは引数を二つとり,一つが要素のサイズ,一つが個数である.mallocとcallocの違いは,個数の設定だけでなく,ひとつひとつの要素を0値で初期化するかしないかである.mallocでは確保されたメモリ領域に最初に何の値が入っているか不明であるが,callocで確保したメモリはすべて0となっていることが保証されている.
p = calloc( sizeof(int),1000 );