【爆速C言語入門】配列がわかるようになる!【講義】

スポンサーリンク
初心者 爆速C言語入門
スポンサーリンク

はじめに

本講義はプログラミングって何?な初心者の方から、C言語は初めて!というほかの言語を多少経験したことのある人を対象としています。

加えて、メモリなどの説明を多めに取り入れているので、C言語はなんとなく知ってるけどコンピューター上でどういう処理になっているの?という人にもぜひ見てもらいたいと思っています!

前回は大まかにプログラムの読み方を学びました。今回の講義では、配列とは何か、用途と使い方を説明していきたいと思います。

配列とは

配列とは、同じデータ型の一つ以上のデータを一時的に記憶するメモリ上の場所・領域のことです。この時、データ1つ1つのことを要素と言います。

変数は連続で定義しても連続的に領域が確保されるか不定です。

それに対して、配列のデータを記憶する領域は、メモリ上に同じサイズ(要素のデータ型が同じであるため)で連続的に領域が確保されます。

メモリ上で領域を確保する際の、変数と配列の違い

配列は同じ意味の複数の値をひとまとめにして記憶する場合等に利用します。(例:学籍番号)

配列の定義

配列の定義では次のようにして、要素のデータ型と識別子(配列名)、要素数を指定します。

要素のデータ型 識別子[要素数];

例えば、int型のデータを5つ格納する「aiData」という配列を定義する場合は次のようにします。

int aiData[5];  //int型の要素数5の配列

要素の参照

配列の要素を参照する場合は、0番目から始まる要素番号を指定します。要素数がn個の時、指定できる要素番号は0番〜n-1番となるので注意してください。

配列のある要素に値を格納する場合には次のようにします。

識別子[要素番号] = 値;

例えば、配列「aiData」の3番目の要素に「10」を格納する場合、次のようになります。

aiData[3] = 10;

また、要素番号に変数を指定することも可能です。

次のプログラムでは、for文で配列aiDataのそれぞれの要素に要素番号と同じ値を格納して、最後に指定した要素番号の値を出力しています。

#include <stdio.h>

//配列の参照

//main関数
int main() {
  int aiData[5];            //int型,要素数5の配列
  unsigned char uchCount;   //for文用のカウント
  unsigned char uchNum = 3; //出力する要素番号

  //配列aiDataの要素uchCountにuchCountを格納
  for(uchCount = 0; uchCount < 5; uchCount++){
    aiData[uchCount] = uchCount;
  }

  //配列aiDataの要素chNumを出力
  printf("aiData[%d] = %d", uchNum, aiData[uchNum]);

  return 0;
}

実行結果は次の通りです。「chNum」を0〜4まで変えて実行すると、正しく動作していることが確認できます。

aiData[3] = 3

要素番号が0番から始まるメモリ上の理由

要素番号が0番から直感的ではないと感じるかもしれません。しかし、メモリ上での領域の確保の仕方を考えれば容易に説明が付きます。

配列がメモリ上の領域を確保する際、「配列に割り当てる領域の先頭の場所(アドレス)から○Byte(配列のサイズ)を配列の領域として確保する」という情報を記憶しています。

この情報から配列の要素を呼び出すには、「配列の先頭からデータ型○個分先のデータ型の領域」というように呼び出す必要があります。

この呼び出し方で考えると、配列の要素の先頭は「配列の先頭からデータ0個分先のデータ型の領域」であるため要素番号は0から始まるのです。

配列の要素が0から始まることの説明

配列の初期化

配列も変数と同様に、定義しただけでは何の値が格納されているか不定です。

これを解決するため、定義時に初期化することができます。配列の初期化は次のように行います。

要素のデータ型 識別子[要素数(n)] = {[0]の初期値,[1]の初期値,...,[n-1]の初期値};

実際のプログラムでは次のように記述します。

int aiData[4] = {0,1,2,3};  //int型配列の初期化

配列の初期化と、初期値の補完

