2008. 3. 7.
石立 喬

Visual C++ 2008 Express Edition の易しい数値計算(3)

――― 入力した数字(文字列)を数値に変換して統計処理をする ―――

 「コマンドプロンプト」ウインドウから数字(文字列)を入力し、それを数値に変換して、処理する方法を述べる。
数値計算の簡単な例として、統計計算とヒストグラム作成の二種類のプログラムを取り上げる。誤入力に対する配慮や、スペース区切り入力方法も紹介する。

従来のデータ型と.NET Frameworkのデータ型の関係
 キー入力した数字(文字列)を数値に変換するには、.NET FrameworkのCTS(Common Type System、共通型システム)に用意されたメソッドを使用する必要がある。
 C言語やC++言語で使い慣れたintやdoubleのデータ型は、「Visual C++ 2008」でも組み込み型キーワードとして使用され、エディタ上では青色で表示される。一方、.NET FrameworkのCTSでは、名前空間Systemの下に多くのデータ型構造体が用意されている。構造体であるので、メンバーとしてフィールド定数や便利なメソッドを有している。これらは、C/C++型と相互に同等に使用できる。技術計算に関係の深い主なものを表1に示す。Systemは、名前空間に指定しておけば省略できる。

表1 主なデータ型の対応

C/C++型 .NET Framework型 ビット幅
bool System::Boolean 8
byte System::Byte 8
short (int) System::Int16 16
int System::Int32 32
long (int) System::Int64 64
float System::Single 32
double System::Double 64


数字(文字列)を数値に変換するいろいろな方法
 int型の変数をaとして、下記の方法で、入力文字列を数値化でき、計算に供することがきる。
1) ConvertクラスのToInt32(string1)メソッドを使う
    a=Convert::ToInt32(Console::ReadLine());
2) データ型クラスのParse()メソッドを使用する
    int a=int::Parse(Console::ReadLine());
または
    int a=Int32::Parse(Console::ReadLine());

数字(文字列)を一個づつ入力する簡単な統計計算プログラム
 ここで紹介するのは、任意の数の正の整数を入力すると、その入力個数、最大値、最小値、平均値および標準偏差が得られるプログラムである。入力の終了は、負の数を入力する。
◎最大値、最小値を求めるアルゴリズム
 あらかじめ予想される入力の最大値を min_numberに、入力の最小値を max_numberに、それぞれ逆に設定しておく方法を採用した。すなわち、入力を正の整数値と限定しているので、max_number=0; とし、min_number=Int32::MaxValue;とした。
◎標準偏差の求め方
 標準偏差(σ)は、分散(σ2)の平方根から求める。分散の定義は、下記による。ただし、Nは入力したデータの個数であり、xの上にバーが付いたものはxiの平均を意味する。

  

 上の式で、最初の等式が、本来の分散の定義であるが、これは一旦平均を求めてから、さらにもう一度減算と二乗和を繰り返さなくてはいけないので、数値計算には適していない。したがって、実際には、変形によって得た最後の式を使用する。
◎ 数値をあらわす数字(文字列)以外の入力があった場合の処理
 アルファベットなどの入力に対しては、エラーとみなし、メッセージを表示して、再入力を促す。
 try 〜 catch 文を使用し、エラーがあれば continue で while ループの先頭に戻るようにしてある。
◎ 一個だけの数値入力で終了させた場合の対応
 最大最小の処理に
   if(a>max_number) max_number=a;
   else if(a<min_number) min_number=a;
を用いると、一個だけの入力で終了させた場合に、最大値=入力値、最小値=2147483647 のように、不自然な結果が得られる。これを防ぐために、あまりスマートではないが、
   if(a>max_number) max_number=a;
   if(a<min_number) min_number=a;
を用いている。

プログラム

#include "stdafx.h"
using namespace System;

int main()
{

   Console::Title="統計計算";

   int a;
    int sum=0,squared_sum=0;
   int count=0;
   int max_number=0;
   int min_number=Int32::MaxValue;
   float average,sigma;

   while(true){
      Console::Write((count+1) + "番目の入力:");   //キー入力のプロンプト
      try{
         a=Convert::ToInt32(Console::ReadLine());  //入力したものを数値に変換
      }catch(Exception^ ex){
         Console::WriteLine("\n" + ex->Message + "再度入力してください。"); //エラー表示
         continue;                     //再入力へ
      };
      if(a<0) break;               //読み込みを終了させる
      sum+=a;                   //合計の足し込み
      squared_sum+=a*a;             //二乗和の足し込み
      if(a>max_number)  max_number=a;   //最大値の設定
      if(a<min_number)  min_number=a;   //最小値の設定
      count++;                   //入力区個数をインクリメント
   }

   Console::WriteLine();              //改行

   if(count==0){
      Console::WriteLine("入力がありませんでした。");
      return 0;
   }
   else{
      average=(float)sum/count;
      sigma=Math::Sqrt((double)squared_sum/count-average*average);
      Console::WriteLine("入 力 数:" + count);
      Console::WriteLine("最 大 値:" + max_number);
      Console::WriteLine("最 小 値:" + min_number);
      Console::WriteLine("平 均 値:" + average);
      Console::WriteLine("標準偏差:" + sigma);
   }

   Console::ReadLine();
   return 0;
}

得られた結果
 プログラム開始後、データの数字を全く入力しないで、すぐに終了のために-1を入力すると、図1のように表示される。


