C++で仮想関数とスマートポインタを使いこなす方法

仮想関数とスマートポインタは、C++プログラミングにおいて重要な概念です。仮想関数は多態性(ポリモーフィズム)を実現するために使用され、異なるクラスに属するオブジェクトを一つのインターフェースで扱うことを可能にします。一方、スマートポインタはメモリ管理を自動化し、メモリリークや解放忘れといった問題を防ぎます。本記事では、仮想関数とスマートポインタの基本概念から応用までを詳しく解説し、実際のコード例を交えながらその使い方を学んでいきます。

目次

仮想関数の基本概念

仮想関数は、基底クラスで定義され、派生クラスでオーバーライドされるメンバ関数です。これにより、ポリモーフィズムを実現し、実行時に正しい派生クラスのメソッドが呼び出されます。仮想関数は、通常、基底クラスのメンバ関数の先頭にvirtualキーワードを付けて宣言されます。以下に、仮想関数の基本的な宣言方法を示します。

class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
};

この例では、Baseクラスに仮想関数displayが定義され、Derivedクラスでオーバーライドされています。仮想関数を使うことで、基底クラスのポインタや参照を通じて派生クラスのメソッドを呼び出すことができます。

仮想関数の具体例

仮想関数を使った具体的な例を示します。以下のコードでは、動物を表すAnimalクラスを基底クラスとして、犬を表すDogクラスと猫を表すCatクラスが派生しています。speakメソッドが仮想関数として定義され、各派生クラスでオーバーライドされています。

#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
    virtual void speak() const {
        std::cout << "Some generic animal sound" << std::endl;
    }

    virtual ~Animal() = default; // 仮想デストラクタ
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Dog>());
    animals.push_back(std::make_unique<Cat>());

    for (const auto& animal : animals) {
        animal->speak();
    }

    return 0;
}

この例では、Animalクラスのvirtual void speak() constという仮想関数が定義されています。DogクラスとCatクラスでは、この仮想関数をオーバーライドして、それぞれ特定の動物の鳴き声を出力するようにしています。

メイン関数では、Animal型のスマートポインタを格納するベクターanimalsが作成され、DogオブジェクトとCatオブジェクトが追加されます。ループを通じて、各動物のspeakメソッドが呼び出され、正しい鳴き声が出力されます。このようにして、仮想関数を使うことで、異なる派生クラスのオブジェクトを一つのインターフェースで扱うことができ、柔軟で拡張性のあるコードを書くことができます。

純粋仮想関数と抽象クラス

純粋仮想関数とは、基底クラスで宣言されるが、実装を持たない仮想関数のことです。このような関数は、派生クラスで必ずオーバーライドする必要があります。純粋仮想関数を一つ以上持つクラスは抽象クラスと呼ばれ、直接インスタンス化することはできません。抽象クラスは、共通のインターフェースを定義し、派生クラスにその実装を任せる役割を果たします。

以下のコード例では、Shapeクラスが抽象クラスとして定義されており、純粋仮想関数drawが宣言されています。CircleクラスとRectangleクラスがこの抽象クラスを継承し、それぞれdraw関数をオーバーライドしています。

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual void draw() const = 0; // 純粋仮想関数

    virtual ~Shape() = default; // 仮想デストラクタ
};

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Rectangle>());

    for (const auto& shape : shapes) {
        shape->draw();
    }

    return 0;
}

この例では、Shapeクラスに純粋仮想関数drawが定義されています。CircleクラスとRectangleクラスは、この純粋仮想関数をオーバーライドし、それぞれの形状を描画する実装を提供します。メイン関数では、Shape型のスマートポインタを格納するベクターshapesが作成され、CircleオブジェクトとRectangleオブジェクトが追加されます。ループを通じて、各形状のdrawメソッドが呼び出され、正しい描画メッセージが出力されます。

純粋仮想関数と抽象クラスを使用することで、共通のインターフェースを提供し、派生クラスに具体的な実装を強制することができます。これにより、コードの柔軟性と拡張性が向上します。

スマートポインタの基本概念

スマートポインタは、C++11で導入されたメモリ管理のためのクラステンプレートです。これにより、手動でのメモリ管理の煩雑さを軽減し、メモリリークや未解放メモリの問題を防ぐことができます。スマートポインタには主に三つの種類があります:unique_ptrshared_ptr、およびweak_ptrです。

  1. unique_ptr:
  • 単独所有権を持つスマートポインタ。
  • コピー不可で、ムーブのみが可能。
  • 所有権が移動した後、元のポインタはヌルポインタになります。
  1. shared_ptr:
  • 共有所有権を持つスマートポインタ。
  • 複数のshared_ptrが同じオブジェクトを指すことが可能。
  • 参照カウントを持ち、最後の一つが破棄されるときにオブジェクトが解放されます。
  1. weak_ptr:
  • shared_ptrの循環参照を防ぐための補助的なスマートポインタ。
  • 所有権を持たず、shared_ptrの有効性を確認するために使用されます。

以下に、各スマートポインタの基本的な使い方を示します。

#include <iostream>
#include <memory>

void uniquePtrExample() {
    std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);
    std::cout << "UniquePtr value: " << *uniquePtr << std::endl;
    // std::unique_ptr<int> anotherPtr = uniquePtr; // エラー:コピー不可
    std::unique_ptr<int> movedPtr = std::move(uniquePtr); // ムーブは可能
    std::cout << "Moved UniquePtr value: " << *movedPtr << std::endl;
}

void sharedPtrExample() {
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(20);
    std::shared_ptr<int> sharedPtr2 = sharedPtr1; // 共有所有権
    std::cout << "SharedPtr1 value: " << *sharedPtr1 << std::endl;
    std::cout << "SharedPtr2 value: " << *sharedPtr2 << std::endl;
    std::cout << "Reference count: " << sharedPtr1.use_count() << std::endl;
}

void weakPtrExample() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(30);
    std::weak_ptr<int> weakPtr = sharedPtr; // 弱い参照
    std::cout << "SharedPtr value: " << *sharedPtr << std::endl;
    if (auto tempPtr = weakPtr.lock()) { // weak_ptrからshared_ptrを取得
        std::cout << "WeakPtr is valid, value: " << *tempPtr << std::endl;
    } else {
        std::cout << "WeakPtr is expired" << std::endl;
    }
}

int main() {
    uniquePtrExample();
    sharedPtrExample();
    weakPtrExample();
    return 0;
}

このコードでは、unique_ptrshared_ptr、およびweak_ptrの基本的な使用例を示しています。unique_ptrは単独所有権を持ち、コピーはできませんが、ムーブは可能です。shared_ptrは共有所有権を持ち、複数のshared_ptrが同じオブジェクトを指すことができます。weak_ptrは、shared_ptrの循環参照を防ぐために使用され、有効性を確認するために使用されます。

スマートポインタを使用することで、安全かつ効率的なメモリ管理を実現し、C++プログラムの信頼性を向上させることができます。

unique_ptrの使い方

unique_ptrは、単独所有権を持つスマートポインタであり、所有するメモリリソースを他のポインタにコピーすることはできません。これにより、特定のリソースに対する所有権を明確に管理し、メモリリークを防ぐことができます。以下に、unique_ptrの基本的な使い方とその利点を示します。

基本的な使用方法

unique_ptrは、標準ライブラリの<memory>ヘッダをインクルードすることで使用できます。メモリを割り当てる際にはstd::make_unique関数を使用します。

#include <iostream>
#include <memory>

int main() {
    // unique_ptrの作成と初期化
    std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);

    // 値のアクセス
    std::cout << "UniquePtr value: " << *uniquePtr << std::endl;

    // unique_ptrはコピーできない
    // std::unique_ptr<int> anotherPtr = uniquePtr; // これはエラーになります

    // unique_ptrはムーブが可能
    std::unique_ptr<int> movedPtr = std::move(uniquePtr);
    std::cout << "Moved UniquePtr value: " << *movedPtr << std::endl;

    // uniquePtrは現在ヌルポインタ
    if (!uniquePtr) {
        std::cout << "uniquePtr is now null" << std::endl;
    }

    return 0;
}

