コンピュータによる情報処理においては,四則演算や比較などの「データ処理法」と,条件分岐・繰り返しなどの制御構文にくわえて,
コンピュータ上で「データをどのように表現するか」の設計が重要となる.
これらは,それぞれアルゴリズムとデータ構造と呼ばれ,プログラムを書くためには双方が必要である.
工学で用いるデータ処理プログラムでは,数MB〜数GB以上の大量のデータを扱う場合がある.
例えば,高速度カメラによる動画像処理では,1000x1000pixel, 1000fps(毎秒1000枚の画像取得)のデータ取得があり,これは1秒あたり1GBのデータを扱うこととなる.
このような大量のデータをメモリ上に保存し,計算処理に用いるためには,通常の単純変数では不可能である.
一例として,キーボードから入力した 10 個の実数の平均と標準偏差を求めるプログラムを考えてみよう.
#include <stdio.h>
#include <math.h>
int main(void)
{
// 10 個の float 型変数
float a0, a1, a2, a3, a4, a5, a6, a7, a8, a9;
// KBD 入力させる
printf("a0 = "); scanf("%f", &a0);
printf("a1 = "); scanf("%f", &a1);
printf("a2 = "); scanf("%f", &a2);
printf("a3 = "); scanf("%f", &a3);
printf("a4 = "); scanf("%f", &a4);
printf("a5 = "); scanf("%f", &a5);
printf("a6 = "); scanf("%f", &a6);
printf("a7 = "); scanf("%f", &a7);
printf("a8 = "); scanf("%f", &a8);
printf("a9 = "); scanf("%f", &a9);
// 平均(average, mean)の計算
float ave = 0.0;
ave = ( a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 ) / 10.0;
// 分散(variance)の計算
float var;
var = (a0-ave)*(a0-ave) + (a1-ave)*(a1-ave) + (a2-ave)*(a2-ave) // 長いので途中で改行
+ (a3-ave)*(a3-ave) + (a4-ave)*(a4-ave) + (a5-ave)*(a5-ave)
+ (a6-ave)*(a6-ave) + (a7-ave)*(a7-ave) + (a8-ave)*(a8-ave) + (a9-ave)*(a9-ave);
var /= 10;
// 標準偏差(standard deviation)の計算
float stdev = sqrt(var);
// 結果の表示
printf("average = %f, standard deviation = %f \n", ave, stdev);
return 0;
}
このプログラムで正しい計算を行うことができる.
しかしよく見ると,このプログラムでは
printf, scanf
など,似たような同種の処理が繰り返されている.
このような場合は,for, while
などの「繰り返し文」を使いたいところだが,
変数名が a0, a1, ... , a9
と,それぞれ異なるため,繰り返し処理でうまく扱うことはできない.
さらに問題は,処理するデータ件数が10件ではなく5件,20件,100件と変化した時に,ソースコードを大幅に変更しなければならない.
これは煩雑になるだけでなく,記述ミスによるバグの温床になる.
そこで,複数の変数をまとめて 単一の識別子(=名前)で扱う方法が用意されている.
これを,配列, array(アレイ)と呼ぶ.
配列変数を使用したい場合は,プログラム中において以下の様に記述する.
型名 配列名[個数];
例1:整数型で,要素数10個の配列int a[10];
例2:実数型,要素数100個の配列float data[100];
配列定義のルール
int, float
など).const
指定した変数.
配列変数をプログラム中で参照したり,値を代入するには,以下のように記述する.
基本的には普通の変数と同じである.
#include <stdio.h>
int main(void)
{
int a[10]; // 整数配列
a[0] = 100; // 代入
a[1] = 200; // 代入
a[2] = 300; // 代入
printf("a[0] = %d, a[1] = %d, a[2] = %d \n", a[0], a[1], a[2]); // 参照する例
float b[100]; // 実数配列
b[0] = 0.01; // 代入
b[1] = 0.2; // 代入
b[2] = -9.99; // 代入
printf("b[0] = %f, b[1] = %f, b[2] = %f \n", b[0], b[1], b[2]); // 参照する例
double data[200]; // 実数配列
char string[100]; // 文字配列(文字列)
return 0;
}
要するに,配列名 + 添字で一つの変数だとみなせば良い.
10人分のテストの点数を格納する整数型の配列.
おすすめ:配列の要素数(ここでは10という定数)は,プログラム中の様々な場所で何度も使用するので,一般に定数変数(const
指定した変数)として定義しておく.
const int N = 10; // N は配列の要素数. const は「定数」という意味
int score[N]; // 配列の定義
この場合,1個あたり 4 byteの int
型変数が 10個分,メモリ上に確保される.
(具体的には,40 byte の領域がメモリ上に確保される.)
この配列を使って,何か処理をしてみよう.
// 値を順次代入する
score[0] = 100; // 配列の 先頭に 100 を代入
score[1] = 50; // 配列の 2番目に 50 を代入
.
.
.
score[9] = 90; // 配列の 終端に 90 を代入
for(int i=0; i<N; i++) { // i<10; とは書かない方が良い
printf(" %d 番目のデータは,%d です.\n", i, score[i]); // 全員の点数を画面に表示
}
Cの文法規則では,配列の添字は必ず0から始まるので,配列の最後の要素は score[9]
である.(score[10]
ではない)
したがって,要素数が10個の配列変数に,先頭から順にアクセスするための繰り返し文は,for(i=0; i<10; i++)
となる.
繰り返すが,この場合,i
は 10 以上の値にならないように気を付ける.
一般に,要素数 N 個の配列要素の最後の添字は N-1
である.添字の値はこの値を超えてはならない.
以下のソースコードは,初めに示した平均と標準偏差を計算するプログラムを,配列を用いて書き直したバージョンである.
どの箇所が異なるか,比較してみよう.
#include <stdio.h>
#include <math.h> // sqrt()用
int main(void)
{
const int N = 10; // const とは,値を変更できない変数であるという指示
// データの個数
float a[N]; // 配列の定義.float型変数,N個を記憶する場所が確保される
int i; // 繰り返し処理用のカウンタ変数
for(i=0; i<N; i++) { // N 回繰り返す
printf("a[%d] = ", i);
scanf("%f", &a[i]); // KBD入力.変数名の前に &
}
// 平均の計算
float ave = 0.0;
for(i=0; i<N; i++) {
ave += a[i];
}
ave /= N;
// 分散の計算
float var = 0.0;
for(i=0; i<N; i++) {
var += (a[i] - ave) * (a[i] - ave);
}
var /= N;
// 標準偏差の計算
float stdev = sqrt(var); // sqrt=square root. 平方根の計算用ライブラリ関数
// 結果の表示
printf("average = %f, standard deviation = %f \n", ave, stdev);
return 0;
}
N
の値を変更してみよう.例えば,3, 5 や,20,100 などとして,うまく処理できるか確認してみよう.const
指定について
基本的には,配列の要素数は,変数として用意しておく.
以下の様に,変数 N
は定数でなければならないが,最近の C99 規格に準拠したコンパイラでは,この制限が緩和されている.
コンパイラによっては,コンパイル時にオプションが必要な場合がある.
#include <stdio.h>
int main(void)
{
// 古いコンパイラでは, const int N = 100; としないと,エラーとなる.
int N = 100;
int aaa[N];
aaa[0]=0;
aaa[1]=10;
return 0;
}
配列では数多くのデータを扱うことが多いので,その値を代入・設定する方法に工夫が必要である.
実習課題で,以下のように複数のデータを入力させる問題がある場合は,これらの方法を用いる.
実行例:
Input 5 integers.
1 2 3 4 6 <-この行をキーボード等から入力
1 2 3 4 6 <-この行が出力される
#include <stdio.h>
int main(void)
{
int i;
const int N = 5;
int a[N];
printf("Input %d integers.\n", N);
scanf("%d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4]);
// 配列を表示して確認
for(i=0; i<N; i++) {
printf("%d ", a[i]);
}
return 0;
}
通常は様々な配列サイズに対応できるよう,繰り返し文を使用する.
#include <stdio.h>
int main(void)
{
int i;
const int N = 5;
int a[N];
for(i=0; i<N; i++) {
scanf("%d", &a[i]);
}
// 配列を表示して確認
for(i=0; i<N; i++) {
printf("%d ", a[i]);
}
return 0;
}
実行例1:1行にスペース区切りで入力
1 2 3 4 6 <-この行をキーボード等から入力
1 2 3 4 6 <-この行が出力される
実行例2:1行に1個,数値を入力
1 <-この行から
2
3
4
6 <-この行までをキーボード入力,毎回Enterキーを押す
1 2 3 4 6 <-この行が出力される
毎回数値を手入力するのは面倒なので,数値データなどを別ファイルなどに記録しておき,そこから「リダイレクト」と呼ばれる方法でデータをプログラム内の配列に流し込む方法もある.
以下の例では,あらかじめ in.txt というテキストファイルを作成しておき,ファイル内に書かれたデータを一度に配列に入力させることができる.
以下の内容をin.txtという名前で保存 1 2 3 4 6
実行方法(リダイレクト入力) .\a.exe < in.txt 1 2 3 4 6 <-実行結果
C言語では,処理効率を優先するため,配列の添字範囲が
したがって,配列の添字が有効範囲を超えてもコンパイラはエラーを出さない.
さらに悪いことに,実行時にエラーとなることもあればエラーとならないこともある.
また,一見うまく動作しているように見えても,偶発的にソフトウエアが落ちることがある.
(buffer overflow という)
以下のプログラムを実行して,動作を確認せよ.
配列要素を超えるとどうなるか,実行してみよう.for文中の 定数 1000 を 10000 や 100000 などに変更してみよう.
#include <stdio.h>
int main(void)
{
const int N = 10;
int x[N]; // 要素数 N 個の配列
int i; // カウンタ変数
for(i=0; i<1000; i++) { // 添え字の範囲は 0 から N-1(ここでは9) まで許されるが...
x[i] = i*i;
printf("x[%d] = %d \n", i, x[i]);
}
return 0;
}
【ヒント】繰り返し文などの終了判定に,直接「定数」を置くのが間違いのもと!
C言語の配列の名称は,配列の先頭アドレス(=メモリ中の場所のこと)を示すラベルにすぎない.
したがって,C言語では,配列全体を一度に代入する 「=」 演算子が無いため,面倒でも1つ1つ代入する必要がある.
(ここでは説明しないが,メモリの内容をコピーするライブラリ関数は存在する.)
配列全体の正しいコピー方法
const int N = 10;
int a[N], b[N]; // 配列が二つあるとする.
// a の中身を順に b にコピー
int i;
for(i=0; i<N; i++) {
b[i] = a[i]; // 各要素を 1 個づつ代入
}
以下は,全て誤った配列のコピー方法.
正しくないばかりか,プログラムが不正終了する場合がある.
b = a; // ダメな例.
b[9] = a[9]; // 最後の要素一つがコピーされるだけ.
b[10] = a[10]; // そもそも [10] というのがアクセス違反
配列は,ただ定義しただけでは中身は「不定」となる仕様となっている.
即ち,どのような値が入っているか規格上は決められていない.
したがって,必要に応じて,あらかじめ 0 などの初期値を代入する必要がある.
以下は,実数型配列の中身を全てゼロに設定する例である.
// clear array : 配列のゼロクリア
const int N = ???; // N は要素数.いくつでも良い.
float array[N]; // 配列本体
int i;
for(i=0; i<N; i++) {
array[i] = 0.0; // ひとつずつ0を代入
}
配列中にゼロ以外の値を設定するためには,基本的には1つ1つの要素に値を代入する.
int array[5]; // 要素数5の配列変数の定義
array[0] = 1; // 1個ずつ代入
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
しかし,配列の「定義」と同時に値を設定する場合には,以下のような初期化リストを使った「配列の初期化」を行うことができる.
// これはOK.代入ではなく初期化という.
int array[5] = { 1, 2, 3, 4, 5 };
さらに,初期化リストより配列の要素数が確定しているので,[ ]
の中の要素数を省略できる.
// この場合は,配列サイズ省略OK
int array[] = { 1, 2, 3, 4, 5 };
この場合,配列のサイズはコンパイラが自動的に決定してくれる.
しかし,配列の要素数はそのあとの処理で必要になるので,一般的には省略しないほうが良い.
ここまでの「=」記号は,「配列の初期化」であり,(記号は同じであるが)代入ではない.
従って,以下のコード中の =
はすべて代入とみなされるので,いずれもコンパイルエラーとなる.
int array1[5]; // 配列の定義
array1 = { 1, 2, 3, 4, 5 }; // これはエラー!
array1[] = { 1, 2, 3, 4, 5 }; // これもエラー
array1[0] = { 1, 2, 3, 4, 5 }; // もちろんこれもエラー
Cでは,1次元の配列だけではなく,2次元の配列を作ることもできる.
例えば,3x4の2次元配列を作るには,
int a[3][4]; // 2次元配列の定義
a[0][0] = 10; // 代入例
a[1][2] = 5;
とする.
この時の配列の形は,概念的には以下のようになる.
代数でいうところの行列によく似ている.
最初の[]
が行を,後の[]
が列を表している.
実際のコンピュータのメモリは1次元なので,あくまで概念的に2次元と考えられる,と理解すると良い.
(内部で2次元→1次元の添字の変換計算が自動的に行われる.)
2次元配列の場合も以下のとおり「定義と初期化」が可能である.
const int M = 4;
const int N = 3;
int array2D [M][N] = {{ 1, 2, 3}, // <- これで1行分
{ 4, 5, 6},
{ 7, 8, 9},
{10, 11, 12} };
ただし,要素数の省略は,最初の[]
のみ許される.
const int M = 4;
const int N = 3;
int array2D [ ][N] = {{ 1, 2, 3},
{ 4, 5, 6},
{ 7, 8, 9},
{10, 11, 12} };
1次元配列と同様,2次元配列にデータを格納するには以下のように2重ループを用いる. 添字の順序に注意して記述する.
#include <stdio.h>
int main(void)
{
const int M = 4;
const int N = 3;
int a[M][N];
int i,j;
for(i=0; i<M; i++) {
for(j=0; j<N; j++) {
scanf("%d", &a[i][j]); // a[i][j] で,1つの変数と見れば良い.
}
}
// 配列を表示して確認
for(i=0; i<M; i++) {
for(j=0; j<N; j++) {
printf("%d ", a[i][j]);
}
printf("\n"); // 1行分表示したら改行
}
return 0;
}
実行例1
1 2 3 <- この行から
4 5 6
7 8 9
10 20 30 <- この行までをキーボード入力,1行入力するごとにEnterキー
1 2 3 <- 出力
4 5 6
7 8 9
10 20 30
実行例2:行-列の順に,1行にまとめて入力
1 2 3 4 5 6 7 8 9 10 20 30 <- 1行で入力
1 2 3 <- 出力
4 5 6
7 8 9
10 20 30
一次元,二次元の配列だけではなく,三次元以上の配列を作ることもできる.
int a3[10][3][4];
これは,3×4 の 二次元配列が 10 個,計120個の整数が利用できる.
さらに配列の次元を増やして,
int a3[10][3][4][5][10][3];
のような記述も可能であるが,工学上の問題の性質を考えれば,せいぜい2∼3次元程度が実用的である.
いずれの多次元配列も,コンピュータのメモリ上では1次元配列として格納されている.
以下の問それぞれに対応するプログラムを作成しなさい.
キーボードから整数を10個入力して配列に順次格納し,正順および逆順で画面に表示せよ.
#include <stdio.h>
int main(void)
{
const int N = 10; // 配列の要素数
int array[N]; // 整数 N 個分の配列
// データ入力
for(...) {
// printf(...); // 必要に応じて
scanf(...);
}
// 配列データを正順に出力
// 配列データを逆順に出力
return 0;
}
実行例:
1 0 6 2 5 3 7 9 8 4 <- キーボードから入力 またはファイルからリダイレクト
1 0 6 2 5 3 7 9 8 4 <- 正順で出力
4 8 9 7 3 5 2 6 0 1 <- 逆順で出力
キーボードから実数を10個入力し配列に格納し,それらの平均値を指定の桁数(例えば小数点以下3桁)で画面に表示せよ.
ヒント:合計値を記憶しておく変数を別途用意し,0で初期化しておき,そこに配列の各要素の値を順次,足してゆく.
#include <stdio.h>
int main(void)
{
const int N = 10; // 配列の要素数
double data[N]; // 実数 N 個分の配列
// データ入力
for(...) {
// printf(...); // 必要に応じて
scanf("%lf", &...); // double には %lf
}
// 合計値の計算
double sum = 0.0;
for(...)
...
// 結果の出力
printf(" ??? ", .... );
return 0;
}
実行例:(結果の数値は正しいとは限らない)
1 2 5 3 10 9 0.9 1.1 2.2 0.1 <- キーボードから入力 またはファイルからリダイレクト
5.320 <- 小数点以下3桁で出力
0から100までの整数値をランダムに10個生成して配列に格納,そのなかから最大値を探し,値を画面に表示せよ.
注1:ランダムな整数値(乱数と呼ぶ)の生成には,ライブラリ関数であるrand()
を用いる.
参考までに,以下ソースコードを用いてよい.
注2:渡された配列内の値の最大値を探索する場合,暫定1位を記憶しておく変数max
を用意して,配列の値を順次比較する.
#include <stdio.h>
#include <time.h> // time()用
#include <stdlib.h> // rand() 用
int main(void)
{
const int N = 10;
int a[N]; // 整数 N 個分の配列
////////////////////////////////////////////
// 0~100 の乱数を生成して配列に代入
srand(time(NULL)); // 乱数テーブルを初期化
for(int i=0; i<N; i++) {
a[i] = rand() % 101; // rand()は呼ばれるたびに整数の乱数を返す.
// printf("%d ", a[i]); // debug用
}
printf("\n");
//////////////////////////////////////////////////
// 配列の中から最大値を見つける
int max; // ここに最大値を格納
// 最大値を探す処理
????
// 結果の出力
printf("max = ...\n", max);
return 0;
}
実行例:(乱数の値は毎回異なる.)
55 82 57 62 4 81 19 49 0 26 <- 確認の為の表示
max = 82
配列の数値データを降順(大きい順)に並べ替えて,前後の値を画面に表示せよ.
このような処理を「ソーティング」という.
基本的に二重のループ処理となる.
#include <stdio.h>
int main(void)
{
// 配列サイズや値を変更しても正しく動作すること!
const int N = 5;
int a[N] = {3, 2, 5, 4, 1};
//////////////////////////////
// 最初の配列データの出力
printf("Before :");
for(...) {
printf(" ...", ???);
}
printf("\n");
//////////////////////////////
// 並べ替え.
for(...) {
for(...) {
...
}
}
//////////////////////////////
// 結果の出力
printf("After :");
for(...) {
printf(" ...", ???);
}
printf("\n");
return 0;
}
実行例
Before : 3 2 5 4 1
After : 5 4 3 2 1
配列データの並べ替えには,さまざまなアルゴリズムが提案されている.
一番単純な降順の並べ替えは,以下のような手順で実現可能である.
ここでは配列名を a
,要素数をN
とする.
a[k]
と,それ以降の配列の値 a[l]
, l=k+1, k+2, ... , N-1 とを順次比較.a[l]
がa[k]
より大きければ,a[k]
の値とa[l]
の値を入れ替える.swapという.
l=N-1の時点でa[k]
の値が最大値となっている.
この手順を繰り返し,全てのkに対する処理が完了した時点で,配列の先頭から大きい値が順に並んでいる.
また,上記の「大きければ」の比較を「小さければ」にすれば,昇順(小さい順)の並べ替えができる.
このほかにもさまざまなソーティングアルゴリズムがあるので,興味があれば各自で調べてみよう.
キーワード:バブルソート,クイックソート,ヒープソートなど.