【C言語プログラミング10】ポインタに型は要るか?要るでしょ!
今回はポインタ変数に型が要る理由と構造体のポインタについて書きます。ご存じの通り、変数には型があります。何型の変数を宣言するのかは、宣言した変数にどんな値を入れるつもりなのかによって決めますよね。1バイトで収まる値しか扱わないならchar型で良いし、4バイトならint型やlong型を宣言する必要があります。
そんな中、ポインタ変数にはアドレスが入るので、例えchar型のポインタ変数であっても4バイトになると以前書きました。何型であってもポインタ変数であれば4バイトになるのなら、型なんて関係ないやんけ!と思いますよね。私も最初はそう思っていました。しかし、ポインタに型は要るんです。
なんで型が要るねん
ポインタ変数にはアドレスが入ります。アドレスはメモリ上のある一点を指し示します。前の例え話で言うと、アドレスはロッカーの番号でしたね。アドレスさえ分かれば、メモリ上の一点は特定できますが、その一点から何バイト分を扱うの?という情報が要ります。つまり、ロッカーの大きさが必要になるのです。ロッカーには大きなロッカーもあれば、中くらいのもあれば、小さいのもあります。この大きさは型で決まるのです。
ポインタを一つ進める(隣のロッカーに移る)
ポインタ変数にアドレスが入っていたとします。今回は仮に0番地が入っていたとしましょう。そして、そのアドレスを一つ進めた場合にアドレスがどのように変化するのかを見てみましょう。プログラミングでは何らかの値を1つ増加させることをインクリメントと言い、減少させることをデクリメントと言います。
char型のポインタ変数の場合
char型のポインタはメモリを1バイトずつ扱います。そのため、ポインタをインクリメントすると1バイト(1番地)分進みます。1→2→3・・・という感じ。デクリメントした場合も同様です。
short型のポインタ変数の場合
short型のポインタはメモリを2バイトずつ扱います。そのため、ポインタをインクリメントすると2バイト(2番地)分進みます。2→4→6・・・という感じ。デクリメントも同様です。
int型のポインタ変数の場合
int型のポインタはメモリを4バイトずつ扱います。そのため、ポインタをインクリメントすると4バイト(4番地)分進みます。4→8→12・・・という感じ。デクリメントも同様です。
構造体のポインタ
C言語プログラミング7で構造体について説明しましたが、char型やint型と同様に、構造体の型のポインタ変数も宣言することができます。構造体ポインタ変数には構造体のアドレスが入ります。以下のような構造体の型があったとします。
typedef struct {
int a;
short b;
char c[2];
} ST_SAMPLE;
この構造体の型の構造体を普通に宣言すると上の行のようになり、ポインタにすると下の行のようになります。アスタリスクを付けるだけです。
ST_SAMPLE stSample; // 通常の変数
ST_SAMPLE *pstSample; // ポインタ変数
ポインタ変数に構造体のアドレスを入れる場合は以下の通りです。char型やint型の時と同じですね。
pstSample = &stSample;
構造体配列とポインタ
構造体も配列にできることは既に説明しました。構造体ポインタには構造体配列のアドレスも当然入れることが出来ます。具体的には以下の通りです。
ST_SAMPLE stSample[10];
ST_SAMPLE *pstSample;
pstSample = &stSample[0];
構造体配列の先頭アドレスを関数の引数として渡す方法は実際の開発で頻繁に使います。
構造体ポインタを一つ進める
構造体ポインタをインクリメントするとどうなると思いますか?勘の良い人ならお分かりだと思いますが、構造体のサイズ分進みます。ST_SAMPLE構造体は全部で8バイトになるので、一つ先に進むと8バイト(8番地)分一気に進みます。メモリのイメージは以下の通り。構造体のサイズがもっと大きくなれば、一回のインクリメントで何百バイト、何千バイト進むことになります。
構造体ポインタからメンバ変数にアクセスする
構造体のメンバ変数にデータを入れたり、取り出したりする方法を覚えていますか?こんな感じで、構造体名.メンバ変数名でアクセスします。
stSample.a = 1;
stSample.b = 2;
stSample.c[0] = 3;
stSample.c[1] = 4;
構造体配列の場合は次のようになります。
stSample[0].a = 1;
stSample[0].b = 2;
stSample[0].c[0] = 3;
stSample[0].c[1] = 4;
これが構造体ポインタになると、少し変わります。
pstSample = &stSample;
pstSample->a = 1;
pstSample->b = 2;
pstSample->c[0] = 3;
pstSample->c[1] = 4;
このようにピリオドではなく、"->"アロー演算子という記号でメンバ変数にアクセスします。たったこれだけの違いです。
構造体ポインタを関数に渡すサンプルコード(またクロノトリガーで)
クロノトリガーで敵とバトルをした後、経験値が貰えます。経験値が次のレベルに上がるためのリミットを超えていたらレベルが1上がります。この動きをプログラミングしてみます。処理の流れは以下のようにします。この図のことをフローチャート図と言います。プログラミングにおいて、ソースコードを書く前にどんな処理の流れにするかを考える必要があります。その考えをまとめたり、人に伝えたりする場合にこの図を書きます。
このフローチャートをプログラムにするとざっくりこんな感じになります。
#include <stdio.h>
#include <string.h>
typedef struct {
char level; // レベル
int exp; // 経験値
int next; // 次のレベルアップまでに必要な経験値
} ST_CharStatus;
void Battle(ST_CharStatus *pstCharStatus, int CharNum);
int CheckLevelUp(ST_CharStatus* pstCharStatus);
void main(void)
{
int exp = 0;
int levelUp = 0;
/*--------------------------------------*/
/* パーティを作る */
/*--------------------------------------*/
ST_CharStatus stParty[2];
memset(stParty, 0, sizeof(stParty)); // パーティ構造体を0で初期化
stParty[0].level = 1; // クロノのレベルは1
stParty[1].level = 1; // マールのレベルは1
stParty[0].next = 200; // 次のレベルアップまでに必要な経験値200
stParty[1].next = 250; // 次のレベルアップまでに必要な経験値250
/*--------------------------------------*/
/* バトル関数をコール */
/*--------------------------------------*/
Battle(&stParty[0], 2);
/*--------------------------------------*/
/* レベルアップチェック関数をコール */
/*--------------------------------------*/
levelUp = CheckLevelUp(&stParty[0]);
if (levelUp == 1) {
printf("クロノのレベルが%dになりました。\n", stParty[0].level);
}
levelUp = CheckLevelUp(&stParty[1]);
if (levelUp == 1) {
printf("マールのレベルが%dになりました。\n", stParty[1].level);
}
}
void Battle(ST_CharStatus *pstCharStatus, int CharNum)
{
int counter;
for (counter = 0; counter < CharNum; counter++)
{
pstCharStatus->exp = 200; // 経験値200を獲得
pstCharStatus++; // 次のキャラクターに移動
}
}
int CheckLevelUp(ST_CharStatus* pstCharStatus)
{
int levelUp = 0;
if (pstCharStatus->exp >= pstCharStatus->next) {
pstCharStatus->level++;
levelUp = 1; // 現在の経験値が次のレベルアップに必要な経験値を超えていたらレベルを1上げて、returnで1を返す
}
return levelUp;
}
解説をすると、まずパーティ構造体を宣言して、パーティメンバーのレベルと次のレベルアップに必要な経験値を設定します。クロノは後200で、マールは後250でレベルアップします。クロノの方がレベルアップし易いですね。次に、Battle関数に対してパーティ構造体配列の先頭アドレスとパーティの人数を引数として渡します。Battle関数内ではパーティ人数分ループし、各パーティメンバーに対して200の経験値を与えます。
Battle関数から戻って来ると、パーティ構造体の[0](クロノの情報)をCheckLevelUp関数に渡します。CheckLevelUp関数内では現在の経験値と次のレベルアップに必要な経験値を比較し、超えていればレベルを1アップさせ関数の戻り値を1にします。「1はレベルアップしましたよ」と言う意味です。
CheckLevelUp関数から戻って来ると、main関数ではCheckLevelUp関数の戻り値をチェックし、1だったらprintf関数をコールし、キャラクターのレベルを表示します。これと同じことをマールに対しても実行して全処理が終了します。
され、このプログラムの実行結果は分かりますか?答えは次の通りです。
クロノのレベルが2になりました。
マールのレベルアップには経験値が250必要なので、当然この結果になりますね。
構造体ポインタの利用方法はこんな感じです。実際の開発では、もっと大きな構造体や複雑な構造を扱いますが、結局メモリの何番地から何番地までの話をしてるわけ?ってのが頭の中でイメージ出来ていれば大して難しいことではありません。今回はポインタに型がある理由と構造体ポインタの利用方法についての話でした。