要素数よりも初期値の数が少ない場合には、先頭の要素から順に初期値が格納され、残りは0で初期化されます。次のプログラムを実行して確認してみましょう。

#include <stdio.h>

//初期化の動作確認

//main関数
int main() {
  int aiData[5] = {1,2};  //int型配列の初期化
  unsigned char uchCount;

  //要素数分ループ処理
  for(uchCount = 0; uchCount < 5; uchCount++){
    //配列aiDataの要素uchCountを出力
    printf("%d\n", aiData[uchCount]);
  }

  return 0;
}

プログラムを実行すると次のように表示され、0で初期化されていることがわかります。

1
2
0
0
0

このため、次のようにすることで、配列の全ての要素に0を格納することができます。

データ型 識別子[要素数] = {};

プログラムでは次のように記述します。

int aiData[5] = {}; //配列の全ての要素に0を格納

殆どのコンパイラが上記のような「{}」の中を空にした記述が可能です。

しかし、C言語の厳密な規格では「{}」の中に1つ以上の初期値を持たせる必要があるため次のようにします。

データ型 識別子[要素数] = { 0 };
int aiData[5] = { 0 }; //配列の全ての要素に0を格納

配列の初期化と要素数の省略

初期化をする場合には、要素数を省略することが可能です。このため次の2つのプログラムはどちらも同じ意味となります。

int aiData[] = {1,2,3,4};   //要素数4の配列を定義
int aiData[4] = {1,2,3,4};  //要素数4の配列を定義

要素数の取得

要素数の取得には「sizeof演算子」を利用します。

「sizeof演算子」は識別子(変数、配列等)に割り当てられているメモリや、データ型のサイズ(Byte単位)を取得することができます。

このことから、「sizeof演算子」を用いて「(配列に割り当てられたメモリ/データ型のサイズ)」を計算することで、配列の要素数を取得することができます。

sizeof演算子を用いた要素数の取得

次のプログラムは、定義した2つの配列の要素数をそれぞれ出力します。定義時の要素数を変更しても結果が正しく出力されることを確認してみてください。

#include <stdio.h>

//sizeof演算子の動作例

//main関数
int main() {
  char achData[5];                             //char型配列の定義
  int aiData[4];                               //int型配列の定義

  int iSize_ch = sizeof(achData)/sizeof(char); //char型配列の要素数を計算
  int iSize_i = sizeof(aiData)/sizeof(int);    //int型配列の要素数を計算

  printf("char型配列の要素数は%dです\n",iSize_ch);
  printf("int型配列の要素数は%dです\n",iSize_i);

  return 0;
}

次のように配列にループ処理でアクセスするプログラムでは、「sizeof演算子」を利用することで後から要素数を変更する場合に配列の定義部分を変えるだけで済むことがメリットの一つです。

配列の要素数を好きな値にしてプログラムを実行してみてください。

#include <stdio.h>

//sizeof演算子の動作例2

//main関数
int main() {
  int aiData[] = {10,0,5,-1};                 //int型配列の定義
  int iSize_i = sizeof(aiData)/sizeof(int);   //int型配列の要素数を計算

  unsigned char uchCount;                     //for文で利用

  //要素数分ループ
  for(uchCount = 0; uchCount < iSize_i; uchCount++){
    //配列aiDataの要素chCountの値を出力
    printf("aiData[%d] = %d\n", uchCount, aiData[uchCount]);
  }

  return 0;
}

文字定数

文字定数とは、コンピュータ上で扱われる文字に対応する1Byteの数(この数のことを文字コードと呼ぶ)のことです。

前回、文字定数は「’(シングルクォーテーション)」で囲って表現すると述べましたが、実際には文字を「’」で囲むことで文字コードを得ることができます。

例えば「A」は「65」という文字コードなのですが、次のプログラムではどちらも同じ数値、文字が出力されます。

#include <stdio.h>

//文字定数の動作確認

