C++入門|7章の練習問題

2つの気の流れは、本当に同じなのか。長さと許容誤差を使い、演算子オーバーロードでスマートに判定しよう。

7章では、C++で少し高度なプログラムを作るために欠かせない機能を学んできました。

参照渡しでは、関数の中から呼び出し元の変数を扱う方法を確認しました。const修飾子では、変更してはいけない値やオブジェクトを保護する方法を学びました。

さらに、抽象クラスやインターフェースを使い、異なるクラスを共通の型として扱う方法も確認しました。

演算子オーバーロードでは、自分で作成したクラスに対して、+、-、+=、==などの演算子がどのような処理を行うのかを定義しました。

今回の練習問題では、気の方向と強さを表すKiVector2Dクラスを使用します。

2つの気のベクトルを入力し、その差の長さを求めます。そして、差の長さが0.1未満であれば、2つの気の流れはほぼ同じと判定します。

ドラゴンボール風にたとえるなら、2人の戦士が放った気の方向を神殿の測定装置で比較する課題です。

気の向きや強さが完全に一致していなくても、差がごく小さければ、実戦上は同じ気の流れとして扱います。

この練習問題では、次の内容を確認します。

確認する機能役割
length気のベクトルの長さを求める
operator-2つのベクトルの差を求める
operator==2つのベクトルがほぼ等しいか判定する
operator!=2つのベクトルが等しくないか判定する
constメンバ関数オブジェクトを変更しないことを示す
const参照コピーを避けながら引数の変更を防ぐ

図:練習問題で行う判定の全体像

この図が示していること

この図では、2つのKiVector2Dオブジェクトを比較する流れを表しています。

最初にfirstとsecondの差を求めます。次に、その差が表すベクトルの長さをlengthで計算します。

差の長さが0.1未満なら、2つの気の流れはほぼ等しいと判定します。0.1以上なら、等しくないと判定します。

練習問題7-1:2つの気のベクトルを比較する

問題

気の方向と強さを表すKiVector2Dクラスを作成しなさい。

KiVector2Dクラスは、x方向とy方向の気の力をdouble型のメンバ変数として持つものとします。

次の機能を追加してください。

KiVector2Dの長さを求める

KiVector2Dクラスに、ベクトルの長さを求めるlengthを追加してください。

宣言は次のようにします。

double length() const;

ベクトルの長さは、次の式で求めます。

=(x×x+y×y)長さ = \sqrt{(x × x + y × y)}

たとえば、xが3、yが4の場合、長さは5です。

(3×3+4×4)=(9+16)=25=5\sqrt{(3 × 3 + 4 × 4)} = \sqrt{(9 + 16)} = \sqrt{25} = 5

-演算子を追加する

2つのKiVector2Dオブジェクトの差を求めるため、-演算子をオーバーロードしてください。

宣言は次のようにします。

KiVector2D operator-(
    const KiVector2D& left,
    const KiVector2D& right);

x同士、y同士をそれぞれ減算し、計算結果を持つ新しいKiVector2Dオブジェクトを返します。

==演算子を追加する

2つのKiVector2Dオブジェクトがほぼ等しいかを判定するため、==演算子をオーバーロードしてください。

宣言は次のようにします。

bool operator==(
    const KiVector2D& other) const;

2つのベクトルの差を求め、その差の長さが0.1未満の場合はtrueを返します。

差の長さが0.1以上の場合はfalseを返します。

!=演算子を追加する

2つのKiVector2Dオブジェクトが等しくないかを判定するため、!=演算子をオーバーロードしてください。

宣言は次のようにします。

bool operator!=(
    const KiVector2D& other) const;

!=では、==と反対の結果を返すようにしてください。

キーボードから値を入力する

main関数では、firstとsecondという2つのKiVector2Dオブジェクトを作成します。

それぞれのxとyをキーボードから入力してください。

入力後、2つのベクトルと差の長さを表示します。

最後に、==を使って、ほぼ等しいか、等しくないかを表示してください。

期待される実行結果の例1

2つの気のベクトルがほぼ等しい場合です。

first.xを入力:1.0
first.yを入力:1.0
second.xを入力:1.05
second.yを入力:1.05

