【C++】クラスの使い方について詳しく解説

この記事では、C++プログラミングにおける「クラス」について詳しく解説します。

クラスは、データとその操作を一つにまとめたもので、プログラムを整理しやすくするための重要な概念です。

初心者の方でも理解しやすいように、クラスの基本から実際の使い方まで、具体的な例を交えて説明します。

この記事を読むことで、クラスの定義や使い方、継承やオーバーロードなど、C++のクラスに関する基本的な知識を身につけることができます。

目次から探す

クラスとは何か

C++におけるクラスは、オブジェクト指向プログラミング(OOP)の中心的な概念です。

クラスは、データとそのデータを操作する関数を一つの単位としてまとめたものです。

これにより、プログラムの構造を整理し、再利用性や保守性を高めることができます。

クラスの基本概念

クラスは、オブジェクトの設計図のようなものです。

クラスを定義することで、そのクラスに基づいたオブジェクトを作成することができます。

クラスは、以下のような要素を持ちます。

  • メンバ変数: クラス内で定義される変数。

オブジェクトの状態を保持します。

  • メンバ関数: クラス内で定義される関数。

オブジェクトの動作を定義します。

  • アクセス修飾子: メンバ変数やメンバ関数のアクセス範囲を指定します(public, private, protected)。

以下に、簡単なクラスの例を示します。

#include <iostream>
using namespace std;
class Person {
public:
    string name;
    int age;
    void introduce() {
        cout << "名前: " << name << ", 年齢: " << age << "歳" << endl;
    }
};
int main() {
    Person person;
    person.name = "太郎";
    person.age = 30;
    person.introduce();
    return 0;
}

この例では、Personというクラスを定義し、その中にnameageというメンバ変数、introduceというメンバ関数を持たせています。

main関数内でPersonクラスのオブジェクトを作成し、メンバ変数に値を設定してからintroduce関数を呼び出しています。

オブジェクト指向プログラミング(OOP)におけるクラスの役割

オブジェクト指向プログラミング(OOP)は、プログラムをオブジェクトの集合として捉える考え方です。

クラスは、このオブジェクトを定義するためのテンプレートとして機能します。

OOPの主な特徴には以下のものがあります。

  • カプセル化: データとその操作を一つの単位(クラス)にまとめることで、データの隠蔽と保護を実現します。
  • 継承: 既存のクラスを基に新しいクラスを作成することで、コードの再利用性を高めます。
  • ポリモーフィズム: 同じインターフェースを持つ異なるクラスのオブジェクトを同一視することで、柔軟なコード設計を可能にします。

以下に、継承とポリモーフィズムの簡単な例を示します。

#include <iostream>
using namespace std;
class Animal {
public:
    virtual void speak() {
        cout << "動物の鳴き声" << endl;
    }
};
class Dog : public Animal {
public:
    void speak() override {
        cout << "ワンワン" << endl;
    }
};
class Cat : public Animal {
public:
    void speak() override {
        cout << "ニャーニャー" << endl;
    }
};
void makeAnimalSpeak(Animal &animal) {
    animal.speak();
}
int main() {
    Dog dog;
    Cat cat;
    makeAnimalSpeak(dog);
    makeAnimalSpeak(cat);
    return 0;
}

この例では、Animalという基底クラスを定義し、その中にvirtual関数speakを持たせています。

DogCatAnimalを継承し、それぞれの speak関数をオーバーライドしています。

makeAnimalSpeak関数は、Animal型の参照を受け取り、speak関数を呼び出します。

これにより、DogCatのオブジェクトを渡しても、それぞれの speak関数が呼び出されます。

このように、クラスはOOPの基本的な構成要素であり、プログラムの構造を整理し、再利用性や保守性を高めるために重要な役割を果たします。

クラスの定義と宣言

クラスの基本構文

C++におけるクラスの定義は、classキーワードを使用して行います。

クラスは、データ(メンバ変数)とそのデータを操作する関数(メンバ関数)をまとめたものです。

以下に基本的なクラスの構文を示します。

class クラス名 {
public:
    // パブリックメンバ
    型 メンバ変数;
    型 メンバ関数(引数リスト);
    
private:
    // プライベートメンバ
    型 メンバ変数;
    型 メンバ関数(引数リスト);
};

メンバ変数とメンバ関数

クラス内には、データを保持するためのメンバ変数と、そのデータを操作するためのメンバ関数を定義します。

以下に具体例を示します。

class Person {
public:
    // パブリックメンバ変数
    std::string name;
    
    // パブリックメンバ関数
    void setName(std::string newName) {
        name = newName;
    }
    
    std::string getName() {
        return name;
    }
    
private:
    // プライベートメンバ変数
    int age;
    
    // プライベートメンバ関数
    void setAge(int newAge) {
        age = newAge;
    }
    
    int getAge() {
        return age;
    }
};

この例では、Personクラスが定義されています。

nameはパブリックメンバ変数であり、外部から直接アクセスできます。

一方、ageはプライベートメンバ変数であり、外部から直接アクセスすることはできません。

アクセス修飾子(public, private, protected)

クラス内のメンバには、アクセス修飾子を使用してアクセスレベルを指定できます。

主なアクセス修飾子には以下の3つがあります。

  • public: パブリックメンバは、クラスの外部からもアクセス可能です。
  • private: プライベートメンバは、クラスの外部からはアクセスできません。

クラス内のメンバ関数からのみアクセス可能です。

  • protected: プロテクトメンバは、クラス自身とその派生クラスからアクセス可能です。

以下にアクセス修飾子の使用例を示します。

class Example {
public:
    int publicVar; // パブリックメンバ変数
    
    void publicMethod() {
        // パブリックメンバ関数
    }
    
private:
    int privateVar; // プライベートメンバ変数
    
    void privateMethod() {
        // プライベートメンバ関数
    }
    
protected:
    int protectedVar; // プロテクトメンバ変数
    
    void protectedMethod() {
        // プロテクトメンバ関数
    }
};

この例では、publicVarpublicMethodはパブリックメンバであり、クラスの外部からもアクセス可能です。

一方、privateVarprivateMethodはプライベートメンバであり、クラスの外部からはアクセスできません。

protectedVarprotectedMethodはプロテクトメンバであり、クラス自身とその派生クラスからアクセス可能です。

以上が、クラスの定義と宣言に関する基本的な内容です。

次に、コンストラクタとデストラクタについて詳しく解説します。

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

コンストラクタの役割と使い方

コンストラクタは、クラスのインスタンス(オブジェクト)が生成される際に自動的に呼び出される特殊なメンバ関数です。

コンストラクタの主な役割は、オブジェクトの初期化を行うことです。

例えば、メンバ変数に初期値を設定したり、リソースの確保を行ったりします。

コンストラクタの名前はクラス名と同じで、戻り値を持ちません。

以下に基本的なコンストラクタの例を示します。

#include <iostream>
class MyClass {
public:
    int value;
    // コンストラクタ
    MyClass() {
        value = 0;
        std::cout << "コンストラクタが呼び出されました。" << std::endl;
    }
};
int main() {
    MyClass obj; // コンストラクタが呼び出される
    std::cout << "obj.value: " << obj.value << std::endl;
    return 0;
}

この例では、MyClassのコンストラクタがオブジェクトobjの生成時に呼び出され、valueが0に初期化されます。

デフォルトコンストラクタと引数付きコンストラクタ

デフォルトコンストラクタは、引数を持たないコンストラクタのことを指します。

一方、引数付きコンストラクタは、オブジェクト生成時に初期化のための引数を受け取るコンストラクタです。

以下にデフォルトコンストラクタと引数付きコンストラクタの例を示します。

#include <iostream>
class MyClass {
public:
    int value;
    // デフォルトコンストラクタ
    MyClass() {
        value = 0;
        std::cout << "デフォルトコンストラクタが呼び出されました。" << std::endl;
    }
    // 引数付きコンストラクタ
    MyClass(int val) {
        value = val;
        std::cout << "引数付きコンストラクタが呼び出されました。" << std::endl;
    }
};
int main() {
    MyClass obj1; // デフォルトコンストラクタが呼び出される
    MyClass obj2(10); // 引数付きコンストラクタが呼び出される
    std::cout << "obj1.value: " << obj1.value << std::endl;
    std::cout << "obj2.value: " << obj2.value << std::endl;
    return 0;
}

この例では、obj1はデフォルトコンストラクタによって生成され、valueは0に初期化されます。

一方、obj2は引数付きコンストラクタによって生成され、valueは10に初期化されます。

デストラクタの役割と使い方

デストラクタは、クラスのインスタンスが破棄される際に自動的に呼び出される特殊なメンバ関数です。

デストラクタの主な役割は、リソースの解放やクリーンアップ処理を行うことです。

例えば、動的に確保したメモリの解放やファイルのクローズなどを行います。

デストラクタの名前はクラス名の前にチルダ(~)を付けたもので、戻り値を持ちません。

また、引数を取ることもできません。

以下にデストラクタの例を示します。

#include <iostream>
class MyClass {
public:
    int* ptr;
    // コンストラクタ
    MyClass(int val) {
        ptr = new int(val);
        std::cout << "コンストラクタが呼び出されました。" << std::endl;
    }
    // デストラクタ
    ~MyClass() {
        delete ptr;
        std::cout << "デストラクタが呼び出されました。" << std::endl;
    }
};
int main() {
    MyClass obj(10); // コンストラクタが呼び出される
    std::cout << "obj.ptr: " << *(obj.ptr) << std::endl;
    // デストラクタが自動的に呼び出される
    return 0;
}

この例では、MyClassのコンストラクタがオブジェクトobjの生成時に呼び出され、動的にメモリを確保します。

プログラムの終了時にデストラクタが自動的に呼び出され、確保したメモリを解放します。

コンストラクタとデストラクタを適切に使うことで、オブジェクトの初期化とクリーンアップを自動化し、コードの安全性と可読性を向上させることができます。

メンバ関数の定義と実装

メンバ関数の宣言と定義

クラスのメンバ関数は、クラスの内部で宣言し、クラスの外部で定義することが一般的です。

メンバ関数の宣言はクラスの定義内で行い、実際の処理内容はクラスの外部で定義します。

以下に、メンバ関数の宣言と定義の例を示します。

#include <iostream>
using namespace std;
class MyClass {
public:
    void display(); // メンバ関数の宣言
};
// メンバ関数の定義
void MyClass::display() {
    cout << "Hello, World!" << endl;
}
int main() {
    MyClass obj;
    obj.display(); // メンバ関数の呼び出し
    return 0;
}

この例では、MyClassというクラスを定義し、その中にdisplayというメンバ関数を宣言しています。

メンバ関数の定義はクラスの外部で行い、MyClass::displayという形式で定義しています。

インライン関数と非インライン関数

メンバ関数は、クラスの内部で定義することもできます。

この場合、そのメンバ関数はインライン関数として扱われます。

インライン関数は、関数呼び出しのオーバーヘッドを減らすために、コンパイラによって関数の呼び出し部分に直接展開されます。

以下に、インライン関数の例を示します。

#include <iostream>
using namespace std;
class MyClass {
public:
    void display() { // クラス内で定義されたインライン関数
        cout << "Hello, World!" << endl;
    }
};
int main() {
    MyClass obj;
    obj.display(); // メンバ関数の呼び出し
    return 0;
}

この例では、displayメンバ関数がクラスの内部で定義されており、インライン関数として扱われます。

一方、非インライン関数はクラスの外部で定義されるメンバ関数です。

非インライン関数は通常の関数呼び出しとして扱われ、関数呼び出しのオーバーヘッドが発生します。

constメンバ関数

constメンバ関数は、メンバ関数がオブジェクトの状態を変更しないことを保証します。

constメンバ関数は、関数宣言の末尾にconstキーワードを付けることで定義されます。

以下に、constメンバ関数の例を示します。

#include <iostream>
using namespace std;
class MyClass {
private:
    int value;
public:
    MyClass(int v) : value(v) {}
    void display() const { // constメンバ関数
        cout << "Value: " << value << endl;
    }
};
int main() {
    MyClass obj(10);
    obj.display(); // constメンバ関数の呼び出し
    return 0;
}

この例では、displayメンバ関数がconstメンバ関数として定義されています。

この関数は、オブジェクトの状態を変更しないことを保証します。

constメンバ関数は、constオブジェクトからも呼び出すことができます。

const MyClass obj(10);
obj.display(); // constオブジェクトからの呼び出し

このように、constメンバ関数はオブジェクトの状態を変更しないことを保証するため、安全なコードを書くために役立ちます。

クラスの継承

継承の基本概念

継承は、既存のクラス(基底クラスまたは親クラス)を基にして新しいクラス(派生クラスまたは子クラス)を作成する機能です。

これにより、コードの再利用性が向上し、オブジェクト指向プログラミングの重要な概念である「多態性(ポリモーフィズム)」を実現できます。

基底クラスと派生クラス

