Lecture 7 配列(1)

配列(1)の解説動画

昨年度の講義(昨年度は第6回として実施)の録画を,以下に載せました. 聞き逃した人はこちらを確認してください.




配列(array)は,変数のリスト(並び)のことを言う.プログラムでは,単一の変数を一つ一つ宣言して利用するよりも, 複数の変数の並び(=配列)を用いたほうがずっと使いやすいことが多々ある.ここではそのような配列の使い方について学ぶ.

1次元配列

1次元配列は,以下の様に宣言する.
  型 変数名 [個数];
「型」はCの変数型名,「変数名」は配列の名前,「個数」は配列の要素の数を示す.例えば,1クラス最大80名の学生のテストの点数を示す配列を宣言するなら以下の様にするだろう.
  int result_of_a_test [80];
または,正の整数だけ扱えればよく,最小のメモリ消費に抑えたければ,以下の様にする.
  unsigned char result_of_a_test[80];	// 0-255までの値を80個扱える配列
配列の要素にアクセスするには,要素の番号を使って,配列に添字(index)をつける. 配列の一番最初の要素にアクセスするには,
  result_of_a_test[0] = 100;	// 配列の先頭の要素に100を代入
とする.配列の要素番号は0から始まる.従って,最初に80個宣言した配列の最後の要素は,
  result_of_a_test[79] = 100;	// 配列の最後の要素に100を代入
のように,indexが79となる.

配列のメモリ配置

C言語では,配列は連続したメモリに配置される.その時,低いアドレスから順に配置される.つまり最初の要素が最も低い(小さい)アドレスにある. 例えば,
  int a[5];
  for(int i=0; i<5; i++)
      a[i] = 'A'+i;
が実行されると,メモリ配置は,



となる.ここで,要素の中身を示す a[i] に対して,& をつけるとその要素のアドレスになる.

配列の要素番号(index)が0から始まる理由

配列の要素にアクセスするための書式,
    [要素番号]
は,実は「演算子」であり,その意味するところは,
先頭アドレスからのオフセット」すなわち
「先頭アドレスから『要素番号』分,離れたアドレスにある要素」
を示す.最初の要素は先頭アドレスにあるため,オフセットは0になる.この考え方は,C言語というよりも,デジタルコンピュータに共通のアドレス変換に関する考え方によるもので,次回に学習する「ポインタ」の考え方に共通する. 次回のテーマである,ポインタを先取りして簡単に紹介すると,例えば,今回の配列の要素へのアクセス,
    a[3]
は,ポインタを用いて,
    *(a+3)
と書くことができる.ここで,aは配列名であり,配列の先頭アドレスを示し,+3はオフセット,すなわち (a+3)は先頭から3要素分離れた場所にある要素へのアドレス(参照)を示す.その前についている *(asterisk) は,参照の中身を示す演算子(ポインタ)であり,上のテーブルの場合,
    *(a+3) == 'D'
である.
このポインタを用いて上記の配列のメモリ配置を示すと以下のようになる.

このように配列とポインタは互換性がある.
【ヒント】配列表記との違いに気をつけよう


配列の添字範囲のチェックはされない

Cでは,配列の添字範囲が正しいかどうかはプログラマに委ねられている. 配列の添字を超えるアクセスは,多くの場合,ソフトウェアの実行そのものに甚大な悪影響を及ぼす. 以下のプログラムを実行して,動作を確認せよ.
#include <stdio.h>
int main(void)
{
    int x[10];
    for(int i=0; i<100; i++){
        x[i] = i*i;
        printf("x[%d]=%d\n", i, x[i]);
    }
}
※上記のプログラムのどこがいけなかったのだろうか?
※どこをどのように直せばよいだろうか?
※同じような誤りを犯さない様にするにはどうすればよいだろうか?
【ヒント】ソースコード中に直接「数字」を書いてはいけない!

配列をひとまとめにコピーすることはできない

配列は,一つ一つの変数のリストにすぎず,配列の名称は,配列の先頭アドレスを示すラベルにすぎない. 従って,要素一つ一つをコピーすることなしに,(代入文などによって)全体のコピーを作ることはできない
    int a[10], b[10];
    b = a;		// エラー.a, b共にラベルであって,変数ではない.
    b[9] = a[9];	// 最後の要素一つがコピーされるだけ.
