第四回課題解説

第四回課題pの解説を行う。

典型的な間違い例もあげるので、自分の理解に不備がないかチェックして欲しい。


1:配列の動的確保

一番ということで、ほとんどの人がそこそこ出来ていたが、完璧な解答は多くなかった。

「プログラム実行中に配列のサイズを決める」というのは、プログラムを書き出すと すぐに必要と感じる機能の一つではないかと思う。
卒論などでプログラムを書く必要がある人は覚えておくと役にたつかもしれない。

まず、

int n;
double x[n];


ような宣言をやりたくなるかもしれないが、これではコンパイルが通らない。
なぜなら、C/C++ では配列のサイズはコンパイル時に確定していなければならないからである。

次にやりたくなるのは「 x[1000] などの大きな配列を用意しておいて、 先頭の数十要素のみを使う」ことであるが (そういう解答をしている人もいた)、
これも良くない。要素数 1000 以上の配列を使わないという保証はどこにもないからである。

結局、以下の解答のように、ポインタと new 演算子を用いるのが定石である。 重要な3行に () をつけた。

#include <iostream>
using namespace std;

double average(double *x, int n);

int main(){

  double *x;    // () 
  int n;

  cout << "配列のサイズ?";
  cin >> n;

  x = new double[n];     // () 

  for(int i=0 ; i<n ; i++){
    cout << i << "番目の要素?";
    cin >> x[i];
  }

  cout << "平均は" << average(x,n) << "です。\n";

  delete[] x;    // () 

  return 0;
}

double average(double *x, int n){

  double sum=0;

  for(int i=0 ; i<n ; i++){
    sum = sum + x[i];
  }

  sum = sum/n;

  return sum;
}


「delete[] x;」がない人がかなり多かった。
new で確保した領域は delete で、 new …[] で確保した領域は delete[] で削除する」癖をつけよう。

なお、delete[] p; とすべきところで delete p; してしまうと何が起こるかであるが、
C++ の仕様上は不定、つまり、何が起こるかわからない。
# 典型的には「p[0] のみが解放され、p[1]〜p[n-1] は解放されず残る」ことが予想される。

また、関数を用いることができない人も多かった。
ここで関数を使わせたのは、関数に配列へのポインタを渡す方法を思い出して欲しかったからである。 (参考:第六回-01)

上のような関数を指して「(C 言語的な) 関数」と呼んだのであるが、その意味がわからず、
クラスを使っている解答もいくつかあった。クラスを使った場合の解答例は以下のようになるだろうか。
new/delete がそれぞれコンストラクタ、デストラクタに入るため、 main 関数でので delete し忘れを心配する必要がなくなるのがメリットである。
ただし、上の解答例よりは若干記述量が多くなる。

#include <iostream>
using namespace std;

class samp{

 private:
   double *x;
   int n;
 public: 
   samp(int i){
     n=i; 
     x = new double[n];
   }

   ~samp(){ delete[] x;}

   void set_x(int i, double d){ x[i] = d;}

   double average(){
     double sum=0;
     for(int i=0 ; i<n ; i++){
       sum = sum + x[i];
     }
     sum = sum/n;

     return sum;
   }
};
   
int main(){

  int n;
  double d;

  cout << "配列のサイズ?";
  cin >> n;

  samp ob(n);

  for(int i=0 ; i<n ; i++){
    cout << i << "番目の要素?";
    cin >> d;
    ob.set_x(i,d);
  }

  cout << "平均は" << ob.average() << "です。\n";

  return 0;
}


間違い例は以下の通り。

まず、これは間違いとまでは言えないのだが、「ポインタ x と整数 n を main 関数の外で定義し、グローバル変数とした」例である。
こうすると、これらの変数はどこからでも用いられるため、関数に引数を渡す必要がなくなる。

#include <iostream>
using namespace std;

double average();

double *x;
int n;