この例では、unique_ptrが整数へのポインタを管理しています。std::make_unique関数を使用してメモリを割り当て、その値にアクセスしています。また、unique_ptrはコピーできないため、所有権を別のポインタに移動するためにはstd::move関数を使用します。

カスタムデリータの利用

unique_ptrはカスタムデリータを使用することができます。これは、リソースの解放方法をカスタマイズする場合に便利です。

#include <iostream>
#include <memory>

void customDeleter(int* ptr) {
    std::cout << "Custom deleter called for pointer: " << ptr << std::endl;
    delete ptr;
}

int main() {
    // カスタムデリータを指定したunique_ptr
    std::unique_ptr<int, decltype(&customDeleter)> uniquePtr(new int(20), customDeleter);

    std::cout << "UniquePtr value with custom deleter: " << *uniquePtr << std::endl;

    // uniquePtrがスコープを抜けると、カスタムデリータが呼ばれる
    return 0;
}

この例では、カスタムデリータcustomDeleterが定義され、unique_ptrがスコープを抜けるときにこのデリータが呼び出されます。

配列の管理

unique_ptrは動的配列も管理できます。配列を管理する場合は、std::make_unique<T[]>(size)を使用します。

#include <iostream>
#include <memory>

int main() {
    // 配列を管理するunique_ptr
    std::unique_ptr<int[]> arrayPtr = std::make_unique<int[]>(5);

    // 配列に値を設定
    for (int i = 0; i < 5; ++i) {
        arrayPtr[i] = i * 10;
    }

    // 配列の値にアクセス
    for (int i = 0; i < 5; ++i) {
        std::cout << "Array element " << i << ": " << arrayPtr[i] << std::endl;
    }

    return 0;
}

この例では、unique_ptrが動的に割り当てられた整数配列を管理しています。配列の要素に値を設定し、アクセスしています。

unique_ptrを使用することで、メモリ管理が自動化され、メモリリークのリスクが減少します。また、所有権の移動やカスタムデリータの利用など、柔軟なメモリ管理が可能になります。

shared_ptrの使い方

shared_ptrは、共有所有権を持つスマートポインタで、複数のshared_ptrが同じオブジェクトを指すことができます。オブジェクトは最後のshared_ptrが破棄されるときに自動的に解放されます。これにより、メモリ管理が容易になり、特に複雑な所有権関係がある場合に便利です。以下に、shared_ptrの基本的な使い方と注意点を示します。

基本的な使用方法

shared_ptrは、標準ライブラリの<memory>ヘッダをインクルードすることで使用できます。メモリを割り当てる際にはstd::make_shared関数を使用します。

#include <iostream>
#include <memory>

int main() {
    // shared_ptrの作成と初期化
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(30);

    // 共有所有権
    std::shared_ptr<int> sharedPtr2 = sharedPtr1;

    // 値のアクセス
    std::cout << "SharedPtr1 value: " << *sharedPtr1 << std::endl;
    std::cout << "SharedPtr2 value: " << *sharedPtr2 << std::endl;

    // 参照カウントの確認
    std::cout << "Reference count: " << sharedPtr1.use_count() << std::endl;

    return 0;
}

この例では、shared_ptrが整数へのポインタを管理しています。std::make_shared関数を使用してメモリを割り当て、複数のshared_ptrが同じオブジェクトを指すことで、共有所有権が実現されています。また、use_countメソッドを使って現在の参照カウントを確認できます。

循環参照に注意

shared_ptrを使用する際には、循環参照に注意する必要があります。循環参照が発生すると、オブジェクトが解放されずにメモリリークを引き起こす可能性があります。この問題を回避するために、weak_ptrを使用します。

#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;

    Node(int val) : value(val) {}
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    node1->next = node2;
    node2->next = node1; // 循環参照が発生

    // 参照カウントの確認
    std::cout << "Node1 use_count: " << node1.use_count() << std::endl;
    std::cout << "Node2 use_count: " << node2.use_count() << std::endl;

    return 0;
}

この例では、Nodeクラスのインスタンスが相互に参照し合うことで循環参照が発生し、メモリリークの原因となります。このような場合、weak_ptrを使用することで循環参照を防ぎます。

カスタムデリータの利用

shared_ptrもカスタムデリータを使用することができます。これは、リソースの解放方法をカスタマイズする場合に便利です。

#include <iostream>
#include <memory>

void customDeleter(int* ptr) {
    std::cout << "Custom deleter called for pointer: " << ptr << std::endl;
    delete ptr;
}

int main() {
    // カスタムデリータを指定したshared_ptr
    std::shared_ptr<int> sharedPtr(new int(40), customDeleter);

    std::cout << "SharedPtr value with custom deleter: " << *sharedPtr << std::endl;

    return 0;
}

この例では、カスタムデリータcustomDeleterが定義され、shared_ptrがスコープを抜けるときにこのデリータが呼び出されます。

shared_ptrを使用することで、複数の所有者が同じリソースを安全に共有でき、メモリ管理が自動化されます。しかし、循環参照の問題に注意し、適切にweak_ptrを使用することが重要です。

weak_ptrの使い方

weak_ptrは、shared_ptrと組み合わせて使用される補助的なスマートポインタで、共有所有権を持たずにオブジェクトへの弱い参照を提供します。これにより、shared_ptrの循環参照を防ぐことができ、メモリリークを回避することができます。weak_ptrは有効なオブジェクトが存在するかを確認し、必要に応じて一時的にshared_ptrに昇格させることができます。

基本的な使用方法

weak_ptrは、shared_ptrを観察するために使用されます。weak_ptrは参照カウントに影響を与えず、shared_ptrの有効性を確認するためのlockメソッドを提供します。

#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // weak_ptrを使用して循環参照を防ぐ

    Node(int val) : value(val) {}
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    node1->next = node2;
    node2->prev = node1; // weak_ptrを使用

    // node2のweak_ptrからshared_ptrを取得
    if (auto prev = node2->prev.lock()) {
        std::cout << "Node2's previous node value: " << prev->value << std::endl;
    } else {
        std::cout << "Node2's previous node is expired" << std::endl;
    }

    return 0;
}

この例では、Nodeクラスのprevメンバにweak_ptrを使用しています。これにより、Nodeオブジェクト間の循環参照が防がれます。node2からprevを取得する際には、lockメソッドを使用して一時的なshared_ptrに変換し、有効なオブジェクトが存在するかを確認しています。

weak_ptrの有効性の確認

weak_ptrは、オブジェクトが有効かどうかを確認するためのexpiredメソッドを提供します。また、lockメソッドを使用して有効なshared_ptrを取得できます。

#include <iostream>
#include <memory>

void checkWeakPtr(const std::weak_ptr<int>& weakPtr) {
    if (weakPtr.expired()) {
        std::cout << "The weak_ptr is expired" << std::endl;
    } else {
        std::cout << "The weak_ptr is valid" << std::endl;
        auto sharedPtr = weakPtr.lock(); // 有効なshared_ptrを取得
        if (sharedPtr) {
            std::cout << "SharedPtr value: " << *sharedPtr << std::endl;
        }
    }
}

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(50);
    std::weak_ptr<int> weakPtr = sharedPtr;

    checkWeakPtr(weakPtr);

    sharedPtr.reset(); // shared_ptrをリセットし、管理しているメモリを解放

    checkWeakPtr(weakPtr); // weak_ptrの有効性を再度確認

    return 0;
}

この例では、weak_ptrの有効性を確認するための関数checkWeakPtrが定義されています。shared_ptrがリセットされる前後で、weak_ptrの有効性を確認し、有効な場合はlockメソッドを使用してshared_ptrを取得しています。

weak_ptrを使用することで、shared_ptrの循環参照を防ぎ、メモリリークのリスクを低減できます。weak_ptrは参照カウントに影響を与えないため、安全にオブジェクトの有効性を監視し、必要に応じてshared_ptrに昇格させることができます。これにより、複雑な所有権関係を持つプログラムでも安全かつ効率的なメモリ管理が可能になります。

スマートポインタと仮想関数の組み合わせ

スマートポインタと仮想関数を組み合わせることで、メモリ管理を自動化しながら、多態性を活用した柔軟な設計を実現できます。このセクションでは、仮想関数とスマートポインタを組み合わせた設計パターンについて紹介します。

