第三回課題解答

第三回課題の解説を行う。

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


1:数字の交換

一問目ということもあり、よくできていた。ポインタを渡す解答例は以下の通り。
変更点は () で示されている。

#include <iostream>
using namespace std;

void koukan(int *x, int *y){ // ()

  int tmp = *x; // ()
  *x = *y;      // ()
  *y = tmp;     // ()

}

int main(){

  int x=3;
  int y=4;

  cout << "交換前\n";
  cout << "x=" << x << ", y=" << y << "\n";

  koukan(&x,&y); // ()

  cout << "交換後\n";
  cout << "x=" << x << ", y=" << y << "\n";

  return 0;
}


なお、参照を渡す場合の解答例は以下の通り。一行のみ (!) の修正で済む。
参照は慣れないと混乱するかもしれないので、まずはポインタをしっかり身につけることをめざそう。

#include <iostream>
using namespace std;

void koukan(int &x, int &y){ // ()

  int tmp = x;
  x = y;
  y = tmp;

}

int main(){

  int x=3;
  int y=4;

  cout << "交換前\n";
  cout << "x=" << x << ", y=" << y << "\n";

  koukan(x,y);

  cout << "交換後\n";
  cout << "x=" << x << ", y=" << y << "\n";

  return 0;
}



2:AND クラス

この問題は問題3(2) のための導入という位置づけであり、 「引数なしのコンストラクタ」および「if 文」が分かっていれば解ける問題である。できは良かった。
「if 文」、「for 文」、「while 文」などは本講義では復習しないので 各自で復習して欲しい。

#include <iostream>
using namespace std;

class AND{
    int out;

  public:
    AND(){ out = 2; }  // () 引数なしのコンストラクタ

    void calc_out(int a, int b);
    int get_out(){ return out; }

};

void AND::calc_out(int a, int b){

  // () if 文による AND 素子の表現

  if(a==2 || b==2){
    out = 2;
  }else if(a==1 && b==1){
    out = 1;
  }else{
    out = 0;
  }
}


条件のうち、「入力のどちらか一方に 2 (不定) があれば、出力も不定となる。 」の意図が正しく伝わらなかったのか、
if 文内で out に 2 を代入していない人が何名かいた。

また、「コンストラクタで out=2 を実行しているため、if 文内では out に 2 を代入しない」という人もいた。
これは関数 calc_out を様々な入力で何度も呼び出すときに問題が起こるので間違いである。

また、out=a*b (a, b は 0 または 1) のように乗算で AND の働きを表現している人が非常に多かった。
もちろん正しく動作するので問題ないのだが、if 文を使った記述もできるようにしておいて欲しい。

さらに、以下のような解答もあった。

class AND{
    int out, in1, in2;  // (※1) 注目

  public:
    AND(){ out = 2; }

    void calc_out(int in1, int in2);  // (※2) 注目
    int get_out(){ return out; }
};


AND 素子に対する入力 i1、i2 が (※1) と (※2) とで2回宣言されている。
このような場合、(※1) の i1、i2 と (※2) の i1、i2 とはメモリ上で全く別の変数として扱われる。
(※1) の i1、i2 はクラスのメンバ変数であり、 (※2) の i1、i2 は calc_out 内部でのみ使える局所的な変数である。

calc_out 関数の中で用いられるのは (※2) の i1、i2 であり、(※1) の i1、i2 は全く使われない。
すなわち、無駄な変数が宣言されているという点で上の例には問題がある。


3(1) ノードとエッジ

ポインタを本格的に用いる問題のため、必ずしもできは良くなかったが、苦労して正解までたどり着いている人も多い。
重要な要素が多く含まれた問題なので、解説を読んで理解しておいて欲しい。

まず、解答例はこちら。クラス実現部のみ記した。

// コンストラクタ
Node::Node(){

  for(int i=0 ; i<EDGE_NUM ; i++){
    edge[i] = 0; // ヌルポインタで初期化
  }
  edgenum=0;  // ()
}

// デストラクタ。特に処理は必要ない。
Node::~Node(){
}

void Node::edgeAdd(Edge *e){
  if(edgenum>=EDGE_NUM){      // ()
    cout << "can not add edge\n";
  }else{

    edge[edgenum] = e;           // ()
    edgenum++;                   // ()

  }
}

// コンストラクタ
Edge::Edge(){
  node = 0; // ヌルポインタで初期化
}

// デストラクタ。特に処理は必要ない。
Edge::~Edge(){
}

void Edge::nodeAdd(Node *n){
  if(node!=0){ // 既にノードにつながっていたら
    cout << "can not add node\n";
  }else{
    node = n;      // ()
  }
}


まず、「ヌルポインタ」の言葉を含む行は必須ではないため、後の注釈で解説することにする。 コメント文の ()~()が必須なポイントである。以下で解説しよう。
以下、模式図を用いてこのプログラムを解説する。 まず、クラス利用部の main 関数で node0、node1、edge1 が宣言された時のメモリの模式図は以下のようになる。(node2 と edge2 は省略)



左の (a) はメモリの模式図、(b) はより Node と Edge らしい図にしてみた。
Node クラスのオブジェクトには 2 つのポインタ edge[0] と edge[1] があり、 Edge クラスのオブジェクトには 1 つのポインタ node があることに注意しよう。
ポインタは「変数やオブジェクトを指し示すもの」であるから、矢印で表現する。