基底クラスは、他のクラスに継承されるクラスです。

派生クラスは、基底クラスを継承して新たに作成されるクラスです。

派生クラスは基底クラスのメンバ変数やメンバ関数を引き継ぎ、さらに独自のメンバを追加することができます。

以下に、基底クラスと派生クラスの基本的な例を示します。

#include <iostream>
// 基底クラス
class Animal {
public:
    void eat() {
        std::cout << "Eating..." << std::endl;
    }
};
// 派生クラス
class Dog : public Animal {
public:
    void bark() {
        std::cout << "Barking..." << std::endl;
    }
};
int main() {
    Dog myDog;
    myDog.eat();  // 基底クラスのメソッドを呼び出す
    myDog.bark(); // 派生クラスのメソッドを呼び出す
    return 0;
}

この例では、Animalクラスが基底クラスであり、Dogクラスがそれを継承した派生クラスです。

DogクラスAnimalクラスeatメソッドを利用でき、さらに独自のbarkメソッドを持っています。

アクセス修飾子と継承

継承には3つのアクセス修飾子(public、protected、private)があり、それぞれの修飾子によって基底クラスのメンバのアクセスレベルが異なります。

  • public継承:基底クラスのpublicメンバは派生クラスでもpublic、protectedメンバはprotectedのまま。
  • protected継承:基底クラスのpublicメンバとprotectedメンバは派生クラスでprotectedになる。
  • private継承:基底クラスのpublicメンバとprotectedメンバは派生クラスでprivateになる。

以下に、public継承の例を示します。

#include <iostream>
class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};
class Derived : public Base {
public:
    void show() {
        publicVar = 1;       // OK
        protectedVar = 2;    // OK
        // privateVar = 3;   // エラー:privateメンバにはアクセスできない
    }
};
int main() {
    Derived obj;
    obj.publicVar = 10;      // OK
    // obj.protectedVar = 20; // エラー:protectedメンバにはアクセスできない
    return 0;
}

仮想関数と純粋仮想関数

仮想関数は、基底クラスで宣言され、派生クラスでオーバーライドされることを期待される関数です。

仮想関数を使うことで、ポリモーフィズムを実現できます。

以下に、仮想関数の例を示します。

#include <iostream>
class Animal {
public:
    virtual void makeSound() {
        std::cout << "Some generic animal sound" << std::endl;
    }
};
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Bark" << std::endl;
    }
};
int main() {
    Animal* animal = new Dog();
    animal->makeSound(); // "Bark"が出力される
    delete animal;
    return 0;
}

純粋仮想関数は、基底クラスで宣言されるが、実装が提供されない関数です。

純粋仮想関数を持つクラスは抽象クラスとなり、インスタンス化できません。

以下に、純粋仮想関数の例を示します。

#include <iostream>
class Animal {
public:
    virtual void makeSound() = 0; // 純粋仮想関数
};
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Bark" << std::endl;
    }
};
int main() {
    Animal* animal = new Dog();
    animal->makeSound(); // "Bark"が出力される
    delete animal;
    return 0;
}

この例では、Animalクラスは純粋仮想関数makeSoundを持つ抽象クラスです。

DogクラスAnimalクラスを継承し、makeSound関数をオーバーライドしています。

オーバーロードとオーバーライド

C++では、同じ名前の関数や演算子を異なる形で使うことができる「オーバーロード」と、基底クラスの関数を派生クラスで再定義する「オーバーライド」があります。

これらの機能を使うことで、コードの可読性や再利用性を高めることができます。

関数のオーバーロード

関数のオーバーロードとは、同じ名前の関数を異なる引数リストで定義することです。

これにより、同じ機能を持つが異なる引数を取る関数を一つの名前でまとめることができます。

#include <iostream>
// 関数のオーバーロード例
void print(int i) {
    std::cout << "整数: " << i << std::endl;
}
void print(double d) {
    std::cout << "浮動小数点数: " << d << std::endl;
}
void print(const std::string& s) {
    std::cout << "文字列: " << s << std::endl;
}
int main() {
    print(10);          // 整数: 10
    print(3.14);        // 浮動小数点数: 3.14
    print("Hello");     // 文字列: Hello
    return 0;
}

この例では、printという名前の関数が3つ定義されていますが、それぞれ異なる引数を取ります。

これにより、異なる型のデータを同じ関数名で出力することができます。

関数のオーバーライド

