C++プログラマへの道 #3 - クラス

Last Edited: 9/6/2024

このブログ記事では、C++におけるクラスの概念を紹介します。

C++ Class

クラス

クラスは、オブジェクトの属性とメソッド(内包された変数や関数)を定義するテンプレート、骨組みのようなものです。 以下は、クラスを定義し、オブジェクトのインスタンスを初期化する方法です。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        void barks () {
            cout << "Woof Woof!" << endl;
        }
};
 
int main () {
 
    Dog dog1;
    dog1.name = "Bell";
    dog2.age = 9;
 
    cout << "Dog's name is " << dog1.name << ", and it is " << dog1.age << " years old." << endl;
    // => Dog's name is Bell, and it is 9 years old. 
 
    dog1.barks();
    // => Woof Woof! 
    
    return 0;
}

public キーワードは、属性やメソッドが外部からアクセス可能であることを意味します(これについては次の記事で詳しく扱います)。 クラスは一見小さな機能のように見えますが、新しいプログラミングパラダイムを生み出すほどに大きな影響をもたらしました。 その影響を今後の記事を通して見ていきます。

コンストラクタとデストラクタ

クラスを使ってオブジェクトを宣言・初期化する際、デフォルトの値を属性に設定したいことがあります。これを実現するために、 以下のようにデフォルトコンストラクタを設定できます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        void barks () {
            cout << "Woof Woof!" << endl;
        }
 
    Dog () {
        name = "Unnamed";
        age = 0;
    }
};
 
int main () {
    Dog dog;
    cout << "Dog's name: " << dog.name << ", Dog's age: " << dog.age << endl;
    // => Dog's name: Unnamed, Dog's age: 0
    return 0;
}

デフォルトコンストラクタは、属性にデフォルト値を設定します。また、パラメータ化されたコンストラクタも設定でき、 これによりパラメータに基づいて初期化が行われます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        void barks () {
            cout << "Woof Woof!" << endl;
        }
 
    Dog () {
        name = "Unnamed";
        age = 0;
    }
 
    Dog (string n, int a) {
        name = n;
        age = a;
    }
};
 
int main () {
    Dog dog;
    cout << "Dog's name: " << dog.name << ", Dog's age: " << dog.age << endl;
    // => Dog's name: Unnamed, Dog's age: 0
 
    Dog dog1("Bell", 9);
    cout << "Dog's name: " << dog1.name << ", Dog's age: " << dog1.age << endl;
    // => Dog's name: Bell, Dog's age: 9
    return 0;
}

異なるパラメータを持つ複数のコンストラクタを作成でき、コンパイラが適切なものを識別します。コンストラクタは、 オブジェクトの属性にメモリを動的に割り当てる際にも便利です。

class ArrayList {
    public:
        // public attributes
        int size;
        int *array;
 
    ArrayList (int s) {
        size = s;
        *array = new int[s];
    }
};

しかし、メモリリークを防ぐためにメモリを解放する必要があります。そのために、 オブジェクトが不要になったときに実行されるデストラクタを使用します。デストラクタは以下のように定義できます。

class ArrayList {
    public:
        // public attributes
        int size;
        int *array;
 
    ArrayList (int s) {
        size = s;
        *array = new int[s];
    }
 
    ~ArrayList () {
        delete array;
    }
};

メンバー初期化リスト

Dog クラスの例を詳しく見ると、パラメータ名が na に設定されていることがわかります。 これはクラスの属性 nameage との曖昧さを避けるためです。しかし、同じ名前を使えた方がわかりやすくなります。 そのために、以下のようにメンバー初期化リストを使用できます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        void barks () {
            cout << "Woof Woof!" << endl;
        }
 
    Dog (string name, int age) : 
        name(name),
        age(age) {}
};

属性を定義する際に、メンバー初期化関数が自動的に作成され、同じ名前の入力で属性を初期化できます。 メンバー初期化リストは、const 属性やデフォルトコンストラクタを持たないオブジェクト属性、 参照属性を初期化する際にも便利です。

this キーワード

this キーワードは、作成されているオブジェクトへのポインタです。これにより、パラメータ名と属性名が 同じ場合でも対応できます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        void barks () {
            cout << "Woof Woof!" << endl;
        }
 
    Dog (string name, int age) {
        this->name = name;
        this->age = age;
    }
};

また、以下のように メソッドチェイニング を作成する際にも役立ちます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        // public methods
        Dog& barks () {
            cout << "Woof Woof!" << endl;
            return *this; // dereference this pointer and return as reference
        }
 
        Dog& sits () {
            cout << "Sit Down." << endl;
            return *this;
        }
};
 
int main () {
    Dog dog;
    dog.barks().sits(); // Method Chaining 
    // => (dog.barks() => dog).sits()
    return 0;
}

コンストラクタの委譲とコピーコンストラクタ