//main関数
int main() {

  char chNum = 65;    //65という数値を格納
  char chCode = 'A';  //Aの文字コード(65)を格納

  //それぞれを数値として出力
  printf("chNum = %d, chCode = %d\n", chNum, chCode);

  //それぞれを文字として出力
  //%cで格納された数値を文字コードとして読み取り、文字を出力する
  printf("chNum = %c, chCode = %c\n", chNum, chCode);

  return 0;
}

尚、プログラム中にも記述しましたが、printfの「”」の間に「%c」を記述すると、「%c」の位置に「変数の値を文字コードで読み取った結果(文字)」を出力することができます。

実行結果は次の通りです。

chNum = 65, chCode = 65
chNum = A, chCode = A

この実行結果からも、「char型」も結局は数値を格納していて出力する際の表現方法を変えているだけだということが理解できたと思います。

特殊文字(エスケープシーケンス)

特殊文字は「\(記号) + 一文字」で表される文字(文字コード)のことです。

例えば「’」は文字コードを取得するためのキーワードになるため、次のように記述するとエラーになってしまいます。

char chTest = ''';

これでは「’」を文字として扱えない(文字コードで入出力できない)ため、特殊文字として「\’」と記述することで「’」の文字コードを取得することができます。

char chTest = '\''; //「\'」の文字コード(39)が格納される

他にも改行の「\n」等の特殊文字があります。知っておくと良いものとしては次のような特殊文字があります。(他にも存在します)

  • 「\n」:改行する
  • 「\t」:タブを挿入する
  • 「\0」:NULL文字
  • 「\a」:警告音を出す
  • 「\\」:「\」を挿入する
  • 「\’」:「’」を挿入する
  • 「\”」:「”」を挿入する

文字列定数

文字列定数は、文字定数が配列になったもののことです。

文字列を「”(ダブルクォーテーション)」で囲うことで文字列定数(文字定数の配列)として扱うことができるようになります。

また、文字列定数の末尾は必ず「\0(NULL文字)」になります。これは文字列の終端(文字列の終わり)を意味しています。

次の2つの配列「achData1」と「achData2」は同じデータを格納しています。

char achData1[] = {'H','e','l','l','o','\0'};
char achData2[] = "Hello";

試しに次のプログラムを実行して、出力してみましょう。

#include <stdio.h>

//文字列定数の出力

//main関数
int main() {

  char achData1[] = {'H','e','l','l','o','\0'};
  char achData2[] = "Hello";

  //%sはその位置に配列に格納された文字列を出力する
  printf("%s\n",achData1);
  printf("%s\n",achData2);

  return 0;
}

実行結果は次のようになり、同じ文字定数の配列を格納していたことがわかりました。

Hello
Hello

多次元配列

多次元配列とは2次元以上の配列のことで、次のように定義します。

要素のデータ型 識別子[次元1の要素数][次元2の要素数]...[次元nの要素数];

例えば「次元1の要素数が2、次元2の要素数が3」のint型の2次元配列を定義する場合、次のようになります。

int aiData[2][3]; //int型,次元1の要素数2,次元2の要素数3の2次元配列を定義

多次元配列の要素を参照するには、各次元の要素番号を指定します。例えば、配列に値を格納する場合は次の通りです。

配列名[次元1の要素番号][次元2の要素番号]...[次元nの要素番号] = 値;

実際のプログラムでは次のように記述します。

aiData[0][2] = 10;  //次元1の要素番号0,次元2の要素番号2の要素に10を格納

多次元配列を定義したとしても、メモリそのものは1次元(1本の棒状)です。このため、メモリ上には次元数分が並列に均されて、連続的に領域が確保されます。

人の感覚(プログラムを書く際)は図の右側のようなイメージかもしれませんが、実際には左のように1次元のメモリに格納されているということを覚えておいてください。

2次元配列の場合のメモリ上の領域と、人の感覚

次の図は、多次元配列がメモリ上の領域を確保した際に、要素の領域がどのように確保されているのかを示しています。

メモリ上の領域は、入れ子構造になっている

メモリ上では、この図のように次元1の要素から順に入れ子構造で各要素の領域が確保されています。

多次元配列の初期化

多次元配列も配列と同様に初期化することができます。

多次元配列の初期化では、メモリ上の構造と等しくなるように「{」と「}」で入れ子構造を作り、配列の各要素の初期値を格納します。

データ型 識別子[A][B]...[Z] = Aの配列{Bの配列{...{Zの配列}}, Bの配列{...{Zの配列}}, ..., Bの配列{...{Zの配列}}};

要するに、「Aの配列の要素はBの配列で、Bの配列の要素はCの配列で、…」という構造の説明をプログラムで示してやる必要があります。

「次元1の要素数が2、次元2の要素数が3」の2次元配列を初期化する場合、次のようになります。

データ型 識別子[2][3] = {{[0][0]の初期値(以後省略), [0][1], [0][2]}, {[1][0], [1][1], [1][2]}};

実際のプログラムでは次のように記述します。

int aiData[2][3] = {{1, 2, 3}, {4, 5, 6}};

多次元配列の初期化と、初期値の補完

多次元配列も配列と同様に、初期値が足りない場合には自動的に0が格納されます。

説明だけでは理解しにくい点もあるため、「次元1の要素数が2、次元2の要素数が3」の配列を例にいくつか紹介します。

値の確認には次のプログラムを用いています。自分で実行して、動作を確認してみると良いと思います。

#include <stdio.h>

//初期化時の0埋め

//main関数
int main() {

  int aiData[2][3] = {{1, 2, 3}, {4, 5, 6}};

  int iCount_i;                     //aiData[i][j]のi
  int iCount_j;                     //aiData[i][j]のj

  //ループ処理で順に表示
  for(iCount_i = 0; iCount_i < 2; iCount_i++){
    for(iCount_j = 0; iCount_j < 3; iCount_j++){
      printf("aiData[%d][%d] = %d \n", iCount_i, iCount_j, aiData[iCount_i][iCount_j]);
    }
  }

  return 0;
}
次元2の要素数が少ない場合。
int aiData[2][3] = {{1, 2}, {3, 4}} 

実行結果は次のようになり、次元2の最後の要素に0が格納されました。

aiData[0][0] = 1
aiData[0][1] = 2
aiData[0][2] = 0
aiData[1][0] = 3
aiData[1][1] = 4
aiData[1][2] = 0
次元1の要素数が少ない場合。
int aiData[2][3] = {{1, 2, 3}};

次元1の要素は次元2の3つの要素からなる配列ですが、実行結果は次のようになり全て0が格納されました。

aiData[0][0] = 1
aiData[0][1] = 2
aiData[0][2] = 3
aiData[1][0] = 0
aiData[1][1] = 0
aiData[1][2] = 0
全ての要素に0を格納する

配列でも全ての要素に0を格納する方法がありましたが、多次元配列でもそれは同じです。

次のようにすることで全ての要素に0が格納されます。

int aiData[2][3] = {};

多次元配列についてもC言語の厳密な規格では次のようにする必要があります。

int aiData[2][3] = { 0 };

多次元配列の初期化と、要素数の省略

多次元配列で初期化を行う場合には、最初の次元の要素数のみ省略して記述することができます。このため、次の2つは同じ意味を持ちます。

int aiData[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int aiData[][2] = {{1, 2}, {3, 4}, {5, 6}};

演習

最後に演習です。今回は配列1問、文字列定数1問、多次元配列1問です。

頑張りましょう!

演習1

配列を次の値で初期化し、配列に格納された値の合計と平均を求め、出力せよ。尚、平均は整数で良い。

92, 73, 60, 71, 86, 55, 98, 85, 69, 58
#include <stdio.h>

//演習1

//main関数
int main() {

  char achData[] = {92, 73, 60, 71, 89, 55, 98, 85, 69, 58};  //値の範囲が55-98なのでchar型配列で十分
  char chSize = sizeof(achData)/sizeof(char);                 //sizeof演算子で配列のサイズを取得

  int iAdd = 0; //合計値を格納
  int iAve = 0; //平均値を格納
  unsigned char chCount; //for文で利用

  //配列の全要素をiAddに加算する
  for(chCount = 0; chCount < chSize; chCount++){
    iAdd += achData[chCount];
  }

  iAve = iAdd/chSize;

  printf("合計は%d, 平均は%d\n", iAdd, iAve);

  return 0;
}

演習2

配列を次の文字列で初期化し、一文字ずつ文字と文字コードを出力せよ。ただしNULL文字は出力しない。

HelloWorld!
#include <stdio.h>

//演習2

//main関数
int main() {

  char chData[] = "HelloWorld!";              //配列に文字列定数を格納
  unsigned char uchCount = 0;                 //chDataの要素番号として利用

  //NULL文字を見つけるまでループ
  while(chData[uchCount] != '\0'){
    printf("%c = %d\n", chData[uchCount], chData[uchCount]);
    uchCount++;
  }

  return 0;
}

演習3

学籍番号と科目毎の得点を記した表がある。

学籍番号英語国語数学
1000848380
1001927984
1002716498
10031006789
1004475358
生徒番号と科目毎の得点

この表をもとに、「次元1の要素数5、次元2の要素数4」の2次元配列を定義、初期化せよ。次元2の要素は「学籍番号を0番目、英語を1番目、国語を2番目、数学を3番目」として扱うこと。

最も合計得点の高い生徒の学籍番号を出力せよ。

#include <stdio.h>

//演習3

//main関数
int main() {

  //適当な改行を行うことで,コードの可読性UP
  //aiData[生徒][情報(学籍番号,英語,国語,数学)]
  int aiData[][4] = {{1000, 84, 83, 80},
                     {1001, 92, 79, 84},
                     {1002, 71, 64, 98},
                     {1003, 100, 67, 89},
                     {1004, 47, 53, 58}};

  //最も合計得点の高い生徒の要素番号(1次元目の要素番号)
  //0番目が最大得点であると仮置きして,0を格納
  unsigned char uch1stNum = 0;
  //0番目の合計得点を格納
  int iMax = aiData[uch1stNum][1]
           + aiData[uch1stNum][2]
           + aiData[uch1stNum][3];

  int iSize1st = sizeof(aiData)/sizeof(aiData[0]);  //1次元目の要素数
  unsigned char uchCount;                           //for文で利用
  int iAdd = 0;                                     //n番目の生徒の合計得点を格納

  //0番目は最大得点であると仮置きしたため
  //1番目から(人数-1)回ループして比較
  for(uchCount = 1; uchCount < iSize1st; uchCount++){

    //uchCount番目の生徒の合計得点を計算
    iAdd = aiData[uchCount][1]
         + aiData[uchCount][2]
         + aiData[uchCount][3];

    //記憶した要素番号の生徒より,合計得点が大きい場合には
    if(iMax < iAdd){
      uch1stNum = uchCount; //記憶する要素番号を変更する
      iMax = iAdd;          //最大得点を変更する
    }
  }

  printf("最大得点の生徒は%d番です\n", aiData[uch1stNum][0]);

  return 0;
}

最後に

ようやく配列が終わりました!

多次元配列の定義、初期化など少し飛ばし気味だったので、わからないことは気軽にコメントで聞いてください!

おすすめの参考書

参考書を数冊載せておきます。人によって好みはあると思うので、書店や試し読みで確認してから購入することをおすすめします。

スッキリわかるC言語入門

新品価格
¥2,970から
(2021/4/25 22:34時点)

たった1日で基本が身に付く! C言語 超入門

新品価格
¥2,266から
(2021/4/25 22:39時点)

新・明解C言語 入門編 (明解シリーズ)

新品価格
¥2,530から
(2021/4/25 22:40時点)

コメント

タイトルとURLをコピーしました