一般に初期化されていないポインタはメモリ上のどこを指しているかは不定である。
#
# (ここは自信のある人向け)
# ちなみに上記の解答例では全てのポインタはヌルポインタ (0) で初期化されている。
# ヌルポインタは「どこも指さないことが保証されている特別なポインタ」である。
# C 言語ではヌルポインタは NULL と表記したが、C++ では ヌルポインタは 0 で表記するのが普通である。
#

さて、クラス利用部で 「node0.edgeAdd(&edge1);」および「edge1.nodeAdd(&node1);」が呼び出されたときに何が起こるだろうか?

「node0.edgeAdd(&edge1);」 では解答例の () 、すなわち 「edge[edgenum] = e;」が呼び出される。
いま、e は &edge1 として呼び出されたのであるから、実質的には node0 の edge[0] が edge1 を指すようになる。

同様に「edge1.nodeAdd(&node1);」 では解答例の () 、すなわち 「node = n;」が呼び出される。
いま、n は &node1 として呼び出されたのであるから、実質的には edge1 の node ポインタが node1 を指すようになる。

以上を図で表現すると以下のようになる。



(a) のメモリの模式図と (b) の Node と Edge の模式図でこのプログラムの動作を理解して欲しい。

間違い例として、(3)の「edge[edgenum] = e;」を

*edge[edgenum] = *e;


と書いている人がいた。 この記述をしてしまったときに何が起きるかを解説しておこう。
edge[edgenum] も e もともにポインタであるから、 *edge[edgenum] と *e はポインタのさす変数やオブジェクトの実体になる。
オブジェクトの実体にオブジェクトの実体を代入しているため、これは 前回扱った「オブジェクトのコピー」となる。
オブジェクトのコピーが行われるとどうなるかを以下の模式図に示す。



node0 の edge[0] ポインタが不定 (初期化していない) 場合、*edge[0]は「メモリ上のどこかわからない場所にある実体」である。
そこに edge1 がコピーされてしまう。もちろん、これは期待した結果と異なり、間違いである。

それに対し、正解の「edge[edgenum] = e;」はポインタ演算であり、実体は操作されない。

3(2) ノードとエッジで AND 演算

3(1) の続きである。ここまで理解できれば、かなりポインタを使いこなせるようになっていると言えるのではないだろうか。

解答は以下のようになる。3(1) と異なる部分に、() をつけた。

#include <iostream>
using namespace std;

#define EDGE_NUM 2  // ノードに接続可能なエッジの数

class Edge;

class Node{

private:
    Edge *edge[EDGE_NUM]; // エッジへのポインタの配列
    int edgenum; // 現在接続しているエッジの数

    int out;     // ()

public:
    // コンストラクタ
    Node();
    // デストラクタ
    ~Node();

    void edgeAdd(Edge *e);  // ノードにエッジを接続

    void set_out(int i){ out=i; } // ()
    int get_out(){ return out;}   // ()
    void set_from_edges();        // ()
};


class Edge{

private:
    Node *node;

public:
    // コンストラクタ
    Edge();

    // デストラクタ
    ~Edge();

    void nodeAdd(Node *n);  // エッジにノードを接続

    int get_out();          // ()
};


// コンストラクタ
Node::Node(){

  for(int i=0 ; i<EDGE_NUM ; i++){
    edge[i] = 0; // ヌルポインタで初期化
  }
  edgenum=0;
}

// デストラクタ。特に処理は必要ない。
Node::~Node(){
}
void Node::edgeAdd(Edge *e){
  if(edgenum>=EDGE_NUM){
    cout << "can not add edge\n";
  }else{

    edge[edgenum] = e;
    edgenum++;

  }
}

// コンストラクタ
Edge::Edge(){
  node = 0; // ヌルポインタで初期化
}

// デストラクタ。特に処理は必要ない。
Edge::~Edge(){
}

void Edge::nodeAdd(Node *n){
  if(node!=0){ // 既にノードにつながっていたら
    cout << "can not add node\n";
  }else{
    node = n;
  }
}

// (※ 以下は全て新たに付け加えられた部分)

void Node::set_from_edges(){

  if(edge[0] && edge[1]){   // 「2つのエッジにともに実体があれば」の意味。「if(edge[0]!=0 && edge[1]!=0)」 と同じ
    int out0 = edge[0]->get_out();
    int out1 = edge[1]->get_out();

    if(out0==2 || out1==2){
      out = 2;
    }else if(out0==1 && out1==1){
      out = 1;
    }else{
      out = 0;
    }

  }else{
    out = 2;
  }
}

int Edge::get_out(){

  if(node){   // node に実体があれば。「if(node!=0)」と同じ
    return node->get_out();
  }else{
    return 2;
  }
}


Edge にも値 (out) を持たせている人が多かった。解答にあるように、Edge には 値は必ずしも必要ではない。

重要なのは、ポインタをたどり node0 から node1 と node2 の out の値を読み出す方法である。
上のプログラムでは以下の模式図のように値を取り出している。



間違い例としては、以下のように、アロー演算子を用いずに無理矢理ドット演算子で頑張るものがあった。


void Node::set_from_edges(){

 Edge e0 = *edge[0];  // 代入が起こる
 Edge e1 = *edge[1];

 out0 = e0.get_out();

 (以下略)


正しく動作するが、コメントにもあるように、上の記述ではオブジェクトの代入が起こり、実行効率が非常に悪い。
解答にあるように、アロー演算子を用いた記述をここで覚えてしまおう。



←第三回課題第四回演習→

第三回トップページへ

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