異なる数のパラメータを持つ複数のコンストラクタがクラス内にある場合、コンストラクタの委譲を使用してコードを短縮できます。 (C++のバージョンによっては使えないことがあります。)

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        Dog (string name) {
            this -> name = name;
        }
 
        Dog (string name, int age) : Dog (name) {
            this -> age = age;
        }
};

また、オブジェクトをコンストラクタに渡してコピーを作成することもできます。

class Dog {
    public:
        // public attributes
        string name;
        int age;
 
        Dog (string name) {
            this -> name = name;
        }
 
        Dog (string name, int age) : Dog (name) {
            this -> age = age;
        }
 
        Dog (const Dog& dog) {
            this -> name = dog.name;
            this -> age = dog.age;
        }
};
 
int main () {
    Dog dog("Bell", 9);
    Dog dogCpy = dog;
    cout << "Copied name: " << dogCpy.name << endl;
    // => Copied name: Bell
    return 0;
}

上記のコピーコンストラクタは、デフォルトで利用可能な標準のコピーコンストラクタです。ここで紹介した理由は、 ポインタを属性として持つオブジェクトをコピーする際に注意が必要だからです。

class ArrayList {
    public:
        int* array;
        int size;
 
    ArrayList (int size) {
        this -> size = size;
        array = (int *) malloc(size * sizeof(int));
    }
 
    ~ArrayList () {
        free(array);
    }
};
 
int main () {
    ArrayList list(5);
    ArrayList copy = list;
 
    cout << "Original Address: " << list.array << ", Copied Address: " << copy.array << endl;
    // => same address!
    exit(0);
    return 0;
}

上記のコードを実行すると、listcopy のポインタのアドレスが同じであることに気づきます。これは、list.array の変更が copy.array にも影響するため、問題があります。また、コードから exit を削除すると、同じメモリが二重に 解放されるため、プログラムがクラッシュします。これを避けるために、次のようにディープコピーコンストラクタを使用できます。

class ArrayList {
    public:
        int* array;
        int size;
 
    ArrayList (int size) {
        this -> size = size;
        array = (int *) malloc(size * sizeof(int));
    }
 
    ArrayList (ArrayList& list) {
        this -> size = list.size;
        array = (int *) malloc(list.size * sizeof(int));
    }
 
    ~ArrayList () {
        free(array);
    }
};
 
int main () {
    ArrayList list(5);
    ArrayList copy = list;
 
    cout << "Original Address: " << list.array << ", Copied Address: " << copy.array << endl;
    // => different address!
 
    return 0;
}

演算子のオーバーロード

オブジェクトに対して代数演算子や比較演算子を適用するのが理にかなう場合があります。

class ArrayList {
    public:
        int* array;
        int size;
 
    void print() {
        cout << "[";
        for (int i = 0; i < size-1; i++) {
            cout << array[i] << ", ";
        }
        cout << array[size-1] << "]" << endl;
    }
 
    ArrayList (int array[], int size) {
        this -> size = size;
        this -> array = (int *) malloc(size * sizeof(int));
 
        for (int i = 0; i < size; i++) {
            this -> array[i] = array[i];
        }
    }
 
    ~ArrayList () {
        free(array);
    }
};
 
int main () {
    int array1[5] = {1,2,3,4,5};
    int array2[5] = {1,2,3,4,5};
    ArrayList list1(array1, 5);
    ArrayList list2(array2, 5);
 
    list1.print();
    list2.print();
 
    if (list1 == list2) {
        cout << "List 1 is the same as List 2!" << endl;
    }
 
    ArrayList concat = list1 + list2;
    concat.print();
 
    return 0;
}

しかし、上記のコードは、オブジェクトに対する演算子が定義されていないため、コンパイルされません。 この問題を解決するために、以下のようにオブジェクトに対する演算子の動作を定義できます。

class ArrayList {
    public:
        int* array;
        int size;
 
    void print() {
        cout << "[";
        for (int i = 0; i < size-1; i++) {
            cout << array[i] << ", ";
        }
        cout << array[size-1] << "]" << endl;
    }
 
    ArrayList (int array[], int size) {
        this -> size = size;
        this -> array = (int *) malloc(size * sizeof(int));
 
        for (int i = 0; i < size; i++) {
            this -> array[i] = array[i];
        }
    }
 
    ~ArrayList () {
        free(array);
    }
 
    ArrayList operator+(const ArrayList& array_list) {
        int final_size = this -> size + array_list.size;
        int final_array[final_size];
        for (int i = 0; i < this -> size; i++) {
            final_array[i] = this -> array[i];
        }
        for (int i = size; i < final_size; i++) {
            final_array[i] = array_list.array[i - this -> size];
        }
        return ArrayList(final_array, final_size);
    }
 
    int operator==(const ArrayList& array_list) {
        if (this -> size != array_list.size) {
            return 0;
        }
        for (int i = 0; i < this -> size; i++) {
            if (this -> array[i] != array_list.array[i]) {
                return 0;
            }
        }
        return 1;
    }
};

上のように演算子を定義すると、+== 演算子を使用して連結や比較が行えるようになります。

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース