クラス 解答ページ | Programming Place Plus 新C++編

トップページ新C++編クラス

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (基本★)

次のプログラムで、コンパイルエラーになる箇所を挙げてください。

#include <iostream>

class C {
public:
    C(int value)
    {
        m_value = value;
    }

    void set_value1(int value)
    {
        m_value = value;
    }
    void set_value2(int value) const
    {
        m_value = value;
    }

    int get_value1()
    {
        return m_value;
    }
    int get_value2() const
    {
        return m_value;
    }

private:
    int m_value;
};

int main()
{
    C c1(100);
    c1.m_value = 200;
    c1.set_value1(300);
    c1.set_value2(400);
    std::cout << c1.get_value1() << "\n";
    std::cout << c1.get_value2() << "\n";

    const C c2(100);
    c2.m_value = 200;
    c2.set_value1(300);
    c2.set_value2(400);
    std::cout << c2.get_value1() << "\n";
    std::cout << c2.get_value2() << "\n";
}


main関数の中を順番に確認していきます。

C c1(100); は問題ありません。クラスC のオブジェクトc1 を定義しており、コンストラクタに 100 という実引数を渡しています(本編解説)。

c1.m_value = 200; はコンパイルエラーです。m_value は private として宣言されているデータメンバなので、クラスの外側から直接アクセスすることが許されません(本編解説)。

c1.set_value1(300); は問題ありません。メンバ関数 set_value1 を呼び出しています。

c1.set_value2(400); は、この1文自体には問題はなく、メンバ関数 set_value2 を呼び出しているだけです。しかし、set_value2メンバ関数の側に問題があります。set_value2メンバ関数の宣言には const が付加されており、constメンバ関数になっています(本編解説)。constメンバ関数の中では、データメンバを書き換えることが許されないため、m_value = value; の部分がコンパイルエラーになります。

std::cout << c1.get_value1() << "\n"; は問題ありません。get_value1メンバ関数を呼び出し、その戻り値を出力しています。

std::cout << c1.get_value2() << "\n"; も問題ありません。get_value2メンバ関数を呼び出し、その戻り値を出力しています。

const C c2(100); は問題ありません。c1 と違って、今度は constオブジェクトとして定義しているため、値を変更しようとする行為が禁じられます。

c2.m_value = 200; はコンパイルエラーです。m_value は private として宣言されています。public だったとしても、c2 が const なので、値を書き換えることはできません。

c2.set_value1(300); はコンパイルエラーです。constオブジェクトである c2 に対しては、constメンバ関数でないメンバ関数を呼び出すことができません(本編解説)。

c2.set_value2(400); は、この1文自体には問題はありません。c2 が constオブジェクトですが、set_value2メンバ関数は constメンバ関数なので、呼び出すことができます。しかし、constメンバ関数の中でデータメンバを書き換えようとしているため、set_value2メンバ関数の側でコンパイルエラーになります。

std::cout << c2.get_value1() << "\n"; はコンパイルエラーです。get_value1メンバ関数が constメンバ関数でないためです。

std::cout << c2.get_value2() << "\n"; は問題ありません。こちらは constメンバ関数なので、呼び出し可能です。

問題2 (基本★★)

ペイントスクリプトの penコマンドを実装することを想定して、ペンを表現したクラスを作成してください。

penコマンドは、たとえば「pen 255 0 0」のように RGB の強さとともに使用し、以降の描画コマンドで使う色が変更されるというものです。現在の色を管理するために、Penクラスを使います。


Penクラスに必要なものが何であるのかを検討する必要があります。問題文には「ペンを表現したクラス」であり、「現在の色を管理するために Penクラスを使う」とありますから、現時点で明らかに必要だと分かるものは、「色」を記憶するデータメンバです。

class Pen {
private:
    Color  m_color;  // 色
}

基本に忠実に、データメンバは private にします(本編解説)。

色を記憶するデータメンバはできましたが、private になっていて外部からアクセスできないので、実際に「記憶させる」ことができません。ここで以下の選択肢があります。

  1. コンストラクタを用意して、m_color に与える初期値を指定できるようにする
  2. public なメンバ関数を用意して、m_color の値を変更できるようにする
  3. 1と2を両方とも用意する

すべての変数を初期化しておくことは、C++ でプログラムの安全性を保つための基本原則として重要です。ここでは m_color の値が不定値のまま使えてしまってはいけないということです。Color がコンストラクタを持っていれば不定値にはなりませんが、そうでないなら Penクラスにはコンストラクタを用意したほうがいいでしょう。