基本的な組み合わせ

スマートポインタと仮想関数を組み合わせることで、オブジェクトのライフタイム管理を容易にし、多態性を効果的に利用できます。以下の例では、Animalクラスを基底クラスとし、その派生クラスであるDogCatunique_ptrで管理します。

#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
    virtual void speak() const = 0; // 純粋仮想関数

    virtual ~Animal() = default; // 仮想デストラクタ
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Dog>());
    animals.push_back(std::make_unique<Cat>());

    for (const auto& animal : animals) {
        animal->speak(); // 多態性の利用
    }

    return 0;
}

この例では、Animalクラスに純粋仮想関数speakが定義されており、DogクラスとCatクラスがこれをオーバーライドしています。unique_ptrを使用して各オブジェクトを管理し、speakメソッドを通じて多態性を利用しています。

shared_ptrを用いたオブジェクト共有

shared_ptrを使用すると、複数の所有者が同じオブジェクトを共有でき、ライフタイム管理がさらに容易になります。以下の例では、複数のshared_ptrが同じAnimalオブジェクトを指し示します。

#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
    virtual void speak() const = 0; // 純粋仮想関数

    virtual ~Animal() = default; // 仮想デストラクタ
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    std::shared_ptr<Animal> dog = std::make_shared<Dog>();
    std::shared_ptr<Animal> cat = std::make_shared<Cat>();

    std::vector<std::shared_ptr<Animal>> animals = {dog, cat};

    for (const auto& animal : animals) {
        animal->speak(); // 多態性の利用
    }

    std::cout << "Dog use count: " << dog.use_count() << std::endl;
    std::cout << "Cat use count: " << cat.use_count() << std::endl;

    return 0;
}

この例では、shared_ptrを使用してDogCatオブジェクトを管理し、複数のshared_ptrが同じオブジェクトを指すことで共有所有権を実現しています。参照カウントはuse_countメソッドで確認できます。

循環参照の回避

スマートポインタを使用する際、循環参照を回避するためにweak_ptrを導入することが重要です。以下の例では、weak_ptrを用いてshared_ptrの循環参照を防ぎます。

#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // weak_ptrを使用して循環参照を防ぐ

    Node(int val) : value(val) {}
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    node1->next = node2;
    node2->prev = node1; // weak_ptrを使用

    // node2のweak_ptrからshared_ptrを取得
    if (auto prev = node2->prev.lock()) {
        std::cout << "Node2's previous node value: " << prev->value << std::endl;
    } else {
        std::cout << "Node2's previous node is expired" << std::endl;
    }

    return 0;
}

この例では、Nodeクラスのprevメンバにweak_ptrを使用して、循環参照を防いでいます。lockメソッドを使用して一時的にshared_ptrに変換し、有効なオブジェクトが存在するかを確認しています。

スマートポインタと仮想関数を組み合わせることで、安全で効率的なメモリ管理を実現し、多態性を活用した柔軟な設計が可能になります。これにより、複雑な所有権関係を持つプログラムでも、メモリリークや未解放メモリの問題を回避しながら、高度なオブジェクト指向プログラミングを行うことができます。

仮想関数とスマートポインタを使ったプロジェクト例

ここでは、仮想関数とスマートポインタを使った小規模なプロジェクト例を紹介します。このプロジェクトでは、様々なタイプのShape(形状)オブジェクトを管理し、それらの描画を行うプログラムを作成します。これにより、仮想関数とスマートポインタの組み合わせの実用的な使い方を理解できます。

プロジェクトの概要

このプロジェクトでは、以下のクラス構造を持つプログラムを作成します。

  • Shapeクラス: 抽象基底クラスで、純粋仮想関数drawを持つ。
  • Circleクラス: Shapeクラスを継承し、drawメソッドを実装。
  • Rectangleクラス: Shapeクラスを継承し、drawメソッドを実装。
  • ShapeManagerクラス: Shapeオブジェクトを管理し、全ての形状を描画するメソッドを持つ。

コード例

#include <iostream>
#include <memory>
#include <vector>

