コンテンツにスキップ

C++ の基礎 Day 6

1. クラス

1.1 class と struct

  • 0 個以上のメンバ変数、0 個以上のメンバ関数を持つ新しい型を作ることができる。
  • クラスは struct(ストラクト)または class(クラス)キーワードで宣言する。
  • メンバを初期化する際に使われる特殊なメンバ関数「コンストラクタ」を持つことができる。
#include <print>
#include <string>

// Point クラス
struct Point
{
	// メンバ変数
	int x;

	// メンバ変数
	int y;

	// メンバ関数
	bool isZero() const
	{
		return ((x == 0) && (y == 0));
	}
};

// User クラス
class User
{
public: // アクセス指定子(パブリック)

	// メンバ関数(コンストラクタ)
	User() = default;

	// メンバ関数(コンストラクタ)
	User(int id, const std::string& name)
		: m_id(id)
		, m_name(name) {}

	// メンバ関数
	int getId() const
	{
		return m_id;
	}

	// メンバ関数
	const std::string& getName() const
	{
		return m_name;
	}

private: // アクセス指定子(プライベート)

	// メンバ変数
	int m_id = 0;

	// メンバ変数
	std::string m_name;
};

int main()
{
	Point point = { 1, 2 };
	point.x += 5;
	std::println("({}, {})", point.x, point.y);
	std::println("isZero: {}", point.isZero());

	User user(1, "Alice");
	std::println("id: {}", user.getId());
	std::println("name: {}", user.getName());
}
  • structclass の違いは、メンバのデフォルトのアクセス指定子が異なるだけ。実行時性能もメモリ使用量も同じ。
キーワード メンバのデフォルトのアクセス指定子
struct public(パブリック)
class private(プライベート)
  • したがって次の 2 つのクラスは同じ意味。

struct Point
{
	int x;

	int y;
};
class Point
{
public:

	int x;

	int y;
};

  • structclass の使い分けに厳密なルールは無いが、慣例として次のように使い分ける。
    • クラス外から直接アクセスさせたくない private メンバ変数を持つ → class
    • それ以外 → struct

1.2 集成体

  • すべてのメンバ変数が public で、ユーザ宣言された(ユーザによってカスタマイズされた)コンストラクタや仮想メンバ関数を持たない、C 言語の構造体に近いクラスは「集成体(または集約型)」と呼ばれる。
  • 集成体は Designated Initializer(デジグネイテッド・イニシャライザー / 指示付き初期化)という、メンバ変数名を指定して初期化する特殊な機能を使うことができる。
#include <string>

struct Student // 集成体の条件を満たすクラス
{
	int id = 0;

	std::string name;

	int age = 0;
};

int main()
{
	// Designated Initializer を使って初期化できる
	Student s1 = { .id = 1, .name = "Alice", .age = 20 };

	Student s2 = { 2, "Bob", 21 }; // 通常の初期化も可能
}

1.3 コンストラクタ

  • ユーザ宣言されたコンストラクタがある場合、Designated Initializer は使えない。代わりにコンストラクタを使って初期化する。
#include <string>

struct Student // ユーザ宣言されたコンストラクタを持つクラス
{
	int id = 0;

	std::string name;

	int age = 0;

	// ユーザ宣言されたコンストラクタ(デフォルトコンストラクタ)
	Student() = default;

	// ユーザ宣言されたコンストラクタ
	Student(int _id, const std::string& _name, int _age)
		: id(_id)
		, name(_name)
		, age(_age) {}
};

int main()
{
	// コンストラクタを使って初期化
	Student s1(1, "Alice", 20);

	// コンストラクタを使って初期化
	Student s2 = { 2, "Bob", 21 };

	// デフォルトコンストラクタを使って初期化
	Student s3;

	// デフォルトコンストラクタを使って初期化
	Student s4{};
}
  • コンストラクタを使う場合、() による初期化と {} による初期化がある。{} による初期化はナローイング(縮小変換)を禁止する。
struct Point
{
	int x;

	int y;

	Point() = default;

	Point(int _x, int _y)
		: x(_x)
		, y(_y) {}
};

int main()
{
	Point p1(1, 2); // OK
	Point p2{ 1, 2 }; // OK

	Point p3{ 1, 2 }; // OK
	Point p4{ 1.1, 2.2 }; // エラー(縮小変換)
}

1.4 コンストラクタにおけるメンバ変数の初期化

  • コンストラクタの : のあとに続くのがメンバの初期化リスト。メンバ変数の初期化を行う。
  • メンバ変数はこの初期化リストを見ながら初期化される。初期化リストにないメンバ変数はデフォルトコンストラクタで初期化される。
  • メンバ変数の初期化順は、初期化リスト内の順番ではなく、クラスでのメンバ変数の宣言順に従う。
  • メンバ変数の初期化はコンストラクタの本体({})よりも先に行われる。
#include <print>

struct Point
{
	int x;

	int y;

	Point()
		: x(123)
		, y(456)
	{
		std::println("Point({}, {})", x, y);
	}