データメンバの宣言とともに初期値を与える構文があるので(「構造体」のページを参照)、コンストラクタでなければならないということでもありませんが、この方法が採れるのは定数式で初期化できる場合に限られます。

2番の「public なメンバ関数」は必要でしょうか? これは考え方次第です。

現実に存在する「ペン」と同じように考えると、色が変わる「ペン」はあり得なさそうです(最初から色のセットが用意してあって、その中から選んで切り替えられるならおかしくないですが)。現実世界の考え方を重視するのはオブジェクト指向的で、これも1つの方向性ではあります。

もう1つの考え方として、変数の値がころころ変わらないほうが分かりやすいプログラムになるという点があります。この考え方を採用するなら、m_color の値を書き換えるメンバ関数は作らないほうが正解です。色を変えたくなったら、別の色で初期化した Pen のオブジェクトを新たに作ることで対応できます。

ここでは(今後も)、後者の考え方を採用して進めます。


コンストラクタを用意しますが、ここでさらに選択肢があります。

  1. 引数に色情報を指定して、その値で m_color を初期化するように実装する
  2. 引数を空にして、m_color をデフォルトの色で初期化するように実装する

まだ解説していませんが、コンストラクタを複数定義できます。そのため、1と2の方法を2つとも実装することは可能です。

2の方法は、ペンにとってのデフォルトカラーとして納得感がある色が存在するかどうかがポイントになります。これといって色を指示しなければ何色になるのが自然といえるのかということです。黒ならおかしくはなさそうですが、人それぞれの感覚といわざるを得ないでしょう。また、キャンバスの最初の色次第であるともいえるかもしれません。今回はデフォルトカラーは定義せず、1番のコンストラクタを実装することにします。

class Pen {
public:
    // コンストラクタ
    //
    // color: ペンの色
    Pen(Color color);

private:
    Color  m_color;  // 色
}

Pen::Pen(Color color)
{
    m_color = color;
}

これで、色を記憶させることができるようになりました。この時点では記憶できただけなので、その色を使う方法がありません。m_color をどのように使うかという点でも、次のように選択肢があります。

  1. Penクラスは色の管理に専念し、実際にキャンバスに色を置く処理は外部に任せる
  2. Penクラス自ら、キャンバスに描けるようにする

2番の考え方なら、次のようになります。

class Pen {
public:
    // コンストラクタ
    //
    // color: ペンの色
    Pen(Color color);

    // 点を描く
    //
    // canvas: キャンバス
    // x: X座標
    // y: Y座標
    void draw_dot(canvas_t* canvas, int x, int y);

private:
    Color  m_color;  // 色
}

Pen::Pen(Color color)
{
    m_color = color;
}

void Pen::draw_dot(canvas_t* canvas, int x, int y)
{
    // キャンバスに点を描く関数を呼ぶ
    paint_dot(canvas, x, y, m_color);
}

この方法では、描きたい図形ごとにメンバ関数を用意しなければなりません。結局、実際にキャンバスに何かを描く部分はキャンバスの側のソースコードにあるはずなので、ある意味、そちらに転送するだけの存在の関数を量産していくことになります。

【上級】「ペン」自身が、ユーザーが描きたい「図形」を知っているような構図になるので、少々違和感があるともいえます。この考え方の場合は「キャンバス」が、描かれる「図形」を知っているのも同様におかしいといえますから、「描く」ことを意味する Painterクラスのようなものに仲介させる発想が出てきます。たとえば、作成済みの pen を使ってキャンバスに直線を描くという指示を、Painter::draw_line(pen, canvas, 10, 10, 30, 30); のように書きます。

Penクラスには色の管理に専念してもらう方針の場合、外部のプログラムで描画できるように、色情報を返すメンバ関数を用意してやる必要があります。

class Pen {
public:
    // コンストラクタ
    //
    // color: ペンの色
    Pen(Color color);

    // 色を返す
    inline Color get_color() const
    {
        return m_color;
    }

private:
    Color  m_color;  // 色
}

Pen::Pen(Color color)
{
    m_color = color;
}

次のように描画できます。

Pen pen({255, 0, 0});
paint_dot(canvas, x, y, pen.get_color());

ペイントスクリプトでは、この最後の方針に沿って進めていきます。

問題3 (応用★★)

ペイントスクリプトで使えるように、以下のメンバ関数を備えた Canvasクラスを作成してください。

  1. 縦横のピクセル数を指定できるコンストラクタ
  2. キャンバス全体を指定色で塗りつぶす
  3. 指定した位置に、ペンで点を描く
  4. 指定した範囲に、ペンで四角形の枠を描く
  5. 4の関数を、ペンで内側を塗りつぶすようにしたバージョン


