はじめに
初級編の前編と後編で、基礎的な文法についてまとめました。
それらを前提とした内容にするため、まだ見てない方はぜひチェックしてみてください☺️
C言語 〜中級〜
中級編も、C言語の基本的な内容ではありますが、より本格的な使い方になっています。
一緒に見ていきましょう👍
構造体と共用体
構造体
たとえば、ゲームのキャラクターには共通のステータスがありますね。
HP、MP、攻撃力、防御力、などなど。。。
これらはバラバラの変数で管理するより、「ステータス」としてひとまとめにしておいたほうが便利です。
struct 構造体名
{
メンバー変数1;
メンバー変数2;
・・・
メンバー変数n;
}
メンバー変数は1つ以上から何個でも定義可能で、それぞれ異なる型でも問題ありません。
#include <stdio.h>
struct STATUS
{
char name[10]; //名前
int hp; //ヒットポイント
int mp; //マジックポイント
int attack; //攻撃力
int defence; //防御力
};
int main(void)
{
//初期ステータスを決める
struct STATUS character = {“eight”, 100, 10, 3, 1};
//ステータスを表示する
printf(“name = %s\n”, character.name);
printf(“hp = %d\n”, character.hp);
printf(“mp = %d\n”, character.mp);
printf(“attack = %d\n”, character.attack);
printf(“defence = %d\n”, character.defence);
return 0;
}
出力はこうなります。
name = eight
hp = 100
mp = 10
attack = 3
defence = 1
構造体は、変数の型として定義します。
構造体変数characterの宣言時、{ }で初期値を与えていますが、これは宣言時のみ可能です。
宣言時以外でアクセスする場合は、
構造体変数名.メンバー変数名
という形式で指定します。
キャラクターごとに構造体変数を作成すれば、キャラクターごとのステータスを管理しやすくなります。
共用体
構造体と似たような書き方ですが、こちらはメンバー変数が同じメモリ領域を共有します。
構造体では全てのメンバーに対して自由にアクセスできましたが、共用体では最後に代入を行ったメンバーのみアクセス可能です。
#include <stdio.h>
union STATUS
{
char name[10]; //名前
int hp; //ヒットポイント
int mp; //マジックポイント
};
int main(void)
{
//初期ステータスを決める(name)
union STATUS character = {“eight”};
//ステータスを表示する
printf(“name = %s\n”, character.name);
printf(“hp = %d\n”, character.hp);
printf(“mp = %d\n\n”, character.mp);
//hpを更新
character.hp = 100;
//ステータスを表示する
printf(“name = %s\n”, character.name);
printf(“hp = %d\n”, character.hp);
printf(“mp = %d\n\n”, character.mp);
return 0;
}
出力はこのようになります。
name = eight
hp = 1751607653
mp = 1751607653name = d
hp = 100
mp = 100
共用体変数の宣言時、メンバー変数を指定していません。
共用体宣言時の初期化では、自動的にメンバー内で最初に定義されているメンバー変数に対するアクセスとなります。上記の場合はnameです。
nameを初期化したあと、hpやmpを更新せずに値を表示すると、おかしな値になっていることがわかります。この時点でアクセス可能なのは、最後に更新したnameだけなので、他のメンバーは正しい値になりません。
その後、hpを更新してから全メンバー変数の値を表示しました。
hpは正しいですが、更新していないmpまで100になっています。これは、全てのメンバーが同じメモリ領域を参照しているからです。
列挙型
たとえばイベントのID(処理番号)のように、値自体に意味はないけど、それぞれ別々の状態として区別したい場合があります。
そんなときは列挙型を使うことで、イベント名を列挙するだけで、値を自動的に1ずつ増加してくれます。
#include <stdio.h>
enum EVENT_NO
{
EVENT1,
EVENT2,
EVENT3
};
int main(void)
{
enum EVENT_NO e1, e2, e3;
e1 = EVENT1;
e2 = EVENT2;
e3 = EVENT3;
printf(“e1 = %d\n”, e1);
printf(“e2 = %d\n”, e2);
printf(“e3 = %d\n\n”, e3);
return 0;
}
実行結果は以下のようになります。
e1 = 0
e2 = 1
e3 = 2
上記例では、EVENT_NOとして3つ定義し、それぞれe1、e2、e3に代入して画面表示しました。
各EVENTには値を設定していませんが、自動的に0から割り当てられていることがわかります。
enum EVENT_NO
{
EVENT1 = 10,
EVENT2,
EVENT3,
EVENT4 = 100,
EVENT5
};
このように値を設定することも可能です。
この場合、EVENT2は11となり、EVENT5は101となります。
ファイル操作
ファイルからデータを読み取ったり、書き込むことができます。
プログラムの動作中は、データはメモリ上に持っておくことができますが、プログラム終了したり電源を切ると、メモリのデータは消えてしまいます。
ゲームの進行状況など、消えてしまうと困るデータは、ファイルとして書き出しておくと良いでしょう。
以下のサンプルコードでは、あらかじめ同じフォルダにsampletxt.txtという名前のファイルを用意しておきます。
sampletxt.txt
abcde
ファイルを開くにはfopen関数を使用します。
第2引数にオープンモードを指定するのですが、以下のような種類があります。
書き込みモード(w)が要注意で、これはファイルが存在する場合でも、最初に中身を空にしてしまいます。
現在のファイル内容を維持しつつ書き込みたい場合、 aまたはa+を使用しましょう。
モード | 機能説明 | ファイルが存在しない場合 |
r | 読み取り専用でオープンする | オープンエラー |
w | 既存ファイルは中身を空にして 書き込み専用でオープンする | 新規作成 |
a | 既存ファイルは中身を維持して 書き込み専用でオープンする | 新規作成 |
r+ | 読み書き可でオープンする | オープンエラー |
w+ | 既存ファイルは中身を空にして 読み書き可でオープンする | 新規作成 |
a+ | 既存ファイルは中身を維持して 読み書き可でオープンする | 新規作成 |
#include <stdio.h>
#define DATASIZE 100
int main(void)
{
FILE *fp;
char readdata[DATASIZE];
//ファイルオープン(ファイル末尾に書き込み)
fp = fopen(“sampletxt.txt”, “a+”);
if (fp == NULL)
{
printf(“ファイルオープン失敗\n”);
fclose(fp);
}
//1行書き込み
fprintf(fp, “fghij”);
fclose(fp);
//ファイルオープン(読み出し)
fp = fopen(“sampletxt.txt”, “r”);
if (fp == NULL)
{
printf(“ファイルオープン失敗\n”);
fclose(fp);
}
//1行ずつデータ読み込み
while (fgets(readdata, DATASIZE, fp) != NULL)
{
printf(“%s\n”, readdata);
}
fclose(fp);
return 0;
}
出力結果はこうなります。
abcdefghij
fprintf関数で、ファイルへの文字列書き込みが可能です。
fgets関数で1行読み込みを行います。
fopen関数でファイルを開いたあと、使い終わったらfcloseで閉じるようにしましょう。
そうしないとプログラム上でファイルオープンしたままになり、誰も編集できなくなってしまいます。
ポインタ
C言語学習において屈指の挫折率を誇るという、ポインタです🥺
ポインタは、変数や関数のアドレスを操作する機能です。
まずアドレスの概要について理解を深めましょう。
アドレス
名前の通り、番地(住所)を表します。
メモリ上で動作するプログラムは、全てアドレスが割り当てられています。
たとえば変数aをint a;で宣言したとき、メモリ上のあるアドレスに変数aが割り当てられます。
仮にアドレス1000に割り当てられたとすると、こうなります。
アドレス | 値 | |
・・・ | ・・・ | ・・・ |
変数a | 1000 | 0 |
・・・ | ・・・ | ・・・ |
ここでa=2を実行すると、アドレスはそのままに値が2になります。
アドレス | 値 | |
・・・ | ・・・ | ・・・ |
変数a | 1000 | 2 |
・・・ | ・・・ | ・・・ |
変数だけでなく、関数もアドレスが割り当てられます。
アドレス | 値 | |
・・・ | ・・・ | ・・・ |
変数a | 1000 | 2 |
・・・ | ・・・ | ・・・ |
main() | 2000 | |
・・・ | ・・・ | ・・・ |
アドレスは宣言時に確定し、基本的に一度決まったら変わることはありません。
また、割り当て済みのアドレスは、解放されるまで二重に確保されることはありません。
我々がプログラムで変数aにアクセスするとき、コンピュータは変数に割り当てられたアドレスを見に行って値を読み書きする、という仕組みになっています。
ポインタ
ポインタはアドレスを直接操作できます。
サンプルコードを見てみましょう。
#include <stdio.h>
int main(void)
{
int *addr;
int a = 0;
addr = &a; //addrはaのアドレス参照になる
a = 1;
printf(“%d\n”, *addr);
printf(“%d\n”, a);
return 0;
}
ポインタの宣言時は、変数名の直前に*を付けます。
普通の変数のアドレスを取り出したいときは、変数名の直前に&を付けます。
addr=&a;によって、addrはaのアドレスを参照するようになります。これで、addrは間接的にaの値を参照することができます。値を読むだけでなく、addrを通してaの値を書き換えることも可能です。
上記サンプルコードの出力結果は以下の通りです。
1
1
ポインタとは、Excelで言うとセル参照のようなものです。
変数addr自身は値を持っていませんが、変数aの値を参照することで、間接的に値を取り出すことができます。
ポインタを用いた配列
配列の基礎は初級(後編)で紹介しました。この内容を前提としています。
ポインタ変数は、アドレスを値のように扱い、演算を行うことが可能です。
これを利用することで、配列のような、連続したアドレスを持つ領域を操作できるようになります。
#include <stdio.h>
int main(void)
{
char *addr;
char c[5] = {“abcde”}; //配列cを初期化
addr = c; //addrはcを参照するようになる
for (int i=0; i<5; i++)
{
printf(“%c\n”, *addr);
addr++; //次のアドレス(要素)を参照するためインクリメント
}
return 0;
}
実行結果は以下のようになります。
a
b
c
d
e
addr = c; では、cに&を付けていません。
配列は変数名そのものが先頭アドレスを指しており、[要素番号]は先頭アドレスから何番目、ということを表しているため、このような指定になります。
配列cは、メモリ上で以下のような構成になっています。
配列は、必ず連続したアドレスで確保されます。
addr自身は配列ではありませんが、アドレスを演算することで、要素にアクセスすることができます。
ポインタを用いた関数引数
関数の基礎は初級(後編)で紹介しました。この内容を前提としています。
関数の引数にポインタを利用することで、関数の自由度を上げることができます。
まず、ポインタ無しの関数の動作を確認しましょう。
#include <stdio.h>
int sumval(int n1, int n2, int e1);
int sumval(int n1, int n2, int e1)
{
int ret;
n1 = 3; //引数n1を書き換え
n2 = 5; //引数n2を書き換え
e1 = 123; //引数e1を書き換え
ret = n1 + n2;
return ret;
}
int main(void)
{
int x = 1;
int y = 2;
int result = 0;
int ext = 0;
result = sumval(x, y, ext);
printf(“x=%d, y=%d, result=%d, ext=%d\n”, x, y, result, ext);
return 0;
}
関数sumvalに変数を3つ渡します。それらはsumvalの引数で受け取りますが、値を更新してから演算してreturnします。この場合、もとの変数x、y、extはどうなっているでしょう?
実行結果はこうなります。
x=1, y=2, result=8, ext=0
関数sumvalの中で、引数を3つとも更新しました。n1とn2を更新した上で加算処理しているため、result(戻り値)は更新後の値に基づいた結果になっています。
ただ、引数として与えた変数x、y、extには影響していません。
このような値の受け渡し方法を、「値渡し」と呼びます。
では、関数sumvalにポインタを使用してみます。
#include <stdio.h>
int sumval(int *n1, int *n2, int *e1);
int sumval(int *n1, int *n2, int *e1)
{
int ret;
*n1 = 3; //引数n1を書き換え
*n2 = 5; //引数n2を書き換え
*e1 = 123; //引数e1を書き換え
ret = *n1 + *n2;
return ret;
}
int main(void)
{
int x = 1;
int y = 2;
int result = 0;
int ext = 0;
result = sumval(&x, &y, &ext); //値ではなくアドレスを渡す
printf(“x=%d, y=%d, result=%d, ext=%d\n”, x, y, result, ext);
return 0;
}
関数sumvalの引数3つを、ポインタで受け取るようにしました。
結果はどうなるでしょうか。
x=3, y=5, result=8, ext=123
引数に渡した変数が、sumval内で更新した値に変わっています。
このような値の受け渡し方法を、「参照渡し」と呼びます。
値渡しでは関数の戻り値は1つだけですが、参照渡しなら複数の戻り値を返すことができるのです。
さいごに
中級ということで、少し踏み込んだ機能のお話しでした。
特にポインタは複雑な機能ですが、C言語の最も重要な機能と言えます。
いずれも現場でよく使う機能なので、ぜひ押さえておきたいですね😌
それでは次回、上級編でお会いしましょう。
ありがとうございました😊
コメント