// 抽象基底クラス Shape
class Shape {
public:
    virtual void draw() const = 0; // 純粋仮想関数

    virtual ~Shape() = default; // 仮想デストラクタ
};

// Circleクラス
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

// Rectangleクラス
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

// ShapeManagerクラス
class ShapeManager {
private:
    std::vector<std::shared_ptr<Shape>> shapes;

public:
    void addShape(const std::shared_ptr<Shape>& shape) {
        shapes.push_back(shape);
    }

    void drawAllShapes() const {
        for (const auto& shape : shapes) {
            shape->draw(); // 多態性を利用
        }
    }
};

int main() {
    ShapeManager manager;

    // 形状オブジェクトを作成し、ShapeManagerに追加
    std::shared_ptr<Shape> circle = std::make_shared<Circle>();
    std::shared_ptr<Shape> rectangle = std::make_shared<Rectangle>();

    manager.addShape(circle);
    manager.addShape(rectangle);

    // すべての形状を描画
    manager.drawAllShapes();

    return 0;
}

解説

  1. 抽象基底クラス Shape:
    • Shapeクラスは純粋仮想関数drawを持つ抽象基底クラスです。このクラスは、具体的な形状を表すクラスの基礎となります。
  2. 具象クラス CircleRectangle:
    • CircleRectangleクラスは、Shapeクラスを継承し、drawメソッドを実装しています。これにより、Shapeクラスのインターフェースを共有しつつ、具体的な描画方法を提供します。
  3. 管理クラス ShapeManager:
    • ShapeManagerクラスは、Shapeオブジェクトを管理し、すべての形状を描画するメソッドを持ちます。shared_ptrを使用して形状オブジェクトを管理し、動的にメモリ管理を行います。
  4. メイン関数:
    • メイン関数では、ShapeManagerオブジェクトを作成し、CircleRectangleオブジェクトを追加します。その後、ShapeManagerdrawAllShapesメソッドを呼び出して、すべての形状を描画します。

このプロジェクト例では、仮想関数とスマートポインタの組み合わせにより、柔軟で拡張性のあるコードを実現しています。新しい形状クラスを追加する場合も、Shapeクラスを継承し、drawメソッドを実装するだけで簡単に拡張できます。スマートポインタを使用することで、メモリ管理が自動化され、安全性が向上します。

演習問題

仮想関数とスマートポインタを用いた実践的な演習問題を提示します。これにより、仮想関数とスマートポインタの使い方を実際に体験し、理解を深めることができます。

演習問題1: 新しい形状クラスの追加

以下の要件に基づいて、新しい形状クラスTriangleを追加してください。

  1. Shapeクラスを継承する。
  2. drawメソッドをオーバーライドして、「Drawing a triangle」と出力する。
  3. ShapeManagerTriangleオブジェクトを追加し、すべての形状を描画する。

ヒント

  • Triangleクラスの定義
  • ShapeManagerのインスタンスにTriangleオブジェクトを追加

解答例

#include <iostream>
#include <memory>
#include <vector>

// 抽象基底クラス Shape
class Shape {
public:
    virtual void draw() const = 0; // 純粋仮想関数

    virtual ~Shape() = default; // 仮想デストラクタ
};

// Circleクラス
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

// Rectangleクラス
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

// 新しい Triangle クラス
class Triangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a triangle" << std::endl;
    }
};

// ShapeManagerクラス
class ShapeManager {
private:
    std::vector<std::shared_ptr<Shape>> shapes;

public:
    void addShape(const std::shared_ptr<Shape>& shape) {
        shapes.push_back(shape);
    }

    void drawAllShapes() const {
        for (const auto& shape : shapes) {
            shape->draw(); // 多態性を利用
        }
    }
};

int main() {
    ShapeManager manager;

    // 形状オブジェクトを作成し、ShapeManagerに追加
    std::shared_ptr<Shape> circle = std::make_shared<Circle>();
    std::shared_ptr<Shape> rectangle = std::make_shared<Rectangle>();
    std::shared_ptr<Shape> triangle = std::make_shared<Triangle>(); // Triangleオブジェクトを追加

    manager.addShape(circle);
    manager.addShape(rectangle);
    manager.addShape(triangle); // ShapeManagerに追加

    // すべての形状を描画
    manager.drawAllShapes();

    return 0;
}