これまでのページでは、キャンバスを std::vector<std::vector<Color>> を使って表現してきました。これをデータメンバとして持った Canvasクラスに仕立てます。

class Canvas {
private:
    std::vector<std::vector<Color>>     m_pixels;
};

問題にある1~4のメンバ関数は、これまでのページで作ってきた関数をメンバ関数へと置き換えていけば作成できます(「多次元配列」のページの練習問題を参照)。

まず1つ目の「縦横のピクセル数を指定できるコンストラクタ」を作成します。

class Canvas {
public:
    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);

private:
    std::vector<std::vector<Color>>     m_pixels;
};

Canvas::Canvas(unsigned int width, unsigned int height)
{
    assert(1 <= width);
    assert(1 <= height);

    m_pixels.resize(height);
    for (auto& row : m_pixels) {
        row.resize(width);
    }
}

2つ目の「キャンバス全体を指定色で塗りつぶす」メンバ関数を作成します。

class Canvas {
public:
    // 全面を塗りつぶす
    //
    // color: 色
    void fill(Color color);
};

void Canvas::fill(Color color)
{
    for (auto& row : m_pixels) {
        std::fill(std::begin(row), std::end(row), color);
    }
}

初期状態を真っ白なキャンバスにしたいと思ったら、コンストラクタの中で fillメンバ関数を呼び出して、白く塗っておくと良いです。

class Canvas {
public:
    // コンストラクタ
    //
    // キャンバスの初期状態は真っ白。
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);
};

Canvas::Canvas(unsigned int width, unsigned int height)
{
    assert(1 <= width);
    assert(1 <= height);

    m_pixels.resize(height);
    for (auto& row : m_pixels) {
        row.resize(width);
    }

    // 初期状態は白
    fill({255, 255, 255});
}

続いて3つ目の「指定した位置に、ペンで点を描く」メンバ関数です。ペンは Penクラスで表現することになっているので、Pen オブジェクトの参照を渡すようにします。また、キャンバスの外側の座標を指定したときの処置も必要です。

class Canvas {
public:
    // 点を描画する
    //
    // x: X座標
    // y: Y座標
    // pen: ペン
    void paint_dot(int x, int y, Pen& pen);

    // 座標がキャンバスの範囲内かどうか調べる
    //
    // x: X座標
    // y: Y座標
    // 戻り値: キャンバス内の座標なら true。そうでなければ false
    bool is_inside(int x, int y) const;
};

void Canvas::paint_dot(int x, int y, Pen& pen)
{
    if (!is_inside(x, y)) {
        return;
    }

    m_pixels[y][x] = pen.get_color();
}

bool Canvas::is_inside(int x, int y) const
{
    if (x < 0 || static_cast<int>(canvas[0].size()) <= x) {
        return false;
    }
    if (y < 0 || static_cast<int>(canvas.size()) <= y) {
        return false;
    }
    return true;
}

次に、4つ目の「指定した範囲に、ペンで四角形の枠を描く」メンバ関数です。さきほど作った paint_dotメンバ関数を使って点を打つようにします。

class Canvas {
public:
    // 矩形を描画する
    //
    // left: 左端X座標
    // top: 上端Y座標
    // right: 右端X座標
    // bottom: 下端Y座標
    // pen: ペン
    void paint_rect(int left, int top, int right, int bottom, Pen& pen);
};

void Canvas::paint_rect(int left, int top, int right, int bottom, Pen& pen)
{
    // 上辺
    for (int x {left}; x <= right; ++x) {
        paint_dot(x, top, pen);
    }

    // 左辺
    for (int y {top + 1}; y < bottom; ++y) {
        paint_dot(left, y, pen);
    }

    // 右辺
    for (int y {top + 1}; y < bottom; ++y) {
        paint_dot(right, y, pen);
    }

    // 下辺
    for (int x {left}; x <= right; ++x) {
        paint_dot(x, bottom, pen);
    }
}

最後に「4の関数を、ペンで内側を塗りつぶすようにしたバージョン」のメンバ関数を作成します。実のところ、こちらのほうが単純です。

class Canvas {
public:
    // 内側を塗りつぶした矩形を描画する
    //
    // left: 左端X座標
    // top: 上端Y座標
    // right: 右端X座標
    // bottom: 下端Y座標
    // pen: ペン
    void paint_filled_rect(int left, int top, int right, int bottom, Pen& pen);
};

void Canvas::paint_filled_rect(int left, int top, int right, int bottom, Pen& pen)
{
    for (int y {top}; y <= bottom; ++y) {
        for (int x {left}; x <= right; ++x) {
            paint_dot(x, y, pen);
        }
    }
}


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る