first = (1.00, 1.00)
second = (1.05, 1.05)
差の長さ = 0.07
firstとsecondはほぼ等しい気の流れです。

firstとsecondの差は、次のようになります。

xの差:1.00 - 1.05 = -0.05
yの差:1.00 - 1.05 = -0.05

差の長さは約0.07なので、0.1未満です。

そのため、ほぼ等しいと判定されます。

期待される実行結果の例2

2つの気のベクトルが等しくない場合です。

first.xを入力:0.0
first.yを入力:0.0
second.xを入力:1.0
second.yを入力:1.0

first = (0.00, 0.00)
second = (1.00, 1.00)
差の長さ = 1.41
firstとsecondは異なる気の流れです。

差の長さは約1.41なので、0.1以上です。

そのため、等しくないと判定されます。

プログラムを作成するヒント

lengthではcmathを使う

平方根を求めるには、cmathヘッダーに用意されているsqrtを使います。

#include <cmath>

lengthの計算は次のようになります。

return std::sqrt(x * x + y * y);

lengthは、xとyを読み取るだけです。

メンバ変数の値を変更しないので、constメンバ関数として定義します。

double length() const;

==では差の長さを調べる

2つのベクトルが近いかを調べるには、自分自身とotherの差を求めます。

KiVector2D difference = *this - other;

thisは、現在operator==を呼び出しているオブジェクト自身を指すポインタです。

*thisとすることで、現在のKiVector2Dオブジェクト本体を表せます。

差の長さが0.1未満かを判定します。

return difference.length() < 0.1;

!=は==を利用する

!=で同じ比較処理をもう一度書く必要はありません。

==の結果を否定すると、反対の結果を得られます。

return !(*this == other);

==の判定方法をあとから変更した場合でも、!=は自動的に同じ基準を使えます。

浮動小数点数を直接比較しない

double型の値は、コンピュータ内部で完全に正確な値として表現できない場合があります。

そのため、次のようにxとyを直接比較するだけでは、期待どおりの結果にならないことがあります。

return x == other.x &&
       y == other.y;

今回の問題では、2つのベクトルの距離が0.1未満なら等しいと考えます。

この0.1が許容誤差です。

プログラムの解答例

プロジェクト名/ファイル名: Prac7_1/ki_vector2d.h

#ifndef PRAC7_1_KI_VECTOR2D_H
#define PRAC7_1_KI_VECTOR2D_H

// 2次元の気の方向と強さを表すクラス
class KiVector2D {
public:
    // 横方向の気の力
    double x;

    // 縦方向の気の力
    double y;

    // コンストラクタ
    KiVector2D(
        double xValue = 0.0,
        double yValue = 0.0);

    // 気のベクトルの長さを求める
    double length() const;

    // 2つの気のベクトルがほぼ等しいか判定する
    bool operator==(
        const KiVector2D& other) const;

    // 2つの気のベクトルが等しくないか判定する
    bool operator!=(
        const KiVector2D& other) const;
};

// 2つの気のベクトルの差を求める
KiVector2D operator-(
    const KiVector2D& left,
    const KiVector2D& right);

#endif

プロジェクト名/ファイル名: Prac7_1/ki_vector2d.cpp

#include "ki_vector2d.h"
#include <cmath>

// コンストラクタ
KiVector2D::KiVector2D(
    double xValue,
    double yValue)
    : x(xValue), y(yValue) {
}

// 気のベクトルの長さを求める
double KiVector2D::length() const {
    return std::sqrt(x * x + y * y);
}

// 2つの気のベクトルがほぼ等しいか判定する
bool KiVector2D::operator==(
    const KiVector2D& other) const {

    // 自分自身と比較対象との差を求める
    KiVector2D difference = *this - other;

    // 差の長さが0.1未満なら、ほぼ等しいとする
    const double tolerance = 0.1;
    return difference.length() < tolerance;
}

// 2つの気のベクトルが等しくないか判定する
bool KiVector2D::operator!=(
    const KiVector2D& other) const {

    // ==の結果を反転して返す
    return !(*this == other);
}

// 2つの気のベクトルの差を求める
KiVector2D operator-(
    const KiVector2D& left,
    const KiVector2D& right) {

    // x同士、y同士を減算した新しいオブジェクトを返す
    return KiVector2D(
        left.x - right.x,
        left.y - right.y);
}

プロジェクト名/ファイル名: Prac7_1/main.cpp

#include <iostream>
#include <iomanip>
#include <string>
#include "ki_vector2d.h"

using namespace std;

// 気のベクトルを表示する関数
void printKiVector(
    const string& name,
    const KiVector2D& vector);

int main() {
    // 比較する2つの気のベクトル
    KiVector2D first;
    KiVector2D second;

    // 第1の気のベクトルを入力する
    cout << "first.xを入力:";
    cin >> first.x;

    cout << "first.yを入力:";
    cin >> first.y;

    // 第2の気のベクトルを入力する
    cout << "second.xを入力:";
    cin >> second.x;

    cout << "second.yを入力:";
    cin >> second.y;

    // 小数点以下2桁で表示する
    cout << fixed << setprecision(2);
    cout << endl;

    // 入力された気のベクトルを表示する
    printKiVector("first", first);
    printKiVector("second", second);

    // 2つのベクトルの差を求める
    KiVector2D difference = first - second;

    // 差の長さを表示する
    cout << "差の長さ = "
         << difference.length()
         << endl;

    // オーバーロードした==で判定する
    if (first == second) {
        cout << "firstとsecondは"
             << "ほぼ等しい気の流れです。"
             << endl;
    }
    else {
        cout << "firstとsecondは"
             << "異なる気の流れです。"
             << endl;
    }

    return 0;
}

// 気のベクトルを表示する
void printKiVector(
    const string& name,
    const KiVector2D& vector) {

    cout << name
         << " = ("
         << vector.x
         << ", "
         << vector.y
         << ")"
         << endl;
}

解説

KiVector2Dクラスの役割

KiVector2Dクラスは、2次元の気の流れを表しています。

xは横方向の気の力です。

yは縦方向の気の力です。

たとえば、次のオブジェクトを考えます。

KiVector2D energy(3.0, 4.0);

このオブジェクトは、横方向へ3、縦方向へ4の気の力を持っています。

原点から座標(3, 4)まで伸びる矢印が、この気のベクトルです。

コンストラクタでxとyを初期化する

コンストラクタは次のように定義しています。

KiVector2D::KiVector2D(
    double xValue,
    double yValue)
    : x(xValue), y(yValue) {
}

メンバ初期化リストを使い、xとyを初期化しています。

引数を省略した場合は、ヘッダーファイルで指定した既定値が使われます。

KiVector2D(
    double xValue = 0.0,
    double yValue = 0.0);

そのため、次の宣言では、xとyがどちらも0になります。

KiVector2D first;

first.x = 0.0
first.y = 0.0

lengthでベクトルの長さを求める

lengthでは、三平方の定理を使ってベクトルの長さを求めます。

double KiVector2D::length() const {
    return std::sqrt(x * x + y * y);
}

xが3、yが4の場合は、次の計算になります。

x × x = 3 × 3 = 9
y × y = 4 × 4 = 16

9 + 16 = 25

√25 = 5

lengthの戻り値は5です。

ドラゴンボール風に考えると、xとyは気の方向別の強さです。

lengthは、その2つを合わせた気の流れ全体の強さを求める測定機能です。

lengthの末尾にconstを付ける理由

lengthの宣言と定義にはconstを付けています。

double length() const;

lengthは、xとyを読み取って計算するだけです。

xやyの値を変更する必要はありません。

constメンバ関数にすることで、lengthの中からメンバ変数を誤って変更することを防げます。

次の処理をlengthの中へ書くと、コンパイルエラーになります。

x = 0.0;

lengthは測定だけを行い、気のベクトルそのものは変更しない関数です。

図:2つのベクトルの差と長さ

この図が示していること

firstとsecondは完全に同じ座標ではありません。

しかし、2つのベクトルの先端は非常に近い位置にあります。

first - secondによって差のベクトルを作り、その長さを測ると約0.07です。

0.07は許容誤差の0.1より小さいため、2つはほぼ等しい気の流れとして扱われます。

