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

配列(2)と式の解釈(3)(C/C++)


それでは、四則演算版計算機の演算種別を数値ではなく文字で入力するように改造していきます。

そのためには、 fgets 関数で取得した文字列から文字を取り出さなければなりません。
文字列は配列であり、文字を取り出すには配列を扱う必要があります。

「配列(C/C++)」 での解説では配列操作には不十分だと思うので、
今回は配列の使い方について細かく書きます。
「配列(C/C++)」 からの続きになりますので、忘れたって人は先に読んできてください。

さて、配列は「同じ型の変数を一度に宣言した」ことと同義です。
つまり、 int 型の要素数10の配列を定義することは、 int 型の変数を10個定義することと同じです。
配列として一度に宣言した変数はそのままだとみんな同じ名前になってしまうので、要素番号が与えられます。

前にも書いたとおり、要素番号は0から始まり、要素数-1 までの値が順番に割り当てられます。
要素番号でアクセスするには [ ] 演算子を用い、
要素番号が無効な値を指していてもコンパイラは指摘してくれません。

このように文章だけ書いていても分かりづらいので、実際に動かしてみましょう。
以下の二つのプログラムの実行結果は全く同じになります。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
#include <stdio.h>

int main(void)
{
   
int n1,n2,n3,n4,n5,n6,n7,n8,n9,n10;//int型の変数を10個定義する
   //各変数に値を設定
   
n1=10;
   n2=20;
   n3=30;
   n4=40;
   n5=50;
   n6=60;
   n7=70;
   n8=80;
   n9=90;
   n10=100;
   
//全部出力
   
printf(" n1=%d\n",n1);
   printf(
" n2=%d\n",n2);
   printf(
" n3=%d\n",n3);
   printf(
" n4=%d\n",n4);
   printf(
" n5=%d\n",n5);
   printf(
" n6=%d\n",n6);
   printf(
" n7=%d\n",n7);
   printf(
" n8=%d\n",n8);
   printf(
" n9=%d\n",n9);
   printf(
"n10=%d\n",n10);
   
//終了待ち
   
getchar();
   
return 0;

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
#include <stdio.h>

int main(void)
{
   
int n[10];//int型の要素数10の配列を定義する
   //各変数に値を設定
   
n[0]=10;
   n[1]=20;
   n[2]=30;
   n[3]=40;
   n[4]=50;
   n[5]=60;
   n[6]=70;
   n[7]=80;
   n[8]=90;
   n[9]=100;
   
//全部出力
   
printf(" n1=%d\n",n[0]);
   printf(
" n2=%d\n",n[1]);
   printf(
" n3=%d\n",n[2]);
   printf(
" n4=%d\n",n[3]);
   printf(
" n5=%d\n",n[4]);
   printf(
" n6=%d\n",n[5]);
   printf(
" n7=%d\n",n[6]);
   printf(
" n8=%d\n",n[7]);
   printf(
" n9=%d\n",n[8]);
   printf(
"n10=%d\n",n[9]);
   
//終了待ち
   
getchar();
   
return 0;

実行結果:
 n1=10
 n2=20
 n3=30
 n4=40
 n5=50
 n6=60
 n7=70
 n8=80
 n9=90
n10=100


配列が変数をまとめて定義しているという意味が分かったでしょうか?
n1 〜 n10 といった変数をいっぱい定義する代わりに、
n[10] という変数を一個定義しただけで10個分使えるようになりました。
メモリももちろん10個分消費していますので、数千万個の要素数で定義しようとすると膨大なメモリを消費します。
(実際にはそんなに確保する前にメモリ不足で落ちますけどね(笑))
(1000万=10Mなら、1GBぐらいメモリ積んでれば確保できるでしょ?と思った方。
実はローカル変数用に確保されるメモリは意外と少ないので、ローカル変数としては確保できないのです。
これはVisual C++では初期状態で1MBなので、それ以上確保しようとすると「stack overflow」というエラーで停止します)


しかし、これでは定義は短くなったもののあまり変化がありませんね。
これでは配列を使う価値というか、真価が分かりません。

では少し仕様を変更して、全ての変数の値を表示するのではなく、
入力した番号ひとつの変数を表示するようにしてみようと思います。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
< 55>
< 56>
< 57>
< 58>
< 59>
< 60>
< 61>
< 62>
#include <stdio.h>

int main(void)
{
   
char buf[64]="";//文字列入力用
   
int in=0;//入力受け取り用
   
int n1,n2,n3,n4,n5,n6,n7,n8,n9,n10;//int型の変数を10個定義する
   //各変数に値を設定
   
n1=10;
   n2=20;
   n3=30;
   n4=40;
   n5=50;
   n6=60;
   n7=70;
   n8=80;
   n9=90;
   n10=100;
   
//番号を入力
   
printf("表示したい番号を入力(0〜9):");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in);
   
//指定番号を表示してみる
   
switch(in){
   
case 0:
      printf(
"n1=%d\n",n1);
      
break;
   
case 1:
      printf(
"n2=%d\n",n2);
      
break;
   
case 2:
      printf(
"n3=%d\n",n3);
      
break;
   
case 3:
      printf(
"n4=%d\n",n4);
      
break;
   
case 4:
      printf(
"n5=%d\n",n5);
      
break;
   
case 5:
      printf(
"n6=%d\n",n6);
      
break;
   
case 6:
      printf(
"n7=%d\n",n7);
      
break;
   
case 7:
      printf(
"n8=%d\n",n8);
      
break;
   
case 8:
      printf(
"n9=%d\n",n9);
      
break;
   
case 9:
      printf(
"n10=%d\n",n10);
      
break;
   
default:
      printf(
"入力された値が無効です。\n");
      
break;
   }
   
//終了待ち
   
getchar();
   
return 0;
 
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
#include <stdio.h>

int main(void)
{
   
char buf[64]="";//文字列入力用
   
int in=0;//入力受け取り用
   
int n[10];//int型の要素数10の配列を定義する
   //各変数に値を設定
   
n[0]=10;
   n[1]=20;
   n[2]=30;
   n[3]=40;
   n[4]=50;
   n[5]=60;
   n[6]=70;
   n[7]=80;
   n[8]=90;
   n[9]=100;
   
//番号を入力
   
printf("表示したい番号を入力(0〜9):");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in);
   
//指定番号を表示してみる
   
if((in>=0)&&(in<10)){//inが有効な番号であることを確認(1)
      
printf("n%d=%d\n",in+1,n[in]);//番号を表示(2)
   }
   
else{//inが有効じゃない(3)
      
printf("入力された値が無効です。\n");
   }
   
//終了待ち
   
getchar();
   
return 0;

実行例:
表示したい番号を入力(0〜9):5
n6=60

今度は配列を使った方のソースが非常に短くなっています。
個別に定義した方は switch を用いて全てのパターンに一々処理を書いています。
しかし配列を使った方はその部分は数行で終わっています。
詳しく見ていきます。

まず、24行目(1)の部分。
この条件式は、「 in が0以上」と「 in が10より小さい」のAND(両成立)条件です。
この条件を通過できるのは in が0〜9の範囲だけです。
これをしておかないと、配列の有効範囲外の値も入れられることになってしまいます。

次に、25行目(2)の部分ですが、これまでの printf 関数とはちょっと違うように見えます。
この printf 関数には引数を3つ渡しているのです。
また、最初は流してしまいましたが第2引数以降は式が渡されています。

実は printf 関数は複数の変数を一度に書式化できるのです。
しかし、その代償としてコンパイラの型チェックや暗黙の型変換が効いてくれません。
これは scanf 系関数も同様です。
そのため、 printf 関数や scanf 系関数ではプログラマがしっかり型を管理しなければならないのです。

それでは、上記の入力例の時どうなっているのか細かく見てみましょう。
A      B C            D  E F   G H I  H' B'
printf ( "n%d=%d\n" , in + 1 , n [ in ]  )


通し記号種別
Aint(*)(const char*,...)0x004013F0関数(printf)
B演算子( () )(その他、優先16、結合→)
Cconst char[8]"n%d=%d\n"文字列定数
Dint5変数(in)
E演算子(+)(2項、算術、優先12、結合→)
Fconst int1定数
Gint[10](省略)変数(n)
H演算子( [] )(その他、優先16、結合→)
Iint5変数(in)
H'演算子Hの終点
B'演算子Bの終点
多いですねぇ(笑)。
さらに何かすごい型名が出てきたりしています(笑)。

関数A のこの型は「関数ポインタ」というC言語の最後辺りに出てくるようなものです。
「ポインタ」の一種なのでこれまた必ず同じ値が出るわけではありません。
(ただし変数のように増えたり減ったりするようなものではないので基本的にビルド時には確定されます)
現時点でこれは続く 演算子B(関数呼び出し) の左辺値に必要な型というだけでOKです。
いや、全く覚えてなくたって当分はどうにでもなりますのでご安心を(笑)。


演算子B( () )は、「関数呼び出し演算子」です。
左辺値に関数ポインタ、右辺値(中辺値?)に関数の引数を与えます。
この演算子に渡す右辺値(中辺値?)は、対応する ) が見つかるまで、カンマ区切りでいくつでも渡せます。
この場合は左辺値は 関数A、右辺値は 文字列定数C、D〜Fの式、G〜Iの式の3つです。

関数呼び出し演算子は、左辺値の関数に右辺値を引数として呼び出します。
この時、関数の宣言時の引数型を元に型チェックと暗黙の型変換(代入時バージョン)が作動しますが、
printf 関数は第2引数以降が「以下略」されていて「何個でも、どんな型でも渡せる」ことになっています。
そのため、第2引数以降では型チェックも型変換も効かないんです。


演算子H( [ ] )は、以前にも触れましたが「配列添字参照演算子」です。
左辺値に(配列への)ポインタ、右辺値(中辺値?)に参照する要素番号を与えます。
この演算子も右辺値(中辺値?)は、対応する ] が見つかるまでで、式を与えることもできます。