演習問題2: スマートポインタのカスタムデリータ

以下の要件に基づいて、Circleクラスにカスタムデリータを適用してください。

  1. カスタムデリータ関数を定義する。
  2. Circleオブジェクトを作成する際にカスタムデリータを指定する。
  3. Circleオブジェクトが解放されるときにカスタムメッセージを出力する。

ヒント

  • カスタムデリータ関数の定義
  • shared_ptrのカスタムデリータ指定方法

解答例

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual void draw() const = 0; // 純粋仮想関数
    virtual ~Shape() = default; // 仮想デストラクタ
};

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

// カスタムデリータ関数
void customCircleDeleter(Circle* ptr) {
    std::cout << "Custom deleter for Circle called" << std::endl;
    delete ptr;
}

class ShapeManager {
private:
    std::vector<std::shared_ptr<Shape>> shapes;

public:
    void addShape(const std::shared_ptr<Shape>& shape) {
        shapes.push_back(shape);
    }

    void drawAllShapes() const {
        for (const auto& shape : shapes) {
            shape->draw(); // 多態性を利用
        }
    }
};

int main() {
    ShapeManager manager;

    // カスタムデリータを指定してCircleオブジェクトを作成
    std::shared_ptr<Shape> circle(new Circle(), customCircleDeleter);

    manager.addShape(circle);

    // すべての形状を描画
    manager.drawAllShapes();

    return 0;
}

演習問題3: weak_ptrの利用

以下の要件に基づいて、Nodeクラスを改良し、weak_ptrを使用して循環参照を防いでください。

  1. Nodeクラスにstd::weak_ptr<Node>型のメンバ変数prevを追加する。
  2. main関数でNodeオブジェクトを作成し、循環参照を設定する。
  3. weak_ptrを使用してprevノードが有効かどうかを確認する。

ヒント

  • weak_ptrの宣言
  • weak_ptrlockメソッドの使用

解答例

#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // weak_ptrを使用して循環参照を防ぐ

    Node(int val) : value(val) {}
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    node1->next = node2;
    node2->prev = node1; // weak_ptrを使用

    // node2のweak_ptrからshared_ptrを取得
    if (auto prev = node2->prev.lock()) {
        std::cout << "Node2's previous node value: " << prev->value << std::endl;
    } else {
        std::cout << "Node2's previous node is expired" << std::endl;
    }

    return 0;
}

これらの演習問題を通じて、仮想関数とスマートポインタの実践的な使い方を学ぶことができます。各問題を解くことで、C++プログラミングの理解が深まります。

まとめ

本記事では、C++における仮想関数とスマートポインタの基本概念から具体的な使用方法までを詳しく解説しました。仮想関数は、多態性を実現し、柔軟で拡張性のあるコードを書くための重要な機能です。一方、スマートポインタはメモリ管理を自動化し、安全性を高めるための強力なツールです。

仮想関数を使用することで、基底クラスのインターフェースを利用して、異なる派生クラスのオブジェクトを統一的に扱うことができます。また、純粋仮想関数と抽象クラスを活用することで、明確な設計を行い、派生クラスに具体的な実装を強制することが可能です。

スマートポインタを利用することで、手動でのメモリ管理の煩雑さを軽減し、メモリリークや未解放メモリの問題を防ぐことができます。特に、unique_ptrshared_ptr、およびweak_ptrの使い分けが重要であり、それぞれの特性を理解し、適切に活用することが求められます。

また、仮想関数とスマートポインタを組み合わせることで、メモリ管理と多態性を両立させた柔軟な設計が可能となります。実践的なプロジェクト例や演習問題を通じて、これらの概念を深く理解し、応用する力を身につけることができました。

今後の学習においても、実際のコードを書きながらこれらの概念を繰り返し復習し、理解を深めていくことが重要です。仮想関数とスマートポインタを効果的に活用することで、C++プログラミングのスキルをさらに向上させることができるでしょう。

コメント

コメントする

目次