2022年10月03日くいなちゃん


6さいからのプログラミング」第12話では、これまでに紹介しきれなかった知っておくべき常識を補足します!

1構造体

第11話までの内容で、ほぼどんなプログラムでも作ることができますが、自分で型が作成できると、値を効率良く管理できて便利です。
例えば「構造体こうぞうたい」を使うと、複数の型を合わせて1つの新しい型を作ることができます。 第9話のときに紹介したC++のクラスに似ていますが、構造体には変数しか持たせることができません。
C言語で構造体を作るには、中身となる変数を「struct 型名{};」で囲むように書きます(図1-1)。
  1. #include <string.h>
  2.  
  3. struct person
  4. {
  5.   char name[32];
  6.   int age;
  7. };
  8.  
  9. int main(void)
  10. {
  11.   struct person x;
  12.   strcpy(x.name, "Kuina-chan");
  13.   x.age = 6;
  14.   return 0;
  15. }
図1-1: struct.c
この例では3~7行目で「struct person」という名前の構造体を作成し、中に文字列型の変数「name」とint型の変数「age」を持たせています。 この構造体「struct person」は型として扱え、11行目のようにstruct person型の変数xを作ることができます。
またC++のクラスと同様、作った変数xに対し「x.name」「x.age」と書くことで、構造体内の変数にアクセスできます。 構造体内の「.name」や「.age」のことを、その構造体の「メンバ」といいます。

2ポインタ渡し

C言語の構造体は、代入や関数の引数渡しのときに全メンバがコピーされるという仕様になっています。 例えば、struct person型の変数xとyがあったとき、「y=x;」と書くとxのメンバの値がすべてyにコピーされます。 これはたくさんの値をまとめて扱えるので便利ですが、関数の引数渡しのたびにコピーが起こるとプログラムの動作が遅くなります。 そこで引数には構造体そのものではなく、構造体のアドレスを渡すことがよく行われます(図2-1)。
  1. #include <stdio.h>
  2.  
  3. struct person
  4. {
  5.   char name[32];
  6.   int age;
  7. };
  8.  
  9. void f(struct person a)
  10. {
  11.   printf("%d\n", a.age);
  12. }
  13.  
  14. void g(struct person* a)
  15. {
  16.   printf("%d\n", a->age);
  17. }
  18.  
  19. int main(void)
  20. {
  21.   struct person x;
  22.   x.age = 6;
  23.   f(x);
  24.   g(&x);
  25.   return 0;
  26. }
図2-1: struct2.c
9行目の関数fは構造体の引数をそのまま渡す「値渡あたいわた」、14行目の関数gはアドレスを渡す「ポインタわた」になっています。 23行目の「f(x);」ではxの全メンバの値がコピーされるため効率的ではありません。 これに対し24行目の「g(&x);」ではxのアドレス、つまり数値1つ分だけが渡るため効率的です。
16行目の「a->age」は「(*a).age」と全く同じ意味です。 第7話で「a[2]」は「*(a+2)」と同じだと言いましたが、それと同様で人間にとって分かりやすい代替表現となっています。 この「->」は「アロー演算子」と呼ばれ、ポインタのアドレス先の構造体のメンバを指すのに使われます。
ここで注意が必要なのは、関数fの場合は値がコピーされているので、関数f内で引数aの値を書き換えても元の構造体xには影響ありませんが、関数gの場合は構造体のアドレスが渡るため、関数g内で引数aのアドレス先の値を書き換えると元の構造体xも変更されることになります。 このことは構造体だけでなくあらゆる型に言えます。 例えば、引数に渡した2つの変数の値を入れ替える関数を作りたい場合には、それぞれの変数の値を書き換える必要がありますので、値渡しではなくポインタ渡しにする必要があります。

3その他の言語の構造体

C言語以外の構造体も見ていきましょう。

3.1C#



C#の構造体は、C言語に似ています(図3-1)。
  1. class HelloWorld
  2. {
  3.   struct person
  4.   {
  5.     public string name;
  6.     public int age;
  7.   }
  8.  
  9.   static void Main(string[] args)
  10.   {
  11.     person x;
  12.     x.name = "Kuina-chan";
  13.     x.age = 6;
  14.   }
  15. }
図3-1: struct.cs
構造体のメンバに「public」を付けたり、構造体の閉じ括弧「}」の後に「;」が不要だったりといくつか差異はありますが、基本的にはC言語と同様です。

3.2Java



Javaには構造体は存在せず、「class」を使って実現することになります。

3.3PHP



PHPにも構造体は存在しません。 その代わりに第7話で紹介した連想配列を使って代用できます(図3-2)。
  1. <?php
  2.   $x = array();
  3.   $x['name'] = 'Kuina-chan';
  4.   $x['age'] = 6;
  5. ?>
図3-2: struct.php

4列挙型

さて、構造体以外で型を作る方法に「列挙型れっきょがた」があります。 これはいくつかの値の中から1つが選ばれるという型です。 例えば、「変数cは、red、green、blueのいずれかの値を持つ」というような型を作りたい場合に使用します。 これをC言語で書くと、図4-1のようになります。
  1. enum color
  2. {
  3.   red,
  4.   green,
  5.   blue
  6. };
  7.  
  8. int main(void)
  9. {
  10.   enum color c;
  11.   c = red;
  12.   if (c == red)
  13.   {
  14.     c = blue;
  15.   }
  16.   return 0;
  17. }
図4-1: enum.c
1行目でenum color型を作成し、10行目でenum color型の変数cを作り、11行目でこの変数cに値redを入れています。
12行目のように列挙型の値は、==演算子や!=演算子での比較もできます。 列挙型の値には0から順に整数値が割り当てられており、メインメモリ上ではこの整数値で扱われます。 例えばこの例ではメインメモリ上でredは0、greenは1、blueは2になります。 「(int)red」のようにキャストすると、この整数値をint型の数値として使用できます。
ちなみにC#で書くと図4-2のようになります。
  1. class Enum
  2. {
  3.   enum color
  4.   {
  5.     red,
  6.     green,
  7.     blue
  8.   }
  9.  
  10.   static void Main(string[] args)
  11.   {
  12.     color c;
  13.     c = color.red;
  14.     if (c == color.red)
  15.     {
  16.       c = color.blue;
  17.     }
  18.   }
  19. }
図4-2: enum.cs
基本的にはC言語と同じです。

5定義の位置

C言語の場合、構造体や列挙型を書く位置は、その変数が作られる位置よりも上でなければなりません。 第4話の関数の紹介のときにも説明しましたが、コンパイルがソースコードの上から下に向かって行われる関係上、使われる位置よりも上に定義が書かれていたほうが都合が良いためです。 もしも使われる位置よりも下で定義したい場合は、関数の場合と同様、上にプロトタイプ宣言を書きます(図5-1)。
  1. struct person;
  2.  
  3. int main(void)
  4. {
  5.   struct person x;
  6.   x.age = 6;
  7.   return 0;
  8. }
  9.  
  10. struct person
  11. {
  12.   char name[32];
  13.   int age;
  14. };
図5-1: struct3.c
1行目の「struct person;」が、プロトタイプ宣言です。 構造体や列挙型の「{...};」を「;」に置換したものを書きます。 これで10行目に定義された構造体を、それより上の5行目で参照することができます。

6識別子の命名

さて、ここからは細かな機能やルールを解説していきます。
多くのプログラミング言語では大文字「ABC...」と小文字「abc...」とを区別します。 例えば変数「X」と変数「x」は別の変数になります。
また多くの言語では、変数名や関数名などに、アルファベット「ABC…Zabc…z」と数字「012…9」とアンダースコア「_」を使用できますが、先頭に数字が付けられないなどの決まりがあります。
全角文字が変数名などに使用できる言語もありますが、入力が大変だったりソースコードが読みにくくなることもあるため、通常は半角文字が使われます。

7改行とスペース

多くの言語では、図7-1のようにソースコード上で比較的自由に改行したりスペースを挟んだりできます。
  1. #include      <stdio.h>
  2.   int
  3. main(
  4.   void){
  5.   printf
  6. ("Hello, world!\n"
  7. );return
  8.  
  9. 0
  10.   ;}
図7-1: line_feed_and_space.c
ただし改行はスペース1つ分として解釈されることが多く、「if」の「i」と「f」の間で改行することはできません。
もちろん無秩序に書くと読みづらいので、開発チーム内で定めたルールなどに従って、分かりやすい位置で改行しましょう。

8コメント

ソースコード中に、日本語や英語などで好きなメッセージを書き込むことができます。 これは「コメント」と呼ばれ、プログラムの動作には影響しません。 C言語では「/*」と「*/」で囲んだ部分がコメントとして、コンパイル時に除外されます(図8-1)。
  1. #include <stdio.h>
  2.  
  3. /* メイン関数 */
  4. int main(void)
  5. {
  6.   /*
  7.   printf("Hello, world!\n");
  8.   */
  9.   return 0;
  10. }