関数のオーバーライドとは、基底クラスで定義された関数を派生クラスで再定義することです。

オーバーライドを行うことで、派生クラスで基底クラスの関数の動作を変更することができます。

#include <iostream>
class Base {
public:
    virtual void show() const {
        std::cout << "Baseクラスのshow関数" << std::endl;
    }
};
class Derived : public Base {
public:
    void show() const override {
        std::cout << "Derivedクラスのshow関数" << std::endl;
    }
};
int main() {
    Base base;
    Derived derived;
    Base* ptr = &derived;
    ptr->show();  // Derivedクラスのshow関数
    return 0;
}

この例では、Baseクラスshow関数Derivedクラスでオーバーライドされています。

Baseクラスのポインタを使ってDerivedクラスのオブジェクトを指すと、オーバーライドされたDerivedクラスshow関数が呼び出されます。

演算子のオーバーロード

演算子のオーバーロードとは、C++の標準演算子をユーザー定義の型に対して再定義することです。

これにより、クラスのオブジェクトに対して直感的な操作を行うことができます。

#include <iostream>
class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r, double i) : real(r), imag(i) {}
    // 演算子+のオーバーロード
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    void display() const {
        std::cout << "(" << real << ", " << imag << ")" << std::endl;
    }
};
int main() {
    Complex c1(1.0, 2.0);
    Complex c2(3.0, 4.0);
    Complex c3 = c1 + c2;
    c3.display();  // (4.0, 6.0)
    return 0;
}

この例では、Complexクラスに対して+演算子がオーバーロードされています。

これにより、Complexオブジェクト同士を+演算子で加算することができます。

オーバーロードとオーバーライドを適切に使うことで、C++のプログラムをより柔軟で直感的にすることができます。

クラスの特殊メンバ関数

C++では、クラスの特殊メンバ関数として「コピーコンストラクタ」、「ムーブコンストラクタ」、「コピー代入演算子」、「ムーブ代入演算子」があります。

これらの関数は、オブジェクトのコピーやムーブ(移動)を行う際に自動的に呼び出されるため、クラスの動作を理解する上で非常に重要です。

コピーコンストラクタ

コピーコンストラクタは、既存のオブジェクトを使って新しいオブジェクトを初期化するためのコンストラクタです。

コピーコンストラクタは、同じクラスの別のオブジェクトを引数として受け取ります。

class MyClass {
public:
    int value;
    // コピーコンストラクタ
    MyClass(const MyClass& other) {
        value = other.value;
    }
};
int main() {
    MyClass obj1;
    obj1.value = 10;
    // コピーコンストラクタが呼ばれる
    MyClass obj2 = obj1;
    std::cout << "obj1.value: " << obj1.value << std::endl;
    std::cout << "obj2.value: " << obj2.value << std::endl;
    return 0;
}

この例では、obj1の値を使ってobj2が初期化されます。

コピーコンストラクタが呼ばれることで、obj2.valueobj1.valueと同じ値になります。

ムーブコンストラクタ

ムーブコンストラクタは、既存のオブジェクトのリソースを新しいオブジェクトに移動するためのコンストラクタです。

ムーブコンストラクタは、右辺値参照(rvalue reference)を引数として受け取ります。

class MyClass {
public:
    int* data;
    // コンストラクタ
    MyClass(int size) {
        data = new int[size];
    }
    // ムーブコンストラクタ
    MyClass(MyClass&& other) noexcept {
        data = other.data;
        other.data = nullptr;
    }
    ~MyClass() {
        delete[] data;
    }
};
int main() {
    MyClass obj1(10);
    // ムーブコンストラクタが呼ばれる
    MyClass obj2 = std::move(obj1);
    return 0;
}

この例では、obj1のリソースがobj2に移動されます。

ムーブコンストラクタが呼ばれることで、obj1.datanullptrに設定され、obj2.dataobj1.dataが指していたメモリを指します。

コピー代入演算子

コピー代入演算子は、既存のオブジェクトに別のオブジェクトの値を代入するための演算子です。

コピー代入演算子は、同じクラスの別のオブジェクトを引数として受け取ります。

class MyClass {
public:
    int value;
    // コピー代入演算子
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            value = other.value;
        }
        return *this;
    }
};
int main() {
    MyClass obj1;
    obj1.value = 10;
    MyClass obj2;
    obj2.value = 20;
    // コピー代入演算子が呼ばれる
    obj2 = obj1;
    std::cout << "obj1.value: " << obj1.value << std::endl;
    std::cout << "obj2.value: " << obj2.value << std::endl;
    return 0;
}

