コンピュータによる情報処理においては,四則演算や比較などの「データ処理法」と,条件分岐・繰り返し・関数などの制御構文にくわえて,
コンピュータ上で「データをどのように表現するか」の設計が重要となる.
これらは,それぞれ「アルゴリズム」と「データ構造」と呼ばれ,よいプログラムを書くためには双方を熟知している必要がある.
工学で用いる実用的なプログラムでは,数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(アレイ)」と呼ぶ.
配列変数を使用したい場合は,プログラム中において以下の様に記述する.
型名 配列名[個数];
例えば,整数型で,要素数10個の配列は
int a[10];
実数型で,要素数100個の配列は
float data[100];
とする.
配列変数の定義:
int, float
など).const
指定した変数.(例外あり)
配列変数をプログラム中で参照したり,値を代入するには,以下のように記述する.
基本的には普通の変数と同じである.
#include <stdio.h>
int main(void)
{
int a[100]; // 整数配列
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[1000]; // 実数配列
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;
}
要するに,「配列名」+「添字」で,一つの変数だとみなせば良い.
const
指定について
基本的には,配列の要素数は,変数として用意しておく.
以下の様に,変数 N
は定数でなければならないが,最近の C99 規格に準拠したコンパイラでは,この制限が緩和されている.
ただし,コンパイル時に別途,オプションが必要な場合がある.
#include <stdio.h>
int main(void)
{
// 古いコンパイラでは,
// const int N=100;
// としないとエラー.
int N = 100;
int aaa[N];
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; ではなく i<N; と書くのがベター
printf(" %d 番目のデータは,%d です.\n", i, score[i]); // 全員の点数を画面に表示
}
Cの文法規則では,配列の要素番号は0から始まるので,配列の最後の要素は,score[10]
ではなく,score[9]
である.
したがって,要素数10の配列要素を先頭から順にアクセスするための繰り返し文は,for(i=0; i<10; i++)
となる.
この場合,i
は10になってはいけない.
同様に 100 個の配列の最後の添字は 99 である.
一般に,要素数 N の配列要素の最後の添字は N-1
である.この値を超えてはならない.
以下のソースコードは,初めに示した平均と標準偏差を計算するプログラムを,配列を用いて書き直したバージョンである.
どの箇所が異なるか,比較してみよう.
#include <stdio.h>
#include <math.h> // sqrt()用
int main(void)
{
const int N = 10; // const とは,値を変更できない変数であるという指示
// ここでは,データの個数を表すために使用
float a[N]; // 配列の定義.N個のfloat型変数を記憶する場所が確保される
int i; // 繰り返し処理用のカウンタ変数
for(i=0; i<N; i++) { // N 回繰り返す
printf("a[%d] = ", i);
scanf("%f", &a[i]);
}
// 平均の計算
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);
// 結果の表示
printf("average = %f, standard deviation = %f \n", ave, stdev);
return 0;
}
N
の値を変更してみよう.例えば,3, 5 や,20,100 などとして,うまく処理できるか確認してみよう.
配列では数多くのデータを扱うことが多いので,その値を代入・設定する方法に工夫が必要である.
(実習課題で,以下のように複数のデータを入力させる問題がある場合は,この方法でおこなう.)
実行例:
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++) {
printf("a[%d]=? ", 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 というテキストファイルを作成しておき,ファイル内に書かれたデータを一度に配列に入力させることができる.
1 2 3 4 6
実行方法(リダイレクト入力) .\a.exe < in.txt 1 2 3 4 6 <-実行結果
C言語では,処理効率を優先するため,配列の添字範囲が
したがって,配列の添字が有効範囲を超えてもコンパイラはエラーを出さない.
さらに悪いことに,実行時にエラーとなることもあればエラーとならないこともある.また,一見うまく動作しているように見えても,偶発的にソフトウエアが落ちることがある.
以下のプログラムを実行して,動作を確認せよ.
配列要素を超えるとどうなるか,実行してみよう.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言語では,配列全体を一度にコピーする演算子が無いため,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] というのがアクセス違反
C言語の規格では,配列は(通常の変数と同様)定義しただけでは中身は「不定」と定義されている.
即ち,どのような値が入っているか規格上は決められていない.
したがって,使用方法によっては,あらかじめ 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
配列を引数にとる関数の作り方・使い方は,非常に重要.
関数の引数として配列を渡す場合,配列本体とあわせて,要素数を一緒に渡すのが基本である.
(∵C言語では,配列は自動的に「アドレス渡し」となる仕様で,呼び出された関数には配列の「先頭の場所」しか渡されないため,合わせて配列サイズがわからないと終端が判別できない.)
具体的には,関数の引数に配列を渡す場合は,以下のように記述する.
// 数値の配列を関数に渡す方法
#include <stdio.h>
// 関数で配列を受け取る方法.
// ここで n は配列の要素数
// 配列本体と要素数を引数に取ることにより,どんな要素数の配列にも対応できる.
void disp_array(int data[], int n ) // [] のみでOK.
{
int i;
for(i=0; i<n; i++) {
printf("data[%d] = %d \n", i, data[i] );
}
}
int main(void)
{
const int N = 5; // 配列の要素数
int a[N] = {2, 5, 13, 14, 15}; // 配列
disp_array( a, N ); // 関数呼び出し,配列を渡す
return 0;
}
別の方法として,配列の要素数をグローバル変数として準備しておく方法もある.
ただし,配列の種類や個数が増えてくると,どの変数がどの配列の個数かわかりにくくなるので,いろいろなサイズの配列がある場合は望ましくない.
// 配列を関数に渡す方法.その2
#include <stdio.h>
const int N = 5; // 配列の要素数をグローバル変数として定義
// 配列のみ渡す
void disp_array(int data[])
{
int i;
for(i=0; i<N; i++) {
printf("data[%d] = %d \n", i, data[i] );
}
}
int main(void)
{
int a[N] = {2, 5, 13, 14, 15}; // 数値の配列
disp_array( a ); // 関数呼び出し
return 0;
}
注意:C言語の仕様では,関数に渡す配列は「アドレス渡し」となり,呼び出した関数内で配列の内容を変更すると,呼び出し元の配列の内容も変更される!
const
をつければOKです.void disp_array(const int data[], int N)
以下の問それぞれに対応するプログラムを作成しなさい.
キーボードから整数を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個入力し配列に格納し,平均値を指定の桁数で画面に表示せよ.
可能であれば,配列の合計値(または平均値)を計算する部分は,関数にしてみよう.
ヒント:合計値を記憶しておく変数を別途用意し,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()
を用いる.参考までに,以下の関数 gen_rand()
を用いてよい.
注2:渡された配列内の値の最大値を探索し返す関数を作成する.暫定1位を記憶しておく変数max
を用意する.
#include <stdio.h>
#include <time.h> // time()用
#include <stdlib.h> // rand() 用
//
// 0~100 の乱数を生成して配列に代入する関数
//
void set_rand(int a[], int n) // a はアドレス渡し,nは配列サイズ
{
srand(time(NULL)); // 乱数を初期化
for(int i=0; i<n; i++) {
a[i] = rand()%101; // rand()は呼ばれるたびに整数の乱数を返す.
// printf("%d ", a[i]); // 確認用
}
// printf("\n"); // 確認用
return;
}
//
// 配列の中から最大値を見つけて,その値を返す関数
//
int find_max(...)
{
int max; // ここに最大値を格納して返す.
// 最大値を探す処理
????
return max;
}
int main(void)
{
const int N = 10;
int a[N]; // 整数 N 個分の配列
// データ入力
set_rand(a,N);
// 結果の出力
printf("max = ...", find_max(???) );
return 0;
}
実行例:(乱数の値は毎回異なる.)
55 82 57 62 4 81 19 49 0 26 <- 確認の為の表示
max = 82
配列の数値データを降順(大きい順)に並べ替えて,前後の値を画面に表示せよ.
このような処理を「ソーティング」という.
基本的に二重のループ処理となる.
配列のソーティング部分については関数にしてみよう.
#include <stdio.h>
void sort_array(...)
{
...
}
int main(void)
{
// 配列サイズや値を変更しても正しく動作すること!
const int N = 5;
int a[N] = {3, 2, 5, 4, 1};
// 最初の配列データの出力
printf("Before :");
for(...) {
printf(" ...", ???);
}
// 並べ替え.まずはここでテストして,関数呼び出しに変更する.
for(...) {
for(...) {
...
}
}
// sort_array(...);
// 結果の出力
printf("After :");
for(...) {
printf(" ...", ???);
}
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に対する処理が完了した時点で,配列の先頭から大きい値が順に並んでいる.
また,上記の「大きければ」の比較を「小さければ」にすれば,昇順(小さい順)の並べ替えができる.
このほかにもさまざまなソーティングアルゴリズムがあるので,興味があれば各自で調べてみよう.