lesson5 (update:2020/10/19)


[ prev | next | index ]


5.配列

目次


5.1 配列(array)

これまでは、1つの変数で1つの数値(値)を扱ってきました。しかし、例えば成績などで ある科目の点数を扱うとします。そのときAさんの点数、Bさんの点数、Cさんの点数など 同じデータ型の変数を複数準備することは非効率的と言えます。そこで、そのような場合 配列を準備してあげると扱いも容易になり、プログラムも見やすくなります。 配列は図に示すようなデータの入れ物としてとらえることができます。

例:成績の点数を入れるための配列

1次配列

配列は1要素の必要バイト数×要素数だけのメモリーを1個の連続した領域として確保します。要素データの参照は配列領域の先頭番地と、そこから何番目の要素かを指定することで行われます。各要素には順番に 0から番号が付けれてます。従って番号は 0〜要素数-1 の範囲の値です。

配列変数の宣言では下記の様に型名、変数名、要素数を指定します。要素数はコンパイルの段階で値が定まっている定数式であることが必要です。

型名 変数名[定数式];

(例)int seiseki[100]; /* 整数型で要素数100個の配列で変数名はseiseki */

seiseki

[0] [1] [2] [3] [4] [98] [99]
1個目の
データ
2個目の
データ
3個目の
データ
4個目の
データ
5個目の
データ
    99個目の
データ
100個目の
データ

個々の要素データの読み書きは番号を指定して行います。この指定に添字演算子 (subscript operator)[ ]を用います。ここで番号は整数値を与える式でもかまいません。

変数名[番号]

(例)seiseki[0]=75;
seiseki[1+2]=85;

array

[0] [1] [2] [3] [4] [98] [99]
75 2個目の
データ
3個目の
データ
85 5個目の
データ
    99個目の
データ
100個目の
データ

配列を用いる計算では、添字の最大値に注意が必要です。配列の範囲外の メモリにデータを書き込む様なプログラムを書いても、コンパイラはエラーを出しません。 実行時に問題が起きて間違いを発見する例は少なくありません。

(例)seiseki[100]=97;

array

[0] [1] [2] [3] [4] [98] [99] [100]
75 2個目の データ 3個目の データ 85 5個目の データ     99個目の データ 100個目の データ 97

この例では添字100に対応するメモリを他の用途に使っているとトラブルが起きる。この間違いはよくある。

(注)メモリ領域の大きさをsizeof演算子で取り出すことができます。これを用いて配列 添字の上限を求めることが可能です。しかし、いつでも有効とは行かないので、配列の要素数は定数か変数として別に持ちましょう。

(例)
int seiseki[100];
int max;
...
printf("seisekiの領域の大きさは%dバイト\n",(int)sizeof(seiseki));
printf("int型データ領域の大きさは%dバイト\n",(int)sizeof(int)); max=sizeof(seiseki)/sizeof(int);/*要素数の計算*/ printf("seisekiの要素数は%dで添え字の最大値は%d\n",max,max-1); ...

しかしながら上記は要素数を定数で与え、下記のように書いた方が良いでしょう。

#define max 100
/*ここで#defineはマクロ命令*/
/*以後の文字列「max」は文字列「100」にコンパイ前に置き換えられる*/
/*配列宣言で要素数に変数が使えない為、マクロ命令で定数もどきを実現した*/
/*const int max=100;の様な定数変数を配列の要素数指定には使えない*/ int seiseki[max];
.....

関数の引数で配列を受け取る場合はポインタ変数が使われることが多く、このsizeof演算子ではポインタ型データの大きさ(4バイト 程度)しかわからないので要素数は別の引数で渡すようにします。

[目次]


5.2 静的な配列

関数の外で定義された配列変数や、関数の中でstaticを指定して定義された配列変数は静的な配列となります。これらの配列はプログラムが開 始して終了するまでのあいだ存在します。

5.2.1 変数の定義と初期化

以下に静的配列の定義例を示します。静的配列は0で初期化されますが、リテラルで初期値を指定することも可能です。

/*関数の外の大域変数*/
int data1[1000];
/*1000個の要素が0で初期化された配列*/ int data2[]={1,2,3};/*リテラルで全要素の初期値を指定した配列*/ int data3[10]={1,2,3};/*指定された要素数の一部の初期値をリテラルで指定した場合。              未指定部分は初期値0となる*/ int data4[3][2]={{0,1},{2,3},{4,5}};/*2次元配列*/
/**/
int main(void)
{
/*関数内の静的変数*/ static int data5[10];/*10個の要素が0で初期化された配列,main関数の局所変数*/
.....
}

5.2.2 例題5-1 文字の置き換え

#include <stdio.h>