この例では、obj1の値がobj2に代入されます。

コピー代入演算子が呼ばれることで、obj2.valueobj1.valueと同じ値になります。

ムーブ代入演算子

ムーブ代入演算子は、既存のオブジェクトに別のオブジェクトのリソースを移動するための演算子です。

ムーブ代入演算子は、右辺値参照(rvalue reference)を引数として受け取ります。

class MyClass {
public:
    int* data;
    // コンストラクタ
    MyClass(int size) {
        data = new int[size];
    }
    // ムーブ代入演算子
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
    ~MyClass() {
        delete[] data;
    }
};
int main() {
    MyClass obj1(10);
    MyClass obj2(20);
    // ムーブ代入演算子が呼ばれる
    obj2 = std::move(obj1);
    return 0;
}

この例では、obj1のリソースがobj2に移動されます。

ムーブ代入演算子が呼ばれることで、obj1.datanullptrに設定され、obj2.dataobj1.dataが指していたメモリを指します。

これらの特殊メンバ関数を適切に実装することで、クラスのオブジェクトが安全かつ効率的にコピーやムーブされるようになります。

クラスとメモリ管理

C++では、メモリ管理が非常に重要な役割を果たします。

特に、動的メモリ管理はプログラムの効率性と安定性に大きな影響を与えます。

ここでは、動的メモリ管理とRAII、そしてスマートポインタの利用について詳しく解説します。

動的メモリ管理とRAII

動的メモリ管理とは、プログラムの実行中に必要なメモリを動的に確保し、不要になったら解放することを指します。

C++では、new演算子delete演算子を使って動的メモリを管理します。

int* ptr = new int; // メモリの動的確保
*ptr = 10; // メモリに値を設定
delete ptr; // メモリの解放

しかし、動的メモリ管理は非常にエラーが発生しやすい部分でもあります。

メモリリークや二重解放などの問題が発生する可能性があります。

これを防ぐために、C++ではRAII(Resource Acquisition Is Initialization)という設計原則が推奨されています。

RAIIは、リソースの取得と解放をオブジェクトのライフサイクルに結びつけることで、リソース管理を自動化する手法です。

具体的には、コンストラクタでリソースを取得し、デストラクタでリソースを解放します。

class Resource {
public:
    Resource() {
        ptr = new int;
        std::cout << "Resource acquired" << std::endl;
    }
    ~Resource() {
        delete ptr;
        std::cout << "Resource released" << std::endl;
    }
private:
    int* ptr;
};
void useResource() {
    Resource res; // コンストラクタでリソースを取得
    // リソースを使用する処理
} // デストラクタでリソースを解放

スマートポインタの利用(std::unique_ptr, std::shared_ptr)

C++11以降では、スマートポインタという便利な機能が追加されました。

スマートポインタは、動的メモリ管理を自動化し、メモリリークや二重解放のリスクを大幅に減少させます。

代表的なスマートポインタには、std::unique_ptrstd::shared_ptrがあります。

std::unique_ptr

std::unique_ptrは、所有権が一意であることを保証するスマートポインタです。

所有権を他のポインタに移すことはできますが、同時に複数のポインタが同じリソースを所有することはできません。

#include <memory>
#include <iostream>
void useUniquePtr() {
    std::unique_ptr<int> ptr1(new int(10));
    std::cout << *ptr1 << std::endl;
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 所有権の移動
    if (!ptr1) {
        std::cout << "ptr1 is null" << std::endl;
    }
    std::cout << *ptr2 << std::endl;
} // ptr2がスコープを抜けるときにメモリが解放される

std::shared_ptr

std::shared_ptrは、複数のポインタが同じリソースを共有することを許可するスマートポインタです。

リソースは、最後のstd::shared_ptrが破棄されるときに自動的に解放されます。

#include <memory>
#include <iostream>
void useSharedPtr() {
    std::shared_ptr<int> ptr1(new int(20));
    std::cout << *ptr1 << std::endl;
    std::shared_ptr<int> ptr2 = ptr1; // 所有権の共有
    std::cout << *ptr2 << std::endl;
    std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl;
} // 最後のshared_ptrがスコープを抜けるときにメモリが解放される