図8-1: comment.c
7行目のように、プログラムの一部の動作を無効化したいときにもコメントが役立ちます。
古すぎないC言語の規格やC++やC#では、この「/*」「*/」形式のコメントの他に「//」形式のコメントも書けます。 これは「//」を書いた箇所からその行の終わりまでをコメント扱いします。 図8-2はC#での例です。
  1. class HelloWorld
  2. {
  3.   // メイン関数
  4.   static void Main(string[] args)
  5.   {
  6.     // System.Console.WriteLine("Hello, world!");
  7.   }
  8. }
図8-2: comment.cs

916進数と8進数

ほとんどの言語では16進数の数値がソースコード中に書けます。 C言語やJavaなどで16進数を書くには、頭に「0x」を付けます。 例えばA0は「0xA0」です。 またあまり使いませんが、C言語では8進数の場合は頭に「0」を付けて、「037」のように書けます。 注意が必要なのは、10進数のつもりで「0123」と書くと8進数になってしまう点です。

10指数表記

多くの言語では「」の形の小数が書けます。 これを「指数表記しすうひょうき」といい、非常に大きな数や小さな数を書くときに使われます。 例えばC言語やJavaなどでは「」は「6.02e23」と書け、「」は「1e-9」のように書けます。 この「e」は指数部を意味する「exponent」に由来します。

11初期化

C言語やJavaなどで「int a; int b; int c;」のように同じ型の変数を複数作成するときは、「int a, b, c;」のようにまとめて作成できます。
変数は、作成と同時に値を代入することもできます。 例えばC言語で「int n;」を作成して「n=2;」と代入する処理は、「int n=2;」のようにまとめることができます。 これらはJava、C#など、多くの言語で同様です。
またC言語の配列は、配列作成時にまとめて全要素に代入することができます(図11-1)。
  1. int main(void)
  2. {
  3.   char a[4] = { 'A', 'B', 'C', '\0' };
  4.   char b[] = "ABC";
  5.   return 0;
  6. }
図11-1: initialization.c
このとき、配列a、bは両方とも「'A'」「'B'」「'C'」「'\0'」の4要素となります。 このように「{}」とカンマ区切りで各要素の値が指定でき、char型の配列に限っては4行目のように{}の代わりに文字列を指定することもできます。 4行目のbのように要素数を省略すると、指定した値がちょうど収まる要素数(この場合は4)に自動的になります。
C言語では構造体も同様に、構造体作成時にまとめて代入できます(図11-2)。
  1. struct person
  2. {
  3.   char name[32];
  4.   int age;
  5.   int array[3];
  6. };
  7.  
  8. int main(void)
  9. {
  10.   struct person x = { "Kuina-chan", 6, { 1, 2, 3 } };
  11.   return 0;
  12. }
図11-2: initialization2.c
メンバの宣言順に沿って値を代入します。 ちなみに作成しただけで一度も代入していない変数には、0ではなく、以前その領域が使われていた際のメインメモリ上のデータの残骸が入っていることがありますので、その場合変数を作成した後には代入を忘れないようにしましょう。

12再帰呼び出し

関数がその関数自身を呼び出すことを「再帰さいき」といいます。
再帰は数学の漸化式と相性が良く、例えば「」という数学の漸化式はC言語で図12-1のように書けます。
  1. #include <stdio.h>
  2.  
  3. int a(int n)
  4. {
  5.   if (n == 0)
  6.   {
  7.     return 3;
  8.   }
  9.   else
  10.   {
  11.     return a(n - 1) + 2;
  12.   }
  13. }
  14.  
  15. int main(void)
  16. {
  17.   printf("%d\n", a(10));
  18.   return 0;
  19. }
図12-1: recursion.c
3~13行目で、「」がそのまま書けていることが何となく分かると思います。
このプログラムの関数aは、a(0)が呼ばれた場合は、7行目により3を返します。 a(1)が呼ばれた場合は、11行目により「a(1-1)+2」を返そうとしますが、ここで自分自身の関数a(0)を呼び出すことになります。 a(0)は3を返すため、「a(1-1)+2」は「3+2」、つまり「5」です。
同様にa(10)が呼ばれた場合を追ってみると、11行目によりa(9)、a(8)、a(7)…と順に呼ばれ、a(0)にたどり着いて「3」を返したのち、呼び出された順序を逆にさかのぼって値を返していきます。
再帰は慣れるまではややこしいですが、データ探索の処理などで多用します。
またHaskellなどの関数型言語では、反復処理が基本的に再帰で書かれるなど、再帰はよく使われます。 先ほどの「」という漸化式をHaskellで書くと、図12-2のようになります。
  1. a 0 = 3
  2. a n = a (n - 1) + 2
  3. main = print $ a 10