要素一つ一つをコピーする関数を作る.
void copy_array(int b[], int a[], int n){
    for(int i=0; i<n; i++)
        b[i] = a[i];
}
int main(void){
    int a[100], b[100];
    copy_array(b,a,100);	// copy a to b
}
あるいは,バイナリコピー関数を利用してコピーする.
#include <string.h>
int main(void){
    int a[100], b[100];
    bcopy(a,b,sizeof(int)*100);	// copy a to b
}
* C++では,定義次第では,代入によって多くのオブジェクトを一気にコピーする機能を作ることもできる.



多次元配列

Cでは,一次元の配列だけではなく,二次元以上の配列を作ることもできる.例えば,3行4列の表(テーブル)状の配列を作るには,
    int a[3][4];
と宣言する.
この時の配列の概念図は以下のようになる.

数学でのそれに近く,最初の[]が行を,後の[]が列を表している.


多次元配列のメモリ配置

プログラム中で多次元配列を作ることができても,実際にはコンピュータのメモリのアドレスは1次元に並んでいる. このため,コンパイラは多次元配列が宣言されると,1次元のメモリ上に以下のように配置する.

従って,この2次元配列 a[i][j] は,以下のように1次元配列としてアクセスすることもできる. この様な,多次元配列を1次元配列としてアクセスすることは非常によく行われる.
    const int M = 3;
    const int N = 4;
    int a2[M][N];
    int* a1 = a2[0];	// 先頭アドレスをあわせる.ポインタを使用
    x = a1[N*i+j];		// == a2[i][j] とおなじ

高次元配列の宣言

3次元以上の配列も,同様に作ることができる.
    int a3[10][3][4];
これは,3x4の2次元配列が10セット,3次元的にレイヤになっていると考えるとわかりやすい.

配列の初期化

配列の初期化は以下のように行う.
     int array1[] = { 1, 2, 3, 4, 5 };
この時,[]の中身を書かなくても良い.サイズはコンパイラが自動的に決定してくれる.これは次の文字列の初期化に利用するとメリットが実感できるだろう.
    char str[] = "mojisuu wo kazoenakutemo yoi!";
後で文字列を変更した時にも,文字数をわざわざ数える必要がない.

多次元配列の場合は,最初の[]のみ,書かなくても許される.
    int array2[][3] = {
       {1, 2, 3},
       {4, 5, 6},
       {7, 8, 9},
       {10, 11, 12}
    };



Quiz 7 配列1(第7回の提出課題ではありません)

以下の問それぞれに対応するプログラムを作成しなさい.

    1. 10個の整数がスペース区切りで並んでいる.これらの数字列を,配列 array[] に読み込む関数 read_array() を作りなさい. 同時に,この配列 array[] から値を読み取り,画面に表示する関数 write_array() を作りなさい.

      入力例:
      3 5 19 100 21 -300 4 -82 5 8
      

      出力例:
      3
      5
      19
      100
      21
      -300
      4
      -82
      5
      8
      



    2. 上で作った配列 array[] から,最も大きな値を探し出す関数 find_max() を作れ.


    3. 配列 array[]の中の最大値が格納されていた配列要素の次の位置にある値を返す関数, the_next_of_max() 関数を作成せよ.もしも,最大値が配列の最後である場合,エラーを返しなさい.


    4. 上で作った配列 array[] から「奇数番目の値」だけ抜き出し,別の配列(ここでは array2[]とする)に 格納する関数 copy_odd_numbered_items() を作りなさい.
      ところで,この新しい配列 array2[] は,元の配列 array[] とは格納されている数値の個数が異なっている. 上記の問題で作成した配列表示関数を改良して,array[] と array2[] のどちらの中身も表示できる関数 write_array2() を作成せよ.


    5. 上とは異なり,配列 array[] から,「奇数の値」のみ抜き出し,別の配列(ここでは odds[])に コピーする関数,copy_odds() を作りなさい.





Assignment 7(第7回の宿題)

課題の提出は Online Judge にて行います.

  • 提出期限があります.できるだけ早めにやりましょう.
  • Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
  • わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
  • それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています. ZoomのリンクはOh-o! meijiで確認してください.
  • Online Judge のページはこちら