	Point(int _x, int _y)
		: x(_x)
		, y(_y)
	{
		std::println("Point({}, {})", x, y);
	}
};

int main()
{
	Point p1; // Point(123, 456)

	Point p2(1, 2); // Point(1, 2)
}
  • デフォルトコンストラクタ(引数を取らないコンストラクタ)を = default で宣言することができる。
  • = default のコンストラクタでは、後述の初期化式が無い場合、メンバ変数をデフォルトコンストラクタで初期化する。
#include <print>

struct Test
{
	int n;

	std::string s;

	Test() = default;
};

int main()
{
	Test test;
	std::println("n: {}", test.n); // 不定値
	std::println("s: {}", test.s); // 空文字列
}
  • メンバ変数の定義時に、= 演算子もしくは { } によるコンストラクタ構文で、初期化式を記述できる。メンバ変数の初期化にはこの初期化式が使われる。
#include <print>

struct Test
{
	int n = 123;

	std::string s = "Alice";

	Test() = default;
};

int main()
{
	Test test;
	std::println("n: {}", test.n); // 123
	std::println("s: {}", test.s); // Alice
}
  • ただし、コンストラクタのメンバの初期化リストに別の初期化方法を記述した場合はそちらが優先される。
#include <print>

struct Test
{
	int n = 123;

	std::string s = "Alice";

	Test() = default;

	Test(int _n, const std::string& _s)
		: n(_n)		// こちらが優先される
		, s(_s) {}	// こちらが優先される
};

int main()
{
	Test t1;
	std::println("n: {}", t1.n); // 123
	std::println("s: {}", t1.s); // Alice

	Test t2(456, "Bob");
	std::println("n: {}", t2.n); // 456
	std::println("s: {}", t2.s); // Bob
}

1.5 メンバ関数

  • クラスはメンバとして関数を持つことができる。これを「メンバ関数」と呼ぶ。
  • メンバ関数はクラス内のすべてのメンバ変数およびメンバ関数にアクセスできる。
#include <print>
#include <string>

class User
{
public:

	User() = default;

	User(int id, const std::string& name)
		: m_id(id)
		, m_name(name) {}

	int getId() const
	{
		return m_id;
	}

	const std::string& getName() const
	{
		return m_name;
	}

	void setName(const std::string& name)
	{
		m_name = name;
	}

	void show() const
	{
		std::println("id: {}", m_id);
		std::println("name: {}", m_name);
	}

private:

	int m_id = 0;

	std::string m_name;
};

int main()
{
	User user(1, "Alice");
	user.show();
	user.setName("Bob");
	user.show();
}
  • クラスのメンバ関数には const 修飾子を付けることができる。これを「const メンバ関数」と呼ぶ。
  • const メンバ関数は、メンバ変数を変更しないことを保証する。メンバ変数を変更するような処理を行うとコンパイルエラーになる。
  • クラスのオブジェクトが const 修飾されている場合、const メンバ関数しか呼び出せない。
#include <print>
#include <string>

class User
{
public:

	User() = default;

	User(int id, const std::string& name)
		: m_id(id)
		, m_name(name) {}

	// const メンバ関数
	int getId() const
	{
		return m_id;
	}

	// const メンバ関数
	const std::string& getName() const
	{
		return m_name;
	}

	void setName(const std::string& name)
	{
		m_name = name;
	}

	// const メンバ関数
	void show() const
	{
		std::println("id: {}", m_id);
		std::println("name: {}", m_name);
	}

private:

	int m_id = 0;

	std::string m_name;
};

int main()
{
	// const オブジェクト
	const User user(1, "Alice");

	// user.setName("Bob"); // コンパイルエラー(const メンバ関数しか呼び出せない)

	user.show(); // 呼び出せる
}

1.6 演算子のオーバーロード

  • クラスに対する演算子の振る舞いをカスタマイズすることができる。これを「演算子のオーバーロード」と呼ぶ。
  • デフォルトでは +-, == などのどの演算子も、クラスに対して使うことができない。これらの演算子を使えるようにするには、オーバーロードする必要がある。
  • 二項演算子のオーバーロードで、第一引数が自身のクラスでない場合、friend を使う。
#include <print>

struct Point
{
	int x;

	int y;

	Point() = default;

	Point(int _x, int _y)
		: x(_x)
		, y(_y) {}

	// 単項 - 演算子のオーバーロード
	Point operator -() const
	{
		return Point(-x, -y);
	}

	// 二項 + 演算子のオーバーロード
	Point operator +(const Point& p) const
	{
		return Point((x + p.x), (y + p.y));
	}

	// 二項 * 演算子のオーバーロード
	Point operator *(int n) const
	{
		return Point((x * n), (y * n));
	}

	// 二項演算子のオーバーロードで、第一引数が自身のクラスでない場合、friend を使う
	friend Point operator *(int n, const Point& p)
	{
		return Point((n * p.x), (n * p.y));
	}
};

