こんにちは、ナナです。
ここまで変数として情報を管理してきました。この記事では新たな情報管理の仕組みとなる「配列」という機能について学びましょう。
本記事では次の疑問点を解消する内容となっています。
では、配列の使い方を学んでいきましょう。
配列変数の定義方法と用語解説
ほい、ほーい。またまた、やってきましたよー。天性の反復使いである僕の出番と聞いて、急いで駆けつけましたよっ!で?なんなんですか?
今回は配列に関する内容だよ。確かに配列と反復処理はセットみたいなものだから、君にうってつけの機能かもしれないね。配列を使うことで同じ系統のデータをまとめて扱うことができるようになるよ!
反復使いの君に配列という武器が新たに加わるってことだね。
配列とは「同じデータ型の変数が複数連続で並んだもの」のことです。その性質上、配列は反復処理とセットで利用されます。
さぁ、それでは配列の変数定義の書き方を知っておきましょう。次のように行います。
定義方法
データ型 変数名[配列要素数];
定義例
char array[5];
これまでの変数定義と異なるのは、変数名のすぐ後に[]を使って個数を指定することです。この数値によってメモリの予約数が変化します。
この配列の数を「配列要素数」、配列1つのデータを「配列要素」と呼びます。
配列の変数名として名付けたラベルは、配列メモリ領域の先頭の場所に貼られます。
そして、配列要素数により「予約済ラベル」が貼られるようなイメージを持つとよいでしょう。別の変数を定義しても、予約済ラベルの場所には貼ることはできないということです。
配列領域のメモリ配置ルール
配列要素は必ず連続したメモリ領域に配置されるというのがC言語のルールです。
このルールは「アラインメント(境界調整)」と呼ばれる知識を理解するうえで重要なものです。
アラインメントについては構造体の記事で別途解説しますよ。このルールがそこで関わってくるんです。皆さんそこまでに覚えていられるでしょうか?
配列の使い方と使うメリット
はい、はーい。配列の作り方はわかりました。で、そろそろ反復王子の僕の出番ですかー?反復したくてうずうずしてまーす。
う、うん(反復王子?)。じゃあ、反復も組み合わせて配列の使い方に入ろうね。配列を使うことで、いったい何が便利になるのかを体験してみよう!
配列の扱い方は少しだけ普通の変数とは異なります。仕組みは簡単ですのでしっかりと習得しましょう。
配列要素へのアクセス方法
配列要素に対してプログラムからアクセスする方法ですが、次のように書きます。
#include <stdio.h>
int main(void)
{
char array[5] = {0, 1, 2, 3, 4};
int index = 3;
array[1] = 10;
array[index] = 30;
return 0;
}
配列要素にアクセスする番号のことを「配列インデックス」と呼び、配列ラベルからの相対距離を示しています。
配列インデックスを使用する時には注意点があります。配列要素数が「5」の場合、配列インデックスで指定できる範囲は0~4となることです。
配列要素数と配列インデックスは別のものなんです。これはものすごく大事なことなんです!
初心者の方はよく間違えて、配列要素数の数を配列インデックスに使って不具合を出します。これは要注意ですよ!
配列を使うメリット
配列を使うといったい何が便利になるのでしょうか。
配列を使う場合と使わない場合で比較して体験してみましょう。次のプログラムは5人の学生のテスト結果に対して、全体の平均点を求めるプログラムです。
配列を使わないプログラムの特徴
全ての学生情報を異なる変数名で定義しています。
また、学生変数を参照する際に個別に名前を逐一書く必要があります。学生が100人いたら大変なことになりそうです。
配列を使ったプログラムの特徴
配列を使う場合、変数定義は配列要素数を指定することで1行で作り出すことが可能です。
また、配列の参照については、for文によるループカウンタで配列インデックスを変化させることで、参照位置を変えることができます。
ほーい。反復処理を使って簡単に情報を管理できたよー。全部僕のおかげだね。僕のことは反復王子って呼んでね!
配列を使うメリット
- 多量の変数の一括定義
- 配列インデックスによる参照変数の変更
配列を使う時に知っておくべき特別なルール
配列と反復、なんてすばらしい組み合わせ。配列王子と反復王子の2冠を獲得しましたよ!殿堂入りしちゃいますよー。
配列王子を名乗りたいんだったら、配列に関する特別なルールをしっかり覚えておこうね。全てを知ってこその配列王子だよ。
配列の初期化方法
通常の変数は次のように定義と同時に初期化することができました。
// 変数の定義と初期化
int num = 0;
配列においても、定義と同時に一括で配列要素を初期化することができます。
次のように{}を使うことで一括、または一部の配列要素を初期化できます。{}が必要になるのが、特徴的ですね。
#include <stdio.h>
int main(void)
{
// 配列の初期化
char array[5] = {0, 1, 2, 3, 4};
int i;
for (i=0 ; i < 5 ; i++)
{
printf("%d\n",array[i]);
}
return 0;
}
この書き方は定義と同時に行う初期化だからこそ書ける書き方です。代入処理においては書くことはできません。
次の書き方はビルドエラーになることに注意しましょう。
#include <stdio.h>
int main(void)
{
char array[5];
// ビルドエラー発生
array = { 0, 1, 2, 3, 4 };
// ビルドエラー発生
array[5] = { 0, 1, 2, 3, 4 };
return 0;
}
初期化と代入の区別がついていない人は、この違いが理解できません。初期化と代入は違う!これがC言語のルールなんです。しっかりと意識できるようになりましょう。
初期化による配列要素数の省略
配列を初期化した場合に限り、配列要素数の記載を省略することが認められています。
配列要素数は初期化項目の数から特定することができるため、省略してもよいということです。
#include <stdio.h>
int main(void)
{
// 配列要素数5の記載を省略
char array[] = { 0, 1, 2, 3, 4 };
return 0;
}
配列要素数がよく変化するようなシステムの場合は、要素数と初期化項目を両方メンテナンスする必要があるため、意図的に要素数の記載を省略することでメンテナンス工数を小さくすることがあります。
もう一度言いますが、次のように初期化していない場合は配列要素数は省略できません。
#include <stdio.h>
int main(void)
{
// 初期化していない配列要素数の省略
// ビルドエラーが発生!
char array[];
return 0;
}
なぜ、この書き方が認められていないかをしっかりと考えることです。
初期化もしてなくて、要素数まで省略されたらメモリにラベルを貼る範囲が特定できないんです。だからエラーが発生するんです。
配列の0初期化
変数の値を初期化する際に値をとりあえず、0の値でクリアしておくことはよく行われる初期化処理です。やはり、0という数字を基準にしたいと皆が思うんです。
それでは皆さん、次のプログラムを書いてみてarray1~array4の各5つの配列要素が、どのような値として初期化されるのかを動かして確認してみてください。
#include <stdio.h>
int main(void)
{
char array1[5];
char array2[5] = { 1, 2, 3, 4, 5 };
char array3[5] = { 1, 2, 3 };
char array4[5] = { 1 };
return 0;
}
動かしてみた方はわかったでしょう。
配列は一部のデータを初期化すると、残りの配列要素は0で初期化されます。
このテクニックを知っていると配列要素を0初期化したい場合はarray4の初期化方法で簡単にできます。
0初期化をしたい場合のテクニックですね。私はよく利用します。
配列範囲外へのアクセスは絶対しちゃダメ
配列では配列インデックスで指定した数次第で予約したメモリ領域の範囲外にアクセスすることが可能です。
もしも、範囲外のメモリにアクセスしてしまった場合いったいどうなるのでしょうか?
配列インデックスの指定は範囲外であってもビルドエラーにはならないため注意が必要です。予約済みのラベルが貼られていない場所でも配列インデックス次第でアクセスが可能です。
この配列範囲外へのアクセスのことを「配列のオーバーラン」と呼びます。
オーバーランは非常に危険なアクセスであり、ソフトウェア上どのような動作が起こるかわかりません。システムがハングアップするかもしれませんし、運よく何も起きないかもしれません。
配列オーバーランは、プログラム初心者がよくやる不具合の1つなんです。配列を使うときには参照範囲が超えていないかを入念にチェックしましょう!
2次元配列の使い方
配列には亜種が存在すると噂されています。この配列王子が成敗してやろうとおもってるんですよー。で、配列の亜種ってなんですか?
亜種?2次元配列のことかな。まぁ配列の仲間だから成敗する必要はないよね。配列王子なら2次元配列もしっかりと扱ってよね。
通常の配列は横並びのデータイメージであり、1次元配列と表現します。
これに対し2次元配列とは、縦横に並んだデータイメージのことを言います。2次元配列のデータイメージは、Excelの表形式を思い浮かべるとわかりやすいでしょう。
2次元配列の定義方法
2次元配列は次のように定義することができ、配列と同様に初期化を行うことも可能です。
定義方法
データ型 変数名[行の配列要素数][列の配列要素数];
使用方法
char array[3][5];
次のプログラムは二次元配列の定義例と初期化例です。
#include <stdio.h>
int main(void)
{
char array2D[3][5] =
{
{56, 1,45,82,29},
{43,99,51,71, 7},
{64,96,33, 0,49},
};
return 0;
}
2次元配列の初期化は{}の中に{}がさらに必要になるから気を付けてください。1行のデータ{}で囲んでいるんです。
2次元配列の配列要素へのアクセス方法
通常の配列と同じように行と列の配列インデックスを指定することで配列要素へアクセスすることができます。
配列インデックスは要素数-1までの範囲であることは同じであり注意が必要です。
これってどっちの数字が行で列か、わからなくなるんですよね。「行列」という言葉がありますね。だから、[行][列]って覚えるといいですよ。
配列データのメモリサイズ算出方法
配列要素を初期化して、配列要素数の指定を省略したんです。そしたら配列要素数がわからないじゃないですか!
配列要素数がわからなかったら、何回反復したらいいかわからないんですよ!反復王子たる僕が反復回数がわからないんて屈辱ですっ。もうだめだ…反復王子とか辞めよう…、ぐすん(泣)。
落ち込むねー。毎度だけどねー。
確かに配列要素数を省略したのはいいけど、実際のデータを処理する際には配列範囲をループするため、配列要素数が必要になっちゃうんだよね。
そんな時に使えるテクニックを教えちゃうよ。
メモリサイズの算出方法は配列要素数の算出と関係性があるため、この章で学んでおきましょう。
sizeof演算子とは
sizeof演算子はデータ型や変数のメモリサイズを算出するための演算子です。
書き方
sizeof(データ型);
sizeof(変数名);
使用例
sizeof(long);
sizeof(num);
見た目が関数呼び出しと同じため、関数と勘違いしている人もいますが、sizeofはあくまでも演算子であり関数ではありません。
実際に使って覚えていきましょう。次のプログラムを皆さん動かしてみてください。データ型や変数名をいろいろ変更してみるのもよいでしょう。
#include <stdio.h>
int main(void)
{
long num;
char tmp;
// 4が表示
printf("%d\n", sizeof(long));
printf("%d\n", sizeof(num));
// 1が表示
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(tmp));
return 0;
}
このようにsizeof演算子によって、データ型や変数のメモリサイズを算出することが可能です。
「データ型のサイズなんて調べなくても知ってるよ」と思うかもしれません。しかし、sizeof演算子の使いどころはいろいろとあるのです。
まずは、配列でよく使われるsizeofのテクニックを紹介しましょう。
sizeof演算子を使った配列要素数の算出
配列を初期化することで、配列要素数の指定が省略できると説明しました。
しかし、配列要素数がわからないと配列インデックスによるループ回数が決められません。
メンテナンス性を上げようとして配列要素数を省略したのに、結局は要素数を指定しなければならないというジレンマが発生するのです。プログラムで表すと次のような例です。
#include <stdio.h>
int main(void)
{
// 配列要素数を省略
char array[] = { 10, 20, 30, 40, 50 };
int i;
// 配列要素数の5を結局指定…
for (i=0 ; i < 5 ; i++)
{
printf("%d\n", array[i]);
}
return 0;
}
この問題はsizeof演算子を使うことで解決します。次のプログラムに置き換えることができます。
#include <stdio.h>
int main(void)
{
// 配列要素数を省略
long array[] = { 10, 20, 30, 40, 50 };
int i;
// sizeof演算子を使った配列要素数の算出
for (i=0 ; i < sizeof(array)/sizeof(array[0]) ; i++)
{
printf("%d\n", array[i]);
}
return 0;
}
配列要素数の5を sizeof(array)/sizeof(array[0]) というプログラムで代用しています。
なぜ、このプログラムが配列要素数の代わりになるのかを説明しましょう。
sizeof(array)で得られる数値
sizeof演算子に配列の変数名を指定した場合は、配列全体のメモリ使用バイト数が得られます。
つまり、long型サイズの4バイト × 配列要素数5個 = 合計20バイト、が得られます。
sizeof(array[0])で得られる数値
sizeof演算子に配列の要素を指定した場合は、要素1つ分のメモリ使用バイト数が得られます。
つまり、long型のサイズ4バイト × 要素数1個 = 合計4Byte、が得られます。
sizeof(array) / sizeof(array[0]) で得られる数値
結果、20バイト ÷ 4バイト = 5個となり配列要素数が算出できます。
このようにsizeof演算子を使うことで配列要素数を算出することができるのです。
この配列要素数を算出するテクニックは結構有名です。C言語の開発者であれば多くの方が知っています。皆さんも身につけておきましょう。
Q&A:配列に関するよくある質問
配列に関して質問していいよ。配列王子もどうぞー。
配列と反復処理を組み合わせたプログラムの書き方のセオリーってあるの?
反復王子と配列王子、この2つ名を持つにふさわしいプログラムの書き方を知りたいんです。
配列インデックスをループカウンタで参照する方法には一番基本となる形があるよ。この基本をベースにケースバイケースで変えていくとよいでしょう。
配列を反復処理で参照する一番基本となる書き方は次の形です。
#include <stdio.h>
int main(void)
{
long num[10];
long i;
// ループカウンタを0で初期化
// カウンタ < 配列要素数 で条件設定
for (i = 0; i < 10; i++)
{
// iは0~9でループする
num[i] = i;
}
return 0;
}
配列を参照する時のfor文の基本形がこの書き方です。形で覚えましょう。
配列要素数と配列インデックスでは共に[]を使って表現するのに、使い方がなんで違うの?
はーい、質問です。配列を使う時に[]を付けますけど要素数なのか配列インデックスなのか、わかんなくなっちゃいました。これって同じなんですか?
何度も言うけど、配列要素数と配列インデックスは別の情報だから扱う時は注意が必要だよ。共に[]を使って表現するけど、記号が同じだけで別のものとして扱ってね。
「定義と処理で記号の意味が変わる」ということを理解しましょう。
プログラミング言語というのは限られた記号を使って処理を記述するわけです。
皆さんキーボードを見てください。$&@+*など様々な記号がありますが、案外使える選択肢は少ないです。そのため記号の使い回しということが発生します。
#include <stdio.h>
int main(void)
{
// ①:要素数を指定する[]
char array[5];
// ②:配列要素を参照するための[]
array[2] = 20;
return 0;
}
[数値]という書き方は同じですが、①と②はそもそも別の用途としての記号なのです。
このように、記号は同じでも文脈によって意味が異なるということが、プログラミング言語にはよくあります。
①はデータ型を伴いますが、②はデータ型がありませんね。このような文脈から「この記号の意味はこれだ」と判断できる力が必要ということです。
実は、②の[]は添字演算子と呼ばれる演算子なんです。①は添字演算子とは呼ばないんです。だから、別の記号なんです。
2次元配列があるということは、3次元・4次元配列なんてものもあるの?
ほい、はーい。今はまだ2次元の世界にいる僕ですが、そろそろ3次元の世界にも進出したいんでーす。配列にも3次元なんてあるんですか?
あるよっ!C言語的には3次元でも4次元でも配列を作ることが可能だよ。
C言語で作る配列には3次元配列も4次元配列も認められています。
#include <stdio.h>
int main(void)
{
// 三次元配列
long array3D[3][4][5];
// 四次元配列
long array4D[3][4][5][6];
return 0;
}
実際に使うかというと、三次元配列を実際の開発の中で見たことはありません。
課題:配列の使い方を学べたかを確認しよう
課題1
課題内容
次のプログラムのコメントで囲まれた中に、配列の中の最大値を検索し表示するプログラムを書け。入力1と入力2を切り替えるだけで最大値の表示が切り替わるようにすること。
#include <stdio.h>
int main(void)
{
// 入力1
int num[] = { 10, 3002, 529, 1920 };
// 入力2
// int num[] = { 918, -792, 1002, 209, 652 , 0x491};
//-----------------------------
// 配列の中の最大値を検索し表示
// ↓
// ↑
//-----------------------------
return 0;
}
出力期待結果
入力1のとき
3002
入力2のとき
1169
main.c
#include <stdio.h>
int main(void)
{
// 入力1
int num[] = { 10, 3002, 529, 1920 };
// 入力2
// int num[] = { 918, -792, 1002, 209, 652 , 0x491};
//-----------------------------
// 配列の中の最大値を検索し表示
// ↓
int i;
int max = num[0];
for (i=1 ; i < sizeof(num)/sizeof(num[0]) ; i++)
{
// 大きいものが見つかれば変更
if (max < num[i])
{
max = num[i];
}
}
printf("%d", max);
// ↑
//-----------------------------
return 0;
}
反復処理を使って順に一番大きな数字を検索してますね。ここまでに学んだ知識を忘れずに活用してください。
課題2
課題内容
次のプログラムのコメントで囲まれた中に、2次元配列numに九九の値を格納するプログラムを書け。出力期待結果に従い九九が表示されることを確認せよ。
#include <stdio.h>
int main(void)
{
int num[9][9] = {0};
int i, k;
//----------------------------------------------
// num[9][9]の中に九九の値を2次元配列で格納せよ
// ↓
// ↑
//----------------------------------------------
for (i = 0 ; i < 9 ; i++)
{
for (k = 0 ; k < 9 ; k++)
{
printf("%2d ", num[i][k]);
}
printf("\n");
}
return 0;
}
出力期待結果
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
main.c
#include <stdio.h>
int main(void)
{
int num[9][9] = {0};
int i, k;
//----------------------------------------------
// num[9][9]の中に九九の値を2次元配列で格納せよ
// ↓
for (i = 0 ; i < 9 ; i++)
{
for (k = 0 ; k < 9 ; k++)
{
num[i][k] = (i + 1) * (k + 1);
}
}
// ↑
//----------------------------------------------
for (i = 0 ; i < 9 ; i++)
{
for (k = 0 ; k < 9 ; k++)
{
printf("%2d ", num[i][k]);
}
printf("\n");
}
return 0;
}
九九は1~9までなんですが、配列インデックスは0~8までですね。この違いをうまく調整してあげる必要がありますよ。
課題3
課題内容
次のプログラムのコメントで囲まれた中に、配列numを昇順に並べ替えるプログラムを書け。出力期待結果に従い昇順の結果が表示されることを確認せよ。
入力1と入力2を切り替えるだけで表示結果が切り替わるようにすること。
#include <stdio.h>
int main(void)
{
// 入力1
int num[] = {762, -98, 128, 8, 378};
// 入力2
// int num[] = {89 , 1076, -102, -409, 2034, 489, 26 };
int i;
//-------------------------------------------
// num配列の中身を昇順に並べ替えよ
// ↓
// ↑
//-------------------------------------------
for (i=0 ; i < sizeof(num)/sizeof(num[0]) ; i++)
{
printf("%d\n", num[i]);
}
return 0;
}
出力期待結果
入力1のとき
-98
8
128
378
762
入力2のとき
-409
-102
26
89
489
1076
2034
main.c
#include <stdio.h>
int main(void)
{
// 入力1
int num[] = {762, -98, 128, 8, 378};
// 入力2
// int num[] = {89 , 1076, -102, -409, 2034, 489, 26 };
int i;
//-------------------------------------------
// num配列の中身を昇順に並べ替えよ
// ↓
int k;
int tmp;
for (i = 0 ; i < sizeof(num) / sizeof(num[0]) ; i++)
{
for (k = i + 1 ; k < sizeof(num) / sizeof(num[0]) ; k++)
{
if (num[i] > num[k])
{
tmp = num[i];
num[i] = num[k];
num[k] = tmp;
}
}
}
// ↑
//-------------------------------------------
for (i=0 ; i < sizeof(num)/sizeof(num[0]) ; i++)
{
printf("%d\n", num[i]);
}
return 0;
}
並べ替えの方法はいろいろありますが、一番有名なバブルソートと呼ばれるのがこの解答です。
2つの変数の中身を入れ替えるときは、いったん別の変数に移し替えてから入れ替えるんですよ。