スマートポインタを利用することで、手動でdeleteを呼び出す必要がなくなり、メモリ管理が大幅に簡素化されます。

これにより、プログラムの安全性と可読性が向上します。

テンプレートクラス

テンプレートクラスの基本

テンプレートクラスは、データ型に依存しない汎用的なクラスを作成するための機能です。

C++のテンプレートは、関数テンプレートとクラステンプレートの2種類がありますが、ここではクラステンプレートについて詳しく解説します。

テンプレートクラスを使うことで、同じロジックを異なるデータ型に対して適用することができます。

これにより、コードの再利用性が向上し、冗長なコードを減らすことができます。

クラステンプレートの定義と使用

クラステンプレートを定義するには、templateキーワードを使用します。

以下に、基本的なクラステンプレートの定義と使用例を示します。

#include <iostream>
// クラステンプレートの定義
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T d) : data(d) {}
    void display() {
        std::cout << "Data: " << data << std::endl;
    }
};
int main() {
    // int型のMyClassインスタンスを作成
    MyClass<int> intObj(10);
    intObj.display(); // 出力: Data: 10
    // double型のMyClassインスタンスを作成
    MyClass<double> doubleObj(3.14);
    doubleObj.display(); // 出力: Data: 3.14
    // std::string型のMyClassインスタンスを作成
    MyClass<std::string> stringObj("Hello");
    stringObj.display(); // 出力: Data: Hello
    return 0;
}

この例では、MyClassというクラステンプレートを定義しています。

Tはテンプレートパラメータで、クラスのデータ型を指定するために使用されます。

main関数では、intdoublestd::string型のインスタンスを作成し、それぞれのデータを表示しています。

テンプレートの特殊化

テンプレートの特殊化は、特定のデータ型に対して異なる実装を提供するための機能です。

これにより、特定の型に対して最適化されたコードを書くことができます。

以下に、クラステンプレートの部分特殊化の例を示します。

#include <iostream>
// クラステンプレートの定義
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T d) : data(d) {}
    void display() {
        std::cout << "Data: " << data << std::endl;
    }
};
// std::string型に対する特殊化
template <>
class MyClass<std::string> {
private:
    std::string data;
public:
    MyClass(std::string d) : data(d) {}
    void display() {
        std::cout << "String Data: " << data << std::endl;
    }
};
int main() {
    // int型のMyClassインスタンスを作成
    MyClass<int> intObj(10);
    intObj.display(); // 出力: Data: 10
    // std::string型のMyClassインスタンスを作成
    MyClass<std::string> stringObj("Hello");
    stringObj.display(); // 出力: String Data: Hello
    return 0;
}

この例では、std::string型に対して特殊化されたMyClassを定義しています。

std::string型の場合、displayメソッドの出力が異なることに注目してください。

テンプレートクラスを使うことで、コードの再利用性が向上し、特定のデータ型に対して最適化された実装を提供することができます。

これにより、効率的で柔軟なプログラムを作成することが可能になります。

名前空間とクラス

名前空間の基本概念

C++では、名前空間(namespace)を使用して、識別子(変数名、関数名、クラス名など)の衝突を避けることができます。

名前空間は、コードを論理的にグループ化し、異なるライブラリやモジュール間で同じ名前の識別子が存在する場合でも、それらを区別するために使用されます。

名前空間の基本的な構文は以下の通りです。

namespace 名前空間名 {
    // 名前空間内のコード
}

例えば、以下のように定義します。

namespace MyNamespace {
    int myVariable = 10;
    void myFunction() {
        // 関数の実装
    }
}

この場合、myVariablemyFunctionMyNamespaceという名前空間に属します。

名前空間内の要素にアクセスするには、スコープ解決演算子(::)を使用します。

int main() {
    // 名前空間内の変数にアクセス
    std::cout << MyNamespace::myVariable << std::endl;
    // 名前空間内の関数を呼び出し
    MyNamespace::myFunction();
    return 0;
}

名前空間とクラスの組み合わせ

名前空間はクラスと組み合わせて使用することができます。

これにより、クラス名の衝突を避けることができ、コードの可読性と保守性が向上します。

以下に、名前空間とクラスを組み合わせた例を示します。

namespace MyNamespace {
    class MyClass {
    public:
        void display() {
            std::cout << "Hello from MyClass in MyNamespace!" << std::endl;
        }
    };
}
int main() {
    // 名前空間内のクラスを使用
    MyNamespace::MyClass obj;
    obj.display();
    return 0;
}