int main()
{
	Point p1(1, 2);
	Point p2(3, 4);

	Point p3 = -p1; // Point(-1, -2)
	Point p4 = p1 + p2; // Point(4, 6)
	Point p5 = p1 * 3; // Point(3, 6)
	Point p6 = 2 * p2; // Point(6, 8)

	std::println("({}, {})", p3.x, p3.y);
	std::println("({}, {})", p4.x, p4.y);
	std::println("({}, {})", p5.x, p5.y);
	std::println("({}, {})", p6.x, p6.y);
}

1.7 std::format による文字列フォーマットへの対応

  • クラスを std::printstd::println で直接出力できるようにするには、std::format(フォーマット)に対応させる必要がある。
  • std::format に対応させるには、std::formatter(フォーマッタ)を特殊化する。

#include <format>
#include <print>
#include <string>

struct Point
{
	int x;

	int y;

	Point() = default;

	Point(int _x, int _y)
		: x(_x)
		, y(_y) {}
};

// Point クラスを std::format に対応させる
template <>
struct std::formatter<Point>
{
	template <class ParseContext>
	constexpr ParseContext::iterator parse(ParseContext& ctx)
	{
		return ctx.begin();
	}

	template <class FmtContext>
	FmtContext::iterator format(const Point& p, FmtContext& ctx) const
	{
		// 最終的に出力される文字列をつくる。
		return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
	}
};

class User
{
public:

	User() = default;

	User(int id, const std::string& name)
		: m_id(id)
		, m_name(name) {}

	int getId() const
	{
		return m_id;
	}

	const std::string& getName() const
	{
		return m_name;
	}

private:

	int m_id = 0;

	std::string m_name;
};

// User クラスを std::format に対応させる
template <>
struct std::formatter<User>
{
	template <class ParseContext>
	constexpr auto parse(ParseContext& ctx)
	{
		return ctx.begin();
	}

	template <class FormtContext>
	auto format(const User& user, FormtContext& ctx) const
	{
		// 最終的に出力される文字列をつくる。
		// "{{" は "{" をエスケープするために使われる
		return std::format_to(ctx.out(), "{{\n  id\t: {},\n  name\t: {}\n}}", user.getId(), user.getName());
	}
};

int main()
{
	Point p(1, 2);
	std::println("{}", p);

	std::println("----");

	User user(1, "Alice");
	std::println("{}", user);
}
出力
(1, 2)
----
{
  id	: 1,
  name	: Alice
}

1.8 入出力ストリームへの対応

  • C++20 以前の std::cout(シーアウト)などの出力ストリームに対応させるには、std::ostream(オーストリーム)に対する operator<< をオーバーロードする。
  • std::cin などの入力ストリームに対応させるには、std::istream(アイストリーム)に対する operator>> をオーバーロードする。

#include <iostream>
#include <string>

struct Point
{
	int x;

	int y;

	Point() = default;

	Point(int _x, int _y)
		: x(_x)
		, y(_y) {}

	friend std::ostream& operator <<(std::ostream& os, const Point& p)
	{
		return os << "(" << p.x << ", " << p.y << ")";
	}

	friend std::istream& operator >>(std::istream& is, Point& p)
	{
		char c;
		return is >> c >> p.x >> c >> p.y >> c;
	}
};

class User
{
public:

	User() = default;

	User(int id, const std::string& name)
		: m_id(id)
		, m_name(name) {}

	friend std::ostream& operator <<(std::ostream& os, const User& user)
	{
		return os << "{\n"
			<< "  id\t: " << user.m_id << ",\n"
			<< "  name\t: " << user.m_name << '\n'
			<< "}";
	}

private:

	int m_id = 0;

	std::string m_name;
};

int main()
{
	Point p1(1, 2);
	std::cout << p1 << '\n';

	Point p2;
	std::cin >> p2;
	std::cout << p2 << '\n';

	std::cout << "----\n";

	User user(1, "Alice");
	std::cout << user << '\n';
}
入力
(3, 4)
出力
(1, 2)
(3, 4)
----
{
  id	: 1,
  name	: Alice
}

1.9 デストラクタ

  • ~クラス名() で表現する特殊なメンバ関数がデストラクタ。
  • 次のように、クラスのオブジェクトが破棄されるタイミングで必ず呼ばれる。
    • 変数の有効なスコープが外れる
    • 配列の要素から削除される

#include <print>

struct Object
{
	int n = 100;

	Object()
	{
		std::println("コンストラクタ: {}", n);
	}

	~Object()
	{
		std::println("デストラクタ: {}", n);
	}
};

int main()
{
	{
		Object obj;
		obj.n = 200;
	}

	std::println("----");

	{
		std::vector<Object> v(1);
		v[0].n = 300;
		v.pop_back();
	}

	std::println("----");
}
コンストラクタ: 100
デストラクタ: 200
----
コンストラクタ: 100
デストラクタ: 300
----

  • 標準ライブラリの各種クラスでは、そのオブジェクトが確保したメモリ・ファイルなどのリソースは、基本的にデストラクタ内ですべて解放される。