配列添字参照演算子は、左辺値の配列の右辺値の番号の要素への参照を返します。
繰り返しになりますがこの時、配列の範囲チェックはしてくれません。
有効な要素番号を指定していることはプログラマが保証しなければなりません。


それでは、処理を進めてみようと思います。
まず、優先順位および結合規則から演算子Bからです・・・が、右辺値にD〜FとG〜Iという式があります。
式のままでは引数として渡せないので、これらの式を先に処理します。

実は、ここでどちらの式から処理されるかは未定義(コンパイラ任せ)だったりします。
そのため同じ変数が複数の式に含まれる場合は、式中で変数を変更してしまうと、予期せぬ結果になることがあります。
片方の式の結果がもう片方の結果にも影響するような場合、
一度別の変数に代入するなどして式を事前に処理しておき、このタイミングで処理しないようにしてください。
今回の例では両方に変数 in が出現しますが、どちらも変更はしていないので問題ありません。

未定義なので必ずという訳ではありませんが、当講座では左から右に処理することにします。
という訳で、まずD〜Fの式から処理します。
D  E F
in + 1


通し記号種別
Dint5変数(in)
E演算子(+)(2項、算術、優先12、結合→)
Fconst int1定数
これまでも出てきた形なので、サクッといきます。
演算子E(+)、左辺D、右辺Fで答えは6、一時変数(vtemp1)に格納されて終了です。