図1 最初から-1を入力して終了させた場合


 数字を入力しないで「Enter」キーを押したり、数値ではないアルファベットなどを入力したりすると、図2のように、再入力を促される。


図2 「Enter」のみの場合、「a」を入力した場合の例


数字(文字列)をスペース区切りで複数個づつ入力するヒストグラム作成プログラム
 0点から100点までの成績(正の整数)を入力すると、その分布が表示されるプログラムである。入力の終了は、負の数を入力する。
 多数のデータを入力する場合には、一個のデータ入力ごとに「Enter」キーを押すのは面倒であり、「コマンドプロンプト」ウインドウで入力データがスクロールアップして見えなくなる恐れがある。これを解決するため、スペース区切りによって一行に複数個のデータを入力できるようにした。
◎ヒストグラム作成のアルゴリズム
 0点から100点までの成績を10点ごとに10段階に分けるために、counter[0]〜counter[10]を用意し、入力した点数aに対して、counter[a/10]をインクリメントする。100点の場合は90点以上に含めることにしたので、counter[9]とcounter[10]を合計する。
◎配列の準備方法
 従来のC/C++系では、たとえば、int counter[11]; で簡単に配列を宣言できた。.NET Frameworkでは、
    array<int>^ counter=gcnew array<int>(11);
を用いる。gcnew はガーベージコレクション付でメモリ領域を確保することを意味し、deleteなどを使用しないでも、不要になれば自動的に開放され、メモリーリークを防止できるメリットがある。
◎文字列を空白(スペース)で区切り、短い文字列に分解する方法
 文字列string1を空白(スペース)で分解した後の文字列配列をsub_string[]とすると、
    array<String^>^ sub_string=string1->Split(‘ ‘);
で、配列化することができる。配列の個数は、あらかじめ決まってなく、区切りながら作られる。Split()の括弧の中は区切り用の文字(スペース、コンマなどを含む)を指定し、’,’ などに書き換えることもできる。
◎for〜 each 文の使用
 前述のように、配列の個数が明示的でない場合に便利なループ処理に、for〜 each 文がある。これは、配列のほかに、コレクションと呼ばれる一連のデータにも使用できる。
 書式は、
    for each(<型名> <変数名> in <配列名>{
       ---- 繰り返す処理を記述する ----
    }
である。
 ここで紹介するプログラムでは、
    for each(String^ st in sub_string){
       a=Convert::ToInt32(st);
       -----------------------
    }
として使用しているが、これは、
    String^ st;
    for(int i=0;i<sub_string->Length;i++){
       st=sub_string[i];
       a=Convert::ToInt32(st);
       -----------------------
    }
と同じ意味になる。後者の方が分かり易ければ、あえて for〜 each 文を使用する必要はないかも知れない。
◎数字以外のものが誤って入力された場合の処理
 配列sub_stringの中に、整数値に変換できない文字が誤って含まれている場合には、
    a=Convert::ToInt32(st);
がエラーになるので、try 〜catch 文で例外を発生させる。例外発生の原因が’ ‘(スペース)以外の文字の場合には、その代わりの文字を再入力させる必要があるので、例外の標準メッセージの後に、「…を再入力してください。」を追加してある。例外が発生すると、while ループの頭に戻り、次のsub_stringを処理する。例外発生の原因が’ ‘(スペース)にある場合は、それをスキップして次の文字列に移ればよいので、入力の必要はない。

#include "stdafx.h"
using namespace System;

int main()
{

   Console::Title="成績のヒストグラム";

   array<int>^ counter=gcnew array<int>(11);      //ヒストグラムを入れる配列counterを用意する
   String^ string1;
   Boolean flag=false;
   int a;

   Console::WriteLine("点数をスペースで区切って入力して下さい。長くなれば改行可。");

   while(true){
      string1=Console::ReadLine();              //一行分を読み込む
      array<String^>^ sub_string=string1->Split('  ');  //空白で区切って配列sub_stringに入れる
      for each(String^ st in sub_string){           //配列sub_stringを順次処理する
         try{
            a=Convert::ToInt32(st);           //数字(文字列)を数値に変換する
         }catch(Exception^ ex){              //数字以外の入力はエラーにする
            Console::WriteLine(ex->Message + " 「" + st + "」だけ再入力して下さい。");
            continue;
         }
         if(a>100) a=100;                 //100を超える数値入力は100とする
         if(a<0){                       //入力を終了させる
            flag=true;
            break;
         }
         counter[a/10]++;                //該当するカウンタをインクリメントする
      }
      if(flag)  break;
   }

   Console::WriteLine();                    //改行してから結果を表示する
   for(int i=0;i<9;i++){
      string1=String::Format("{0,2:D}〜{1,3:D}点:{2,3:D}",i*10,i*10+9,counter[i]);
      Console::WriteLine(string1);
   }
   string1=String::Format("90〜点:{0,3:D}",counter[9]+counter[10]);
   Console::WriteLine(string1);

   Console::ReadLine();
   return 0;
}

得られた結果
 図3は、わざとアルファベットを入力したり、スペースを2個入れてみたりしながらプログラムを実行させた例を示す。「7a」の代わりに「77」を入れたことを明確にするために、念のために、一個の入力のみで改行してあるが、実際には、改行の必要はない。スペース2個に対しては、再入力の必要がないので、無視して良い。


図3 「a」を入れたり、スペースを余分に入れたりした例


「Visual C++ の勉強部屋」(目次)へ