図12-2: recursion.hs
1~2行目に漸化式がほぼそのまま書かれており、3行目でa(10)の値を画面に表示する旨を書いています。 C言語に比べてシンプルなソースコードになっています。

13ヒープ領域

ヒープ領域とは、プログラムの実行中に自由に確保や解放ができるメインメモリの領域でした。 C言語でこの領域を扱うには、確保に「mallocマロック」関数、解放に「freeフリー」関数を使います。 これらの関数は、stdlib.h内に存在します(図13-1)。
  1. #include <stdlib.h>
  2.  
  3. int main(void)
  4. {
  5.   int* p;
  6.   p = malloc(sizeof(int) * 3);
  7.   p[0] = 100;
  8.   p[1] = 200;
  9.   p[2] = 300;
  10.   free(p);
  11.   return 0;
  12. }
図13-1: heap.c
malloc関数は、引数に渡したサイズ分をヒープ領域から確保し、そのアドレスを返します。 mallocは「Memory ALLOCation」の略です。
6行目の「sizeofサイズオブ」演算子は、型のサイズをバイト単位で返す演算子です。 「sizeof(int)」と書くと、int型変数1つ分のサイズが返ります。 6行目では「sizeof(int)*3」としているので、丁度「int p[3];」の配列に相当する領域が確保されます。 よって7~9行目のように読み書きできます。
最後に10行目の「free(p);」で、ヒープから確保した領域を解放しています。 これを書かないと、いつまでも解放されない領域がメインメモリに残り続ける問題「メモリリーク」が発生します。

14関数ポインタ

C言語には、関数のアドレスを入れられるポインタが存在します。 これを「関数かんすうポインタ」といいます。 関数の名前だけを書くと、その関数が静的領域に配置されたときのアドレスとなります(図14-1)。
  1. #include <stdio.h>
  2. #include <math.h>
  3.  
  4. int main(void)
  5. {
  6.   double(*p)(double);
  7.   p = sin;
  8.   printf("%f\n", p(0.0));
  9.   return 0;
  10. }
図14-1: function_pointer.c
7行目に「sin」とだけ書いていますが、こうするとsin関数のアドレスが返り、ポインタpに入ります。 sin関数は「double sin(double x){~}」として、math.h内で作られていると以前説明しましたが、このsin関数のアドレスを入れる関数ポインタの型は「double(*)(double)」型となります。 そしてこの型のポインタをpという名前で作成するには、6行目のように「double(*p)(double);」と書きます。
関数ポインタに関数のアドレスを入れたものは、その関数と同様に扱えます。 8行目に「p(0.0)」と書かれていますが、これは元のsin関数を「sin(0.0)」と呼び出すことと等しいです。
関数ポインタの使い道としては、例えば関数ポインタを関数の引数にすることで、「渡された関数を10回呼び出す関数」のような関数が作れます(図14-2)。
  1. #include <stdio.h>
  2. #include <math.h>
  3.  
  4. void call_ten_times(double(*p)(double))
  5. {
  6.   int i;
  7.   for (i = 0; i < 10; i++)
  8.   {
  9.     printf("%f\n", p((double)i));
  10.   }
  11. }
  12.  
  13. int main(void)
  14. {
  15.   call_ten_times(sin);
  16.   return 0;
  17. }
図14-2: call_ten_times.c
この例では、call_ten_times関数の引数pに渡された関数が10回呼び出されます。 7行目のfor文でiを0~9まで数えながら10回繰り返し、9行目でiの値を関数ポインタpに渡して結果を画面に表示しています。 15行目でこのcall_ten_times関数にsin関数を渡していますので、このプログラムはsin(0.0)、sin(1.0)、sin(2.0)、…、sin(9.0)の結果を表示します。
今回は、今までに紹介しきれなかった細かな機能を補足しました。 次回は、基本編の最終回。 締めくくりとして、C言語でゲームを作ります!
1664793256jaf