[前へ]   [目次へ]   [次へ]

配列(C/C++)


計算機を作るにあたり、避けられないものの一つに入力があります。
キーボードからの入力を処理できなければ計算機とは呼べないでしょう。

scanf 関数で直接変数に受ける手もあるのですが、
scanf 関数は予期せぬ入力を受けた時にハマりやすいので、
一度文字列として保存した上で sscanf 関数を使うことにします。
C/C++において文字列と配列は深い関係にあるので、前提として配列から解説します。


配列は、メモリ上に連続して確保される変数の集団です。
配列には通常要素番号を使ってアクセスし、要素番号の指定には変数も使うことが出来ます。

また、C言語には「文字列」を保存できる型が存在しないため、
一般的に char 型、つまり1文字ずつ集めた配列を使って文字列を表現します。
(C++には「文字列クラス」という文字列を表す型がありますが、C++言語でもC言語の文字列は使用されるので、
   C++でやろうという方も避けては通れません)

というわけで、とりわけC言語においては配列は早いうちから必要になります。

さて、配列変数を定義するには、変数名の後に続いて [ ] を書き、その中に要素数(配列に存在する変数の数)を書きます。

PerlやRuby等の言語をやったことがある人は定義する時点で要素数を記述することを不思議に思うかもしれません。
Perl等のスクリプト言語では配列の要素数は状況に応じて増減されますが、
C言語の配列、C++でもC言語と同じ方法で定義した配列は要素数を増減することが出来ません。
また、Perl等の言語にある push や pop 等に相当する関数も存在しません。
(注:C++では「配列クラス」というものが存在していて、
      それなら要素数を増減したり push や pop に相当する関数も存在します)

話が少し逸れましたが、
配列変数を定義する例として、 int 型の要素数が10の変数「 test 」を定義するには次のようにします。

int test[10];

要素数は上の「要素数を増減できない」とも関係ある事情により、
コンパイル時に確定する必要があり、定義する際には定数を使う必要があります。

また、初期化式も各要素に対応するように少し変化します。
初期化式は要素数の後に = を書いた後、 { } を書き、
その中に書く要素の初期値を  ,  区切りで書いていきます。
先に出てきたほう(つまり左側)が若い番号になり、各値は順番に代入されます。
要素数より少なく記述した場合には残りは0で埋められます。

例えば、各要素に1から10までの値を入れる場合は

int test[10]={1,2,3,4,5,6,7,8,9,10};

とします。
この記法は初期化時の特別な表記のため、定義後の変数の代入などではこの記法は使えません。

変数の定義および初期化については「 変数の宣言と定義 」にも記述があります。


配列として定義された変数は型も配列型になっています。
配列型は、代入時や演算時に自動的に変換できる型が対象の型へのポインタ型のみになります。
・・・というより、ほとんどの場合問答無用で変換されます。

つまり、上の「 test 」の型である int[10] から配列要素の型であるint型へのポインタ型 int* に自動的に変換できます。

例外的に配列型は右辺値に同じ配列型をもってきても代入( = )演算子で代入することができません。
左辺と右辺が同じなら代入できても良さそうなものですが・・・できないのです。


さて、配列の各要素にアクセスするには、 [ ] 演算子(配列添字参照)を使います。
[ ]演算子は、左辺値に(配列への)ポインタ型を取り、 [ ] の間に要求する要素番号を取ります。
上述の通り、配列型はポインタ型に変換されるので、配列に使うとポインタ型に変換してから適用されます。
ので、今は普通に配列に使うものと思って構いません。

そして、該当する要素番号の変数への参照(C++の場合)または変数自体(Cの場合)を返します。
要素番号は対象の定義した要素数分まで指定できます。
要素番号の指定は0から始まり、定義した要素数-1までの範囲(0の分があるため)が有効範囲です。

要注意ポイントとして、
要素番号の指定が有効範囲から外れてもコンパイラはエラーも警告も出さない
ということがあります。

以下のソースをビルドしてみると分かると思います。
コンパイラは6行目の明らかな範囲違反にエラーも警告も出さず、すんなり通ってしまいます。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
#include <stdio.h>

int main(void)
{
   
int array[10];//int型で要素数10の配列、arrayを定義
   
array[100]=0;//arrayの有効範囲は0〜9だが、それを外れた100にアクセス
   
return 0;

Visual C++でデバッグ実行したところ、アクセス違反のエラーが発生しました。
アクセス違反というのは、不正な、またはアクセスできないメモリ領域にアクセスすると発生します。
どっちかというと明白に問題を提起してくれるのでデバッグ段階ではありがたかったりします(笑)

デバッガが動いていない場合は強制終了されますが、この場合にも原因を確認することが出来ます。
下の画像は実際に上のソースを実行して強制終了されたところ(Windows XP)です。(一部固有情報は消去しています)
エラー報告
まず、「問題が発生したため・・・」エラーダイアログ(上)が表示された後に詳細の参照をクリックし、
詳細ダイアログ(中)を開き、技術情報をクリックして詳細情報ダイアログ(下)を開きます。
この情報の「Exception Information」のすぐ下の「Code」の値(赤線部)が「0xc0000005」なら
アクセス違反が「発生した問題」ということになります。


このようにコンパイラはエラーを出しませんが、実行すると問題がおきます。
また、アクセス違反によって強制終了されなかった場合、非常に厄介なことになります。
何故なら、有効範囲から外れた場合には、メモリ上で偶然隣り合った領域にアクセスします。
この領域が未使用だった場合は、一見何の問題もなく動きます。
しかし、他の変数の領域だった場合は、その変数の中身を読み書きしたりしてしまいます。
さらに、それが管理情報で書き込んでしまった場合、管理情報が破壊されてその先がどうなるかは不明になります。
(大抵の場合 強制終了されますが、稀にそのまま暴走することがあります)

いずれにしても、大きな問題になります。
一見動いてしまう未使用だった場合も、今回未使用だったとしても
次に実行した時にも未使用である保障はありませんし、
他の変数の中身を書き換えるケースの問題は言うまでもないかと思います。

どちらのケースもすぐに表面化しないケースも少なくないため、再現性の低いバグになったり、
ソースを編集しているうちに突然表面化したりするなど、厄介な現象です。
そのため、有効範囲を外れないことは常に確認しておくようにしておく必要があります。

私は確保した配列要素の最後の1〜2個は通常使わないようにしています。
理由は、有効範囲を外れるケースで一番多いのは、1個はみだすパターンだからで、
1〜2個余計に確保しておけば もし1個はみだしても有効範囲内に収まるからです。
これは私がやっているだけなので一般的にどうかは知りませんが、
結構有効なんじゃないかなぁと思ってます。


次回はC言語の文字列の基本についての予定です。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17