1. クラスの定義
C++では、クラスを定義する方法として主にclassとstructの2つがあります。C言語のstructとは異なり、C++のstructではメンバ関数を定義できます。一般的にはclassを使用します。
1.1 アクセス指定子
- C++におけるカプセル化の実装方法の一つで、クラスがオブジェクトの属性とメソッドを結びつけ、より完全なオブジェクトを作成します。アクセス権を選択的に設定することで、外部ユーザーにインターフェースを提供します。
- publicで修飾されたメンバはクラス外から直接アクセス可能です。protectedとprivateで修飾されたメンバはクラス外から直接アクセスできません。protectedとprivateは現在の段階では同じように振る舞いますが、継承の章でその違いが明らかになります。
- アクセス権のスコープは、そのアクセス指定子が現れた位置から次のアクセス指定子が現れるまでです。その後にアクセス指定子がない場合、スコープはクラスの終わり(})までです。
- classで定義されたメンバがアクセス指定子で修飾されていない場合、デフォルトでprivateになります。structではデフォルトでpublicになります。
- 一般的に、メンバ変数はprivate/protectedに制限され、外部で使用する必要のあるメンバ関数はpublicに設定されます。
1.2 クラススコープ
クラスは新しいスコープを定義し、クラスのすべてのメンバはクラスのスコープ内にあります。クラス外でメンバを定義する場合、::スコープ解決演算子を使用して、メンバがどのクラススコープに属するかを指定する必要があります。
2. インスタンス化
2.1 オブジェクトのサイズ
メモリアライメント規則:
- 最初のメンバは、構造体のオフセット0のアドレスに配置されます。
- 他のメンバ変数は、特定の数(アライメント数)の整数倍のアドレスにアライメントされます。
- アライメント数は、コンパイラのデフォルトのアライメント数とそのメンバのサイズのうち小さい方です。
- VSでのデフォルトのアライメント数は8です。
- 構造体の全体サイズは、最大アライメント数(すべての変数型の最大値とデフォルトのアライメントパラメータの最小値)の整数倍です。
- 構造体がネストされている場合、ネストされた構造体は自身の最大アライメント数の整数倍にアライメントされ、構造体の全体サイズはすべての最大アライメント数(ネストされた構造体のアライメント数を含む)の整数倍です。
!!!**構造体にメンバがない場合やメンバ関数のみの場合(空クラス)、サイズは1バイトで、オブジェクトが存在することを示すためのプレースホルダーとなります**
**メモリアライメントの目的:コンパイラは任意の位置からデータを読み取ることはできず、整数倍の位置から読み取る必要があります。これにより、読み取りが容易になります**
後から定義されたオブジェクトが先にデストラクタ呼び出されます。
3. thisポインタ
コンパイラがコンパイルした後、クラスのメンバ関数はデフォルトで、最初のパラメータ位置に現在のクラス型のポインタを追加します。これをthisポインタと呼びます。例えば、DateクラスのInitの実際のプロトタイプは、void Init(Date* const this, int year, int month, int day)となります。thisポインタにはこのクラスのアドレスが格納されています。
***thisポインタはオブジェクト内に存在するわけではありません***
4. クラスのデフォルトメンバ関数
デフォルトメンバ関数は、明示的に実装されていない場合、コンパイラが自動的に実装します。一个クラスには主に6つのデフォルトメンバ関数がありますが、ここでは最初の4つについて主に説明します。
4.1 コンストラクタ
インスタンス化時にオブジェクトを初期化するために使用されます。
- 関数名はクラス名と同じです。
- 戻り値はありません(戻り値は何も指定する必要がなく、voidも書く必要はありません。C++の規定です)。
- オブジェクトがインスタンス化されると、システムは自動的に対応するコンストラクタを呼び出します。
- コンストラクタはオーバーロードできます。
#include<iostream>
using namespace std;
class CalendarDate
{
public:
// 1.パラメータなしコンストラクタ
CalendarDate()
{
yearVal = 1;
monthVal = 1;
dayVal = 1;
}
// 2.パラメータありコンストラクタ
CalendarDate(int y, int m, int d)
{
yearVal = y;
monthVal = m;
dayVal = d;
}
// 3.すべてデフォルト引数のコンストラクタ
/*CalendarDate(int y = 1, int m = 1, int d = 1)
{
yearVal = y;
monthVal = m;
dayVal = d;
}*/
void Display()
{
cout << yearVal << "/" << monthVal << "/" << dayVal << endl;
}
private:
int yearVal;
int monthVal;
int dayVal;
};
CalendarDate date1; // デフォルトコンストラクタを呼び出す
CalendarDate date2(2025, 1, 1); // パラメータありのコンストラクタを呼び出す
CalendarDate date3(); // このように呼び出すと、コンパイラは関数宣言かオブジェクトのインスタンス化かを区別できず、エラーになります
4.2 デストラクタ
メモリ空間を解放する必要がある場合やスタックフレームが終了する際に動作します。
- デストラクタ名は、クラス名の前に文字~を付けたものです。
- パラメータも戻り値もありません(コンストラクタと同様に、voidも付けません)。
- クラスにはデストラクタは1つしかありません。明示的に定義されていない場合、システムは自動的にデフォルトのデストラクタを生成します。
- オブジェクトのライフサイクルが終了すると、システムは自動的にデストラクタを呼び出します。
#include<iostream>
using namespace std;
typedef int StackDataType;
class MemoryStack
{
public:
MemoryStack(int capacity = 4)
{
dataPtr = (StackDataType*)malloc(sizeof(StackDataType) * capacity);
if (nullptr == dataPtr)
{
perror("mallocメモリ割り当て失敗");
return;
}
maxSize = capacity;
topIndex = 0;
}
~MemoryStack()
{
cout << "~MemoryStack()" << endl;
free(dataPtr);
dataPtr = nullptr;
topIndex = maxSize = 0;
}
private:
StackDataType* dataPtr;
size_t maxSize;
size_t topIndex;
};
**後から構築されたオブジェクトが先にデストラクタ呼び出されます**
4.3 コピーコンストラクタ
注意:コピーコンストラクタによるコピーは浅いコピー(1バイトずつのコピーで、アドレスの場合は内容ではなくアドレス自体をコピー)です。Stackクラスなどの場合、多くのエラーが発生します。
例えば、以下のコードのdataPtrの場合、コンパイラが自動的に実装するコピーコンストラクタでは、dataPtrのアドレスが新しいクラスにコピーされるだけです。これにより、終了時に複数回のデストラクタ呼び出しが発生します。また、dataPtrの内容を変更する際、同じアドレスではなく同じ内容でないため、一方を変更すると、もう一方の変更する必要のない内容も変更されてしまい、非常に不便です。
以下の場合、対応するコードを明示的に実装する必要があります。
MemoryStack(const MemoryStack& source)
{
// source.dataPtrが指すリソースと同じサイズのリソースを作成し、値をコピーする必要がある
dataPtr = (StackDataType*)malloc(sizeof(StackDataType) * source.maxSize);
if (nullptr == dataPtr)
{
perror("mallocメモリ割り当て失敗!!!");
return;
}
memcpy(dataPtr, source.dataPtr, sizeof(StackDataType) * source.topIndex);
topIndex = source.topIndex;
maxSize = source.maxSize;
}
- コピーコンストラクタはコンストラクタの一種のオーバーロードです。
- コピーコンストラクタのパラメータは1つだけで、***クラス型オブジェクトの参照***でなければなりません。値渡しを使用するとコンパイラは直接エラーを報告します。なぜなら、構文的に無限再帰呼び出しを引き起こすためです。
- C++では、カスタム型オブジェクトのコピー動作ではコピーコンストラクタを呼び出す必要があるため、***カスタム型の値渡しによるパラメータ渡しと値返しは、コピーコンストラクタを呼び出して完了します。***
#include<iostream>
using namespace std;
class CalendarDate
{
public:
CalendarDate(int y = 1, int m = 1, int d = 1)
{
yearVal = y;
monthVal = m;
dayVal = d;
}
// コンパイルエラー:error C2652: "CalendarDate": 不正なコピーコンストラクタ: 最初のパラメータは"CalendarDate"であってはなりません
//CalendarDate(CalendarDate d)
CalendarDate(const CalendarDate& source)
{
yearVal = source.yearVal;
monthVal = source.monthVal;
dayVal = source.dayVal;
}
CalendarDate(CalendarDate* ptr)
{
yearVal = ptr->yearVal;
monthVal = ptr->monthVal;
dayVal = ptr->dayVal;
}
void Display()
{
cout << yearVal << "-" << monthVal << "-" << dayVal << endl;
}
private:
int yearVal;
int monthVal;
int dayVal;
};
4.4 代入演算子のオーバーロード
4.4.1 演算子のオーバーロード
operator+などの演算子を使用して加減乗除などの演算子をオーバーロードしますが、***.\* :: sizeof ?: .***の5つの演算子はオーバーロードできません。
オーバーロードされた演算子関数がメンバ関数である場合、最初の演算対象は暗黙のthisポインタに渡されるため、演算子のオーバーロードをメンバ関数として使用する場合、パラメータは演算対象より1つ少なくなります。
**また、オーバーロードされた演算子関数には、少なくとも1つのクラス型のパラメータが必要です**
bool operator==(const CalendarDate& lhs, const CalendarDate& rhs)
{
return lhs.yearVal == rhs.yearVal
&& lhs.monthVal == rhs.monthVal
&& lhs.dayVal == rhs.dayVal;
}
上記はメンバ関数でない場合の例です。
呼び出し:date1 == date2 または operator==(date1, date2);
bool operator==(const CalendarDate& rhs)
{
return yearVal == rhs.yearVal
&& monthVal == rhs.monthVal
&& dayVal == rhs.dayVal;
}
上記はメンバ関数として1つのパラメータが省略された場合です。
constを付けて変更を防ぎ、&を付けることでコピーコンストラクタの呼び出しを省略できます(値やアドレスを渡すため)。
4.4.2 代入演算子のオーバーロード
代入演算子のオーバーロードはデフォルトメンバ関数であり、**既に存在する2つのオブジェクト**間のコピー代入を完了するために使用されます。
一方、コピーコンストラクタは、あるオブジェクトをコピーして別の**作成されるオブジェクト**を初期化するために使用されます。
// 参照を返すことでコピーを減らす
// date1 = date2;
CalendarDate& operator=(const CalendarDate& source)
{
// 自己代入をチェックする
if (this != &source)
{
yearVal = source.yearVal;
monthVal = source.monthVal;
dayVal = source.dayVal;
}
// date1 = date2式の戻り値オブジェクトはdate1、つまり*thisであるべき
return *this;
}
戻り値は現在のクラスの参照にすることをお勧めします。returnには*thisを返すことができます。
5. アドレス演算子のオーバーロード
5.1 constメンバ関数
constで修飾されたメンバ関数をconstメンバ関数と呼びます。const修飾されたメンバ関数はメンバ関数のパラメータリストの後ろに配置され、const修飾された関数の暗黙のthisポインタは:CalendarDate* const this から const CalendarDate* const this に変わります。
例:
// void Display(const CalendarDate* const this) const
void Display() const
{
cout << yearVal << "-" << monthVal << "-" << dayVal << endl;
}
関数内で変更される値や維持される値がない場合は、constで保護することをお勧めします。
5.2 アドレス演算子のオーバーロード
通常、これら2つの関数はコンパイラが自動的に生成したもので十分であり、明示的に実装する必要はありません。
ただし、現在のクラスオブジェクトのアドレスを取得させたくないような特殊なシナリオでは、自分で実装して適当なアドレスを返すことができます。
CalendarDate* operator&()
{
return this;
// return nullptr;
}
const CalendarDate* operator&() const
{
return this;
// return nullptr;
}
6. 初期化リスト
初期化リストは、コンストラクタのパラメータリストの後に:を付け、クラスメンバを初期化する形式です。その後に括弧(括弧内には値または式)を付けます。初期化の順序は初期化リストの順序ではなく、宣言の順序に従って行われます。
参照とconstメンバは初期化リストで初期化する必要があります。
以下は初期化リストの一般的な問題です。
public:
CalendarDate(int& refVal, int y = 1, int m = 1, int d = 1)
:yearVal(y)
,monthVal(m)
,dayVal(d)
,timeObj(12)
,refMember(refVal)
,constVal(1)
{
// error C2512: "Time": 適切なデフォルトコンストラクタがありません
// error C2530 : "CalendarDate::refMember": 参照を初期化する必要があります
// error C2789 : "CalendarDate::constVal": const修飾型のオブジェクトを初期化する必要があります
}
private:
int yearVal;
int monthVal;
int dayVal;
Time timeObj; // デフォルトコンストラクタなし
int& refMember; // 参照
const int constVal; // const
7. 型変換
C++は、組み込み型からクラス型オブジェクトへの暗黙的な型変換をサポートしており、関連する組み込み型をパラメータとするコンストラクタが必要です。
これは単に便利にするためです。
!コンストラクタの前にexplicitを付けると、暗黙的な型変換はサポートされなくなります。
class ValueContainer
{
public:
// コンストラクタにexplicitを付けると暗黙的な型変換はサポートされなくなります
// explicit ValueContainer(int val)
ValueContainer(int val)
:value(val)
{}
//explicit ValueContainer(int val1, int val2)
ValueContainer(int val1, int val2)
:value(val1)
,secondValue(val2)
{}
void Print()
{
cout << value << " " << secondValue << endl;
}
int main()
{
ValueContainer container = 1;
container.Print();
return 0;
}
8. staticメンバ
- staticで修飾されたメンバ変数を静的メンバ変数と呼びます。***静的メンバ変数は必ずクラス外で初期化する必要があります***。
- 静的メンバ変数はすべてのクラスオブジェクトで共有され、特定のオブジェクトに属するものではなく、オブジェクト内には存在せず、静的領域に配置されます。
- staticで修飾されたメンバ関数を静的メンバ関数と呼びます。静的メンバ関数にはthisポインタがありません。
- 静的メンバ関数では他の静的メンバにアクセスできますが、thisポインタがないため非静的メンバにはアクセスできません。
- 非静的メンバ関数は、任意の静的メンバ変数と静的メンバ関数にアクセスできます。
- クラススコープを突破すれば静的メンバにアクセスでき、クラス名::静的メンバまたはオブジェクト.静的メンバを使用して静的メンバ変数と静的メンバ関数にアクセスできます。
- 静的メンバもクラスのメンバであり、public、protected、privateのアクセス指定子の制限を受けます。
- 静的メンバ変数は宣言位置でデフォルト値による初期化はできません。なぜなら、デフォルト値はコンストラクタの初期化リストのものであり、静的メンバ変数は特定のオブジェクトに属さず、コンストラクタの初期化リストを経由しないためです。
静的メンバはオブジェクト内ではなく静的領域に存在し、関数のデストラクタなどは他のメンバ関数に比べて非常に後になってからデストラクタが呼び出されます。
#include<iostream>
using namespace std;
class InstanceCounter
{
public:
InstanceCounter()
{
++count;
}
InstanceCounter(const InstanceCounter& other)
{
++count;
}
~InstanceCounter()
{
--count;
}
static int GetCount()
{
return count;
}
private:
// クラス内で宣言
static int count;
};
// クラス外で初期化
int InstanceCounter::count = 0;
int main()
{
cout << InstanceCounter::GetCount() << endl;
InstanceCounter obj1, obj2;
InstanceCounter obj3(obj1);
cout << InstanceCounter::GetCount() << endl;
cout << obj1.GetCount() << endl;
// コンパイルエラー:error C2248: "InstanceCounter::count": privateメンバにアクセスできません("InstanceCounter"クラスで宣言)
//cout << InstanceCounter::count << endl;
return 0;
}
9. フレンド
- フレンドはクラスのアクセス指定子によるカプセル化を突破する方法を提供します。フレンドにはフレンド関数とフレンドクラスがあり、関数宣言またはクラス宣言の前にfriendを付け、フレンド宣言をクラス内に配置します。
- 外部フレンド関数はクラスのprivateおよびprotectedメンバにアクセスできます。フレンド関数は単なる宣言であり、クラスのメンバ関数ではありません。
- フレンド関数はクラス定義のどこでも宣言でき、クラスのアクセス指定子の制限を受けません。
- 1つの関数は複数のクラスのフレンド関数になることができます。
- フレンドクラスのメンバ関数はすべて別のクラスのフレンド関数になることができ、別のクラスのprivateおよびprotectedメンバにアクセスできます。
- フレンドクラスの関係は一方向であり、交換性がありません。例えば、AクラスがBクラスのフレンドであっても、BクラスはAクラスのフレンドではありません。
- フレンドクラスの関係は伝達しません。AがBのフレンドで、BがCのフレンドであっても、AはCのフレンドではありません。
- 利便性を提供することもありますが、フレンドは結合度を高め、カプセル化を破壊するため、多用は避けるべきです。
**簡単に言えば、フレンドはアクセスしたい他のクラスの保護されたメンバにアクセスするのに役立ちますが、それは一方向のアクセスのみです**
class DataA
{
// フレンド宣言
friend void ShowValues(const DataA& a, const DataB& b);
private:
int value1 = 1;
int value2 = 2;
};
class DataB
{
// フレンド宣言
friend void ShowValues(const DataA& a, const DataB& b);
private:
int data1 = 3;
int data2 = 4;
};
void ShowValues(const DataA& a, const DataB& b)
{
cout << a.value1 << endl;
cout << b.data1 << endl;
}
10. 内部クラス
あるクラスが別のクラスの内部に定義されている場合、この内部クラスをネストされたクラスと呼びます。内部クラスは独立したクラスであり、グローバルに定義する場合と比較して、外部クラスのクラススコープとアクセス指定子の制限を受けるだけです。したがって、外部クラスで定義されたオブジェクトには内部クラスは含まれません。
内部クラスはデフォルトで外部クラスのフレンドクラスです。
class OuterClass
{
private:
static int staticValue;
int instanceValue = 1;
public:
class InnerClass // InnerClassはデフォルトでOuterClassのフレンド
{
public:
void AccessOuter(const OuterClass& outer)
{
cout << staticValue << endl; // OK
cout << outer.instanceValue << endl; // OK
}
};
};
int OuterClass::staticValue = 1;
11. 無名オブジェクト
名前の通り、型(実引数)で定義されたオブジェクトを無名オブジェクトと呼びます。簡単に言えば、クラス名を定義する必要がないということです。
無名オブジェクトのライフサイクルは現在の行のみで、一時的にオブジェクトを定義してすぐに使用する場合に無名オブジェクトを定義できます。
class Sample
{
public:
Sample(int num = 0):
value(num)
{}
void Display()
{
cout<<value<<endl;
}
private:
int value;
};
int main()
{
// 1行のコードのみのライフサイクル
Sample(5);
cout<<Sample().Display();
return 0;
}