#define max 27
/*文字配列の初期化には文字列も使える*/
char smallChr[max]="abcdefghijklmnopqestuvwxyz";
char largeChr[max]="ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/* 一文字読み込み関数
動作: 一文字読み込み、文字が配列smallChrの文字の場合はlargeChrの文字に変換して戻す。
例外: 読み込みで文字が読めなかった場合は−1を戻す
*/
int readChar()
{
/*この関数でしか使わないなら、ここで静的な変数として定義しても良い
static char smallChr[max]="abcdefghijklmnopqestuvwxyz";
static char largeChr[max]="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
*/
char c;/*読み込んだ文字*/
int num;/*読み込んだデータ数*/
int i;
/**/
num=scanf("%c",&c);
if(num!=1) /*読み込めない場合*/
return -1;/*-1を戻す。途中からのreturnも可能*/
/**/
for(i=0;i<max;i++){
if(c==smallChr[i]){
c=largeChr[i];
break;
}
}
return (int)c;
}
/*main*/
int main( void )
{
int c;
while(1){
c=readChar();
if(c==-1)break;/*通常はCtr+ZでEOFが入力され終了する*/
putchar(c);
}
return 0;
}

[実行結果]

 
入力>abc 123 ABC %&#(この後Enterキー)
出力>ABC 123 ABC %&# <=abcがABCに変換されている
入力>^Z(この後Enterキー)^ZはCtrlキーとZキーを同時押で入力する。入力の終端を意味するコード
(注意:レポートツールでは^Zはキー入力できないのでEOFボタンを押してください)
 [目次]


5.3 動的変数の配列

関数の中でstatic無しで局所変数として配列を定義すると、前ページで紹介したスタックの仕組みでメモリを動的に確保します。関数が終了すると、割り当てられたメモリーは他の目的に再利用されます。従って、配列の寿命は関数の実行中のみとなります。

5.3.1 スタックがあふれないように注意

 関数は呼び出された時点で、まず局所変数をスタックに積み上げます。配列変数でも同様です。この領域は関数が戻る時に崩されますから配列 の寿命は関数の実行中のみです。

 ここで、スタックはプログラムが起動する時に大きさを決めて用意されたメモリー領域で、スタックの大きさはプログラムの起動時に定められます。関数が使うかも知れない配列のことまで考えてスタック領域の大きさを決めているわけではありません。大きな配列を使うとスタックが溢れる場合もありま す、注意して下さい。要素数が数十個なら問題はありませんが、大きな配列を使いたい場合は

  • 静的配列(static)にする
  • ポインタを用いて配列をヒープメモリに確保する
  • スタックを大きく取る(コンパイラの設定等を変更)

といった方法を考えて下さい。

5.3.2 変数の定義と初期化

局所変数の配列ではデフォルトでは初期化は行われない。リテラルによる初期化はC言語の処理系依存で、できない場合もある。以下に局所変数によ る配列の定義と初期化の例を示します。

int main(void)
{
/*関数ブロックの中の局所変数*/ int data[10];/*10個の要素をもつ配列,main関数の局所変数。初期化無し*/
int data2[3]={1,2,3};/*3個の要素を持ち初期値1,2,3で初期化された配列*/
char data3[4]={'a','b','c','\0'};/*文字配列*/
/*このリテラルによる初期化は処理系依存でコンパイルエラーとなることもある*/
.....
/*配列dataをプログラムで0に初期化*/
for(i=0;i<10;i++)/*注意:添字iの範囲は0〜9*/
data[i]=0; ..... }

5.3.3 配列を使う場合の注意

工学的問題ではベクトルがよく出てきます。ここでは3次元空間のベクトルを要素数3の配列で表現して、計算プログラムを作ってみました。この様 な配列を使うプログラムでは注意しなければいけない点が幾つか出てきます。例えば

  • 配列は関数の戻り値型に使えない
  • 仮引数を配列の様に指定しても、実際の仮引数はポインタである
  • スタックを使う局所変数の配列は関数実行中しか存在しない

従って次のような2つの配列を受け取って足し算を行い、その結果を入れた配列を戻すような関数は作れません。

/* これはコンパイルエラーになる*/
double[length] plus(double a[length],double b[length]) ^^^^^^^^^^^^エラー:配列型は関数の戻り値にはできない { double c[length]; int i; for(i=0;i<length;i++)c[i]=a[i]+b[i]; return c; ^^警告:局所変数cの領域は破棄される }

この様な関数は大域変数かポインタを使わないと作れません。

次の例題プログラムはポインタを利用していますが、ポインタが表に出ない形に書かれています。


例題5-2 3次元ベクトルの足し算

この例題プログラムは、配列を使って3次元ベクトルの足し算を行うプログラムです。 プログラムを打ち込んでみて、動作を確かめてみましょう。


/* 3次元ベクトルの足し算 */
#include<stdio.h>
#define length 3  /* 配列の要素数がすべて同じなのでマクロ定数で要素数を示す */