int main(){

(以下略)


しかし、一般に、C/C++ ではグローバル変数は使わないほうが良いと言われている。
(変数が多くなると、管理が大変になるため)。
解答例のように、関数に配列へのポインタを渡す方法を身につけて欲しい。

他には、 など例があった。
とくに2つ目は致命的な間違いで、 ob = new samp[size]; を確保した後は、ob[0]〜ob[size-1] までしか利用できないことをまず理解すべきである。


2:queue について

(1) array クラスの持つ変数を char 型に変更

記述した人はここは大体できていた。

解答は以下のとおり。赤色の部分が int から char に変更されている。
このように、クラスのあるメンバ変数の型を変更するのは面倒である。
この面倒さを解消するため、C++ ではテンプレートという仕組みを用意している。

テンプレートを使うと、ここでの int や char の替わりに抽象的なクラス T を用いて 記述し、array クラスを呼び出すときに int か char かを決めることができるようになる。
教科書に載っているので興味のある人は自習してみるとよいだろう。

#include <iostream>
#include <cstdlib>  // for exit(1)
using namespace std;

class array{   // クラス宣言
    char *p;   // 配列の先頭を指すポインタ
    int size;   
  public:
    array();                // デフォルトコンストラクタ
    array(int sz);          // 要素数を指定するコンストラクタ
    array(const array &a);  // コピーコンストラクタ

    ~array(){ delete[] p;}  // デストラクタ
    array &operator=(const array &a); 
    char &operator[](int i); // [] 演算子の多重定義
    
    int getsize(){ return size; }  // 配列のサイズ取得

};

array::array(){  // デフォルトコンストラクタ
      p = new char[10];
      if(!p) exit(1);
      size = 10;

      cout << "デフォルトコンストラクタ\n";
}

array::array(int sz){  // 要素数を指定するコンストラクタ
      p = new char[sz];
      if(!p) exit(1);
      size = sz;

      cout << "要素数を指定するコンストラクタ\n";
}

array::array(const array &a){   // コピーコンストラクタ
  size = a.size;

  p = new char[size];

  if(!p) exit(1);

  for(int i=0 ; i<a.size ; i++) p[i] = a.p[i];  // 内容をコピー

  cout << "コピーコンストラクタ\n";
}

array &array::operator=(const array &a){

  delete[] p;

  size = a.size;

  p = new char[size];

  if(!p) exit(1);

  for(int i=0 ; i<a.size ; i++) p[i] = a.p[i];  // 内容をコピー

  cout << "代入演算子\n";

  return(*this);
}

char &array::operator[](int i){    // [] 演算子の多重定義
  if(i<0){
    cout << "配列の範囲から外れています!";
    return p[0];
  }else if(i>=size){
    cout << "配列の範囲から外れています!";
    return p[size-1];
  }else{
    return p[i];
  }
} 


(2)配列を用いた queue クラスの完成

解答例は以下の通り。ここもそこそこ出来ていた。

ただし、コンストラクタで size を初期化していない人が多かった。 これを行わないと、push() でのエラーチェックがうまく働かないことに注意しよう。

class queue{

    array p;
    int tos;
    int tos2;
    int size;   

  public:
    queue();       // デフォルトコンストラクタ
    queue(int n);  // 配列のサイズを指定するコンストラクタ

    void push(char ch); // queue に文字をプッシュ
    char pop();         // queue から文字をポップ
};


// デフォルトコンストラクタ
queue::queue():p(){

  tos = tos2 = 0;
  size = p.getsize();

}       

// 配列のサイズを指定するコンストラクタ
queue::queue(int n):p(n){

  tos = tos2 = 0;
  size = p.getsize();

}

// queue に文字をプッシュ
void queue::push(char ch){
  if(tos==size){
        cout << "queue はいっぱいです。\n";
  }else{
        p[tos] = ch;
        tos++;
  }
}

// queue から文字をポップ
char queue::pop(){
  if(tos2==tos){
        cout << "queue は空です。\n";
        return '\0';
  }else{
        tos2++;
        return p[tos2-1];
        
  }
}


(3)なぜ配列は queue に向かないか?

解答は以下である。

一度値をポップすると、その領域は二度と使われないから。


これは下図の矢印の領域のことを指している。



(4) 配列を循環させて queue を作る

こちらで用意していた解答例は以下のようである。
tos や tos2 が size に達したら、強制的に 0 にして配列を循環させている。
さらに、queue が空の状態と一杯の状態とを区別するために isFull というフラグを用いた。

他にもいろいろな解答があった。
tos と tos2 を強制的に 0 にするのではなく、 array の tos%size (size で割った余り) 番目の要素に書き込むようにする方法を 書いた人が何人かおり、
これはこちらで提示した方法よりもシンプルで良いと思う。

class queue{

    array p;
    int tos;
    int tos2;
    int size;

    bool isFull;   // ()

  public:
    queue();       // デフォルトコンストラクタ
    queue(int n);  // 配列のサイズを指定するコンストラクタ

    void push(char ch); // queue に文字をプッシュ
    char pop();         // queue から文字をポップ
};


// デフォルトコンストラクタ
queue::queue():p(){

  tos = tos2 = 0;
  size = p.getsize();
  
  isFull = false;   // ()

}       

// 配列のサイズを指定するコンストラクタ
queue::queue(int n):p(n){

  tos = tos2 = 0;
  size = p.getsize();
  
  isFull = false;   // ()

}

// queue に文字をプッシュ
void queue::push(char ch){

  if(tos==tos2 && isFull){   // ()
        cout << "queue はいっぱいです\n";       
  }else{
        p[tos] = ch;
        tos++;
        if(tos==size){           // ()
                tos=0;           // ()
        }                        // ()
        if(tos==tos2){           // ()
                isFull = true;   // ()
        }                        // ()
  }  
  
}
// queue から文字をポップ
char queue::pop(){
  if(tos2==tos && !isFull){  // () !isFull は isFull==false と同じ
        cout << "queue は空です。\n";
        return '\0';
  }else{
        char returnvalue = p[tos2];   // ()
        tos2++;                       // ()
        if(tos2==size){               // ()
                tos2=0;               // ()
        }                             // ()
        if(tos2==tos){                // ()
                isFull = false;       // ()
        }                             // ()
        return returnvalue;           // ()
  }
}




←第四回課題第四回-付録01 : Windows プログラミング→

第四回トップページへ

クラスから入る C++ へ戻る