この例では、MyNamespaceという名前空間内にMyClassというクラスを定義しています。

main関数内でこのクラスを使用する際には、MyNamespace::MyClassと記述することで、名前空間内のクラスにアクセスできます。

また、名前空間を使用することで、同じ名前のクラスを異なる名前空間内に定義することができます。

以下にその例を示します。

namespace NamespaceA {
    class MyClass {
    public:
        void display() {
            std::cout << "Hello from MyClass in NamespaceA!" << std::endl;
        }
    };
}
namespace NamespaceB {
    class MyClass {
    public:
        void display() {
            std::cout << "Hello from MyClass in NamespaceB!" << std::endl;
        }
    };
}
int main() {
    // NamespaceA内のMyClassを使用
    NamespaceA::MyClass objA;
    objA.display();
    // NamespaceB内のMyClassを使用
    NamespaceB::MyClass objB;
    objB.display();
    return 0;
}

この例では、NamespaceANamespaceBという異なる名前空間内に同じ名前のMyClassを定義しています。

それぞれの名前空間内のクラスを使用する際には、名前空間を明示することで区別できます。

名前空間を適切に使用することで、コードの構造を整理し、名前の衝突を避けることができます。

特に大規模なプロジェクトや複数のライブラリを使用する場合に有効です。

実践例:簡単なクラスの実装

ここでは、実際にC++でクラスを実装し、その利用方法を解説します。

具体的な例を通じて、クラスの設計から実装、利用までの流れを理解しましょう。

クラスの設計

まず、クラスの設計を行います。

ここでは、簡単な「学生(Student)」クラスを設計します。

このクラスは、学生の名前、年齢、成績を管理するためのものです。

設計するクラスの要件は以下の通りです:

メンバ変数名データ型説明
namestd::string学生の名前を保持する
ageint学生の年齢を保持する
gradedouble学生の成績を保持する
メンバ関数名引数戻り値説明
setInfo名前(std::string)
年齢(int)
成績(double)
なし学生の情報を設定する
displayInfoなしなし学生の情報を表示する

クラスの実装例

次に、設計に基づいてクラスを実装します。

以下に「学生(Student)」クラスの実装例を示します。

#include <iostream>
#include <string>
class Student {
private:
    std::string name;
    int age;
    double grade;
public:
    // コンストラクタ
    Student(std::string n, int a, double g) : name(n), age(a), grade(g) {}
    // メンバ関数:学生の情報を設定する
    void setInfo(std::string n, int a, double g) {
        name = n;
        age = a;
        grade = g;
    }
    // メンバ関数:学生の情報を表示する
    void displayInfo() const {
        std::cout << "Name: " << name << std::endl;
        std::cout << "Age: " << age << std::endl;
        std::cout << "Grade: " << grade << std::endl;
    }
};

クラスの利用例

次に、実装したクラスを利用してみましょう。

以下に「学生(Student)」クラスを利用する例を示します。

int main() {
    // Studentオブジェクトの作成
    Student student1("Alice", 20, 85.5);
    // 学生の情報を表示
    student1.displayInfo();
    // 学生の情報を更新
    student1.setInfo("Bob", 21, 90.0);
    // 更新後の学生の情報を表示
    student1.displayInfo();
    return 0;
}

このプログラムを実行すると、以下のような出力が得られます。

Name: Alice
Age: 20
Grade: 85.5
Name: Bob
Age: 21
Grade: 90.0

クラスの重要性と利点

クラスを使用することで、以下のような利点があります:

  1. データのカプセル化:クラスはデータとその操作を一つの単位にまとめることができます。

これにより、データの不正なアクセスを防ぎ、データの整合性を保つことができます。

  1. 再利用性:一度定義したクラスは、他のプログラムでも再利用することができます。

これにより、コードの重複を避け、開発効率を向上させることができます。

  1. メンテナンス性:クラスを使用することで、コードの構造が明確になり、メンテナンスが容易になります。

特に大規模なプログラムでは、クラスを使用することでコードの管理がしやすくなります。

  1. オブジェクト指向の利点:クラスはオブジェクト指向プログラミングの基本要素であり、継承やポリモーフィズムなどの強力な機能を利用することができます。

以上のように、クラスを使用することで、プログラムの品質を向上させることができます。

ぜひ、実際のプログラムでクラスを活用してみてください。

目次から探す