/* {x0, x1, x2} の形でベクトルを書き出す関数 */
void print(double x[length])
{
    int i;
    printf("{");
    for(i=0;i<length;i++) {
        if(i!=0) printf(",");
        printf("%4.1f",x[i]);
    }
    printf("}");
}

/* c = a + b */
/* 結果を返す配列Cを引数で渡します */
void plus(double C[length], double A[length], double B[length])
{
    int i;
    printf("関数plusの仮引数A,B,Cのサイズは\n");
    printf("sizeof(A)=%d\n",(int)sizeof(A)); /* サイズを見ると仮引数はポインタです */
    printf("sizeof(B)=%d\n",(int)sizeof(B));
    printf("sizeof(C)=%d\n",(int)sizeof(C));
    for(i=0;i<length;i++)
        C[i]=A[i]+B[i]; /* 仮引数Cはポインタなので実引数の配列に代入される */
    
}

/* メイン関数 */
int main(void)
{
    /* 局所変数として配列を用意 */
    double a[]={1,2,3}; /* 配列の初期化は処理系依存 */
    double b[length]; /* 初期化が拒否される場合はプログラムで初期化 */
    double c[length];
    
    /* プログラムによる初期化の場合 */
    b[0] = 4; b[1] = 5; b[2] = 6;
    
    /* 計算する前に今の値を表示しておく */
    printf("計算する前のa,b,cは\n");
    printf("a="); print(a); printf("\n");
    printf("b="); print(b); printf("\n");
    printf("c="); print(c); printf("\n"); printf("\n");

    printf("関数mainの実引数a,b,cのサイズは\n");
    printf("sizeof(a)=%d\n",(int)sizeof(a)); /* サイズを見ると実引数は要素数3の配列 */
    printf("sizeof(b)=%d\n",(int)sizeof(b));
    printf("sizeof(c)=%d\n",(int)sizeof(c));
    printf("\n");

    printf("関数plus(c,a,b)を呼ぶと\n");
    plus(c,a,b);
    printf("\n");
    
    /* 計算後の配列c の値を表示 */
    printf("plusは値を返さないが\n");
    printf("実引数c はa+b の値になる\n");
    printf("c ="); print(c); printf("\n");

    return 0;
}


[実行結果の例]

  計算する前のa,b,cは
  a={ 1.0, 2.0, 3.0}
  b={ 4.0, 5.0, 6.0}
  c={ 0.0,-218.7, 0.0} <配列c は定義しただけなので、初期化されていない
  
  関数mainの実引数a,b,cのサイズは
  sizeof(a)=24 <double(8バイト)3個分なので8*3=24バイト
  sizeof(b)=24
  sizeof(c)=24
  
  関数plus(c,a,b)を呼ぶと
  関数plusの仮引数A,B,Cのサイズは
  sizeof(A)=4 <Aはポインタ型変数(4バイト)。メモリ番地の値が入っている
  sizeof(B)=4
  sizeof(C)=4
  
  plusは値を返さないが
  実引数c はa+b の値になる
  c ={ 5.0, 7.0, 9.0} <正しく結果が入っている

 [目次]


5.4 演習課題

このファイル(p05.c)を使ってプログラムを作成すること。

課題5 グラフの作成

鹿児島の月間平均気温(12ヶ月分)が入った下記の配列temperatureがある(単位は℃)。
これを元に各月の気温を棒グラフを表示し、年間平均気温を示すプログラムを作りなさい。
ただし、グラフは1℃を"■"で表し、小数点以下は無視する。

配列temperatureはmain関数の外で宣言するとグローバルな配列となる。main関数の中に入れるときはstaticを付ければ静的な配列になる。

.....

/* 鹿児島の月間平均気温(1月−12月)*/
double temperature[]={8.5,9.8,12.5,16.9,20.8,24.1,28.1,28.5,26.1,21.2,15.9,10.6};

int main(void)
{
.....
}

[期待される実行結果]

 1月: ■■■■■■■■
 2月: ■■■■■■■■■
 3月: ■■■■■■■■■■■■
 4月: ■■■■■■■■■■■■■■■■
 5月: ■■■■■■■■■■■■■■■■■■■■
 6月: ■■■■■■■■■■■■■■■■■■■■■■■■
 7月: ■■■■■■■■■■■■■■■■■■■■■■■■■■■■
 8月: ■■■■■■■■■■■■■■■■■■■■■■■■■■■■
 9月: ■■■■■■■■■■■■■■■■■■■■■■■■■■
10月: ■■■■■■■■■■■■■■■■■■■■■
11月: ■■■■■■■■■■■■■■■
12月: ■■■■■■■■■■

年間平均気温は、18.6 ℃

[ prev | next | index ]