-演算子で差を求める

-演算子は、クラス外の関数として定義しています。

KiVector2D operator-(
    const KiVector2D& left,
    const KiVector2D& right)

leftとrightは、どちらもconst参照です。

参照で受け取るため、KiVector2Dオブジェクト全体のコピーを避けられます。

constが付いているため、leftとrightの値を関数内から変更できません。

計算では、x同士とy同士を減算します。

return KiVector2D(
    left.x - right.x,
    left.y - right.y);

たとえば、次の値を考えます。

left  = (5, 8)
right = (2, 3)

計算結果は次のようになります。

x:5 - 2 = 3
y:8 - 3 = 5

結果:(3, 5)

leftとright自体は変更されません。

差を持つ新しいKiVector2Dオブジェクトが返されます。

operator==の呼び出し

main関数では、次のように==を使っています。

if (first == second) {

この処理によって、firstのoperator==が呼び出されます。

概念的には、次の呼び出しと同じです。

first.operator==(second)

operator==のthisはfirstを指します。

otherはsecondを参照します。

operator==内の要素実際の対象
thisfirst
*thisfirstオブジェクト本体
othersecond
differencefirst - secondの結果

*thisの意味

operator==では、次の処理を行っています。

KiVector2D difference = *this - other;

thisは、現在operator==を実行しているオブジェクトを指すポインタです。

first == secondの場合、thisはfirstを指します。

*thisとすると、firstオブジェクト本体を表します。

this
 ↓
firstへのポインタ

*this
 ↓
firstオブジェクト本体

そのため、次の処理はfirst - secondという意味になります。

*this - other

差の長さを許容誤差と比較する

operator==では、許容誤差を0.1に設定しています。

const double tolerance = 0.1;

差の長さがtolerance未満なら、trueを返します。

return difference.length() < tolerance;

判定結果は次のようになります。

差の長さ判定
0.00ほぼ等しい
0.05ほぼ等しい
0.099ほぼ等しい
0.10等しくない
1.41等しくない

今回の条件は0.1未満です。

そのため、差の長さがちょうど0.1の場合は、等しくないと判定します。

double型に許容誤差を使う理由

double型では、小数を2進数で表現します。

10進数では簡単に表せる小数でも、2進数では正確に表現できないことがあります。

そのため、計算結果が見た目では同じでも、内部ではわずかな違いが残る場合があります。

double value = 0.1 + 0.2;

数学的には0.3ですが、コンピュータ内部では、ごく小さな誤差を含む可能性があります。

ベクトルの成分を次のように直接比較すると、その誤差によってfalseになることがあります。

return x == other.x &&
       y == other.y;

今回のプログラムでは、差の長さが一定範囲内かを調べます。

return difference.length() < 0.1;

これにより、完全一致ではなく、実用上十分に近いかを判定できます。

operator==をconstメンバ関数にする理由

operator==は、2つのオブジェクトを比較するだけです。

firstとsecondのどちらも変更しません。

そのため、宣言の末尾にconstを付けています。

bool operator==(
    const KiVector2D& other) const;

最初のconstは、otherを変更しないための指定です。

最後のconstは、operator==を実行している自分自身を変更しないための指定です。

const KiVector2D& other
          ↑
比較対象を変更しない

operator==(...) const
                 ↑
自分自身を変更しない

operator!=では==を再利用する

operator!=は次のように定義しています。

bool KiVector2D::operator!=(
    const KiVector2D& other) const {

    return !(*this == other);
}

まず、*this == otherで等しいかを判定します。

その結果へ!を付け、trueとfalseを反転します。

==の結果!を付けた結果!=の結果
truefalse等しくないはfalse
falsetrue等しくないはtrue

!=の中へ、差の計算をもう一度書く方法もあります。

KiVector2D difference = *this - other;
return difference.length() >= 0.1;

ただし、この書き方では、==と!=の両方に比較処理が存在します。

あとから許容誤差を変更するときに、一方だけを修正してしまうかもしれません。

operator!=からoperator==を呼び出せば、判定処理を1か所にまとめられます。

main関数で値を入力する

main関数では、firstとsecondを生成します。

KiVector2D first;
KiVector2D second;

そのあと、xとyを順番に入力します。

cout << "first.xを入力:";
cin >> first.x;

cout << "first.yを入力:";
cin >> first.y;

同じように、secondの値も入力します。

今回のKiVector2Dでは、学習内容を演算子オーバーロードへ集中させるため、xとyをpublicにしています。

そのため、main関数から直接入力できます。

printKiVectorで値を表示する

表示処理はprintKiVectorへまとめています。

void printKiVector(
    const string& name,
    const KiVector2D& vector)

nameとvectorは、どちらもconst参照で受け取っています。

関数内では表示するだけなので、引数を変更する必要はありません。

cout << name
     << " = ("
     << vector.x
     << ", "
     << vector.y
     << ")"
     << endl;

複数の場所で同じ表示形式を使う場合、関数へまとめるとコードの重複を減らせます。

fixedとsetprecision

main関数では、次の指定を行っています。

cout << fixed << setprecision(2);

fixedを指定すると、小数を固定小数点形式で表示します。

setprecision(2)を指定すると、小数点以下を2桁で表示します。

1    → 1.00
1.05 → 1.05
1.414213... → 1.41

setprecisionを使うため、iomanipヘッダーを読み込んでいます。

#include <iomanip>

図:operator==とoperator!=の判定の流れ

この図が示していること

operator==では、自分自身と比較対象の差を求めます。

次に、差のベクトルの長さを計算し、0.1未満かを判定します。

operator!=では、同じ計算を繰り返さず、operator==の結果を反転しています。

比較処理をoperator==へ集めることで、判定基準を管理しやすくなります。

==と!=の両方を確認する方法

解答例のmain関数では、==を使って判定しています。

if (first == second) {

!=を使う場合は、次のようにも書けます。

if (first != second) {
    cout << "firstとsecondは"
         << "異なる気の流れです。"
         << endl;
}
else {
    cout << "firstとsecondは"
         << "ほぼ等しい気の流れです。"
         << endl;
}

どちらを使っても、同じ基準で判定されます。

operator!=がoperator==を利用しているため、判定結果が食い違うこともありません。

許容誤差を変更する場合

現在の許容誤差は0.1です。

const double tolerance = 0.1;

より厳密に比較したい場合は、値を小さくします。

const double tolerance = 0.01;

おおまかに同じならよい場合は、値を大きくします。

const double tolerance = 0.5;
許容誤差判定の特徴
0.001非常に厳しい
0.01比較的厳しい
0.1今回の基準
0.5比較的ゆるい

適切な許容誤差は、プログラムの目的によって変わります。

神殿の精密な気力測定では小さな値が必要かもしれません。

大まかな修行方向を判定するだけなら、少し大きな値でも十分な場合があります。

この練習問題で身につけたい考え方

今回のKiVector2Dクラスには、ベクトルの長さを求めるlengthを追加しました。

double length() const;

2つのベクトルの差を求めるため、-演算子をオーバーロードしました。

KiVector2D difference = first - second;

2つのベクトルがほぼ等しいかを、==演算子で判定しました。

if (first == second) {

operator==の中では、差の長さを許容誤差と比較しています。

return difference.length() < 0.1;

operator!=では、operator==の結果を反転しました。

return !(*this == other);

ドラゴンボール風にたとえるなら、2つの気の流れを重ね、その先端の距離を測定しています。

完全に同じ位置でなくても、距離が0.1未満なら、神殿の測定基準ではほぼ同じ気の流れです。

この考え方によって、double型の値を単純に完全一致で比較するのではなく、許容できる範囲を持たせた実用的な比較ができます。

演算子オーバーロードは、記号を使ってコードを短くするだけの機能ではありません。

KiVector2Dが表す気の流れにとって、等しいとはどのような状態なのかを、クラス自身へ定義する仕組みです。

クラスの意味に合った演算子を設計し、const、参照、this、既存の演算子を適切に組み合わせることで、読みやすく安全なC++プログラムを作れるようになります。

判定基準は、問題文の「0.1未満」に合わせ、差の長さがちょうど0.1の場合は等しくない構成にしています。