次、G〜Iの式です。
G H I  H'
n [ in ]


通し記号種別
Gint[10](省略)変数(n)
H演算子( [] )(その他、優先16、結合→)
Iint5変数(in)
H'演算子Hの終点
演算子は一つなので演算子H( [ ] )、左辺G、右辺I の処理です。
今回は、 n の要素番号5の要素への参照を返します。

よって・・・
A
vtemp2


通し記号種別
Aint&60一時変数(参照先:n[5])
となります。


これで二つの式の処理が完了しました。
という訳で最初の式に戻ります。
A      B C            D        E      B'
printf ( "n%d=%d\n" , vtemp1 , vtemp2 )


通し記号種別
Aint(*)(const char*,...)0x004013F0関数(printf)
B演算子( () )(その他、優先16、結合→)
Cconst char[8]"n%d=%d\n"文字列定数
Dint6一時変数
Eint&60一時変数(参照先:n[5])
B'演算子Bの終点
これでやっと演算子Bの処理ができます。
左辺に関数A、右辺に文字列定数C、値D、値Eを持って関数を呼び出します。
この関数の中で画面に出力する処理が行われて画面に文字列が表示されます。

そして、 printf 関数は int 型の値を返すので・・・
A
vtemp3


通し記号種別
Aint6一時変数
となります。
この値は使われていないので破棄されます。



27行目(3)・・・(2)がめちゃくちゃ長かったですが、続けます。
(何やってたか覚えてますか?忘れた人は 上に戻って 思い出してください(笑))
この else は24行目(1)に対する else なので、範囲外の値を入力された時に実行されます。
エラーメッセージを出しているだけですが、ここで何もしないとそのまま終了待ちになってしまうので、
見た目上なんだかよく分からない状態になってしまいます。
こういう状態を作るのは使う側からしてもデバッグする側からしても鬱陶しいので
なんでもいいからメッセージを表示するなどして状態を明示した方がいいです。


次回は文字列から文字を取りだしてみます。

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

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

最終更新 2008/10/17