目次
1. クラスの 6 つのデフォルトのメンバー関数
前の章でのクラスとオブジェクトの学習を通じて、クラスにメンバー変数もメンバー関数も何もない場合、そのクラスを空のクラスと呼ぶことがわかりました。
例えば:class Date {};
では、空のクラスには本当に何もないのでしょうか?
いいえ、どのクラスも何も書き込まない場合、コンパイラは次の 6 つのデフォルトのメンバー関数を自動的に生成します。
デフォルトのメンバー関数: ユーザーが明示的に実装しなくても、コンパイラーによって自動的に生成されるメンバー関数の名前。
2. コンストラクター
2.1 コンセプト
今、そのようなクラスがあります:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023,9,5);
d1.Print();
d2.Init(2023,8,18);
d2.Print();
return 0;
}
クラスの場合、オブジェクトをインスタンス化した後、通常は初期化を行いますが、初期化を忘れてオブジェクトに対して直接何らかの操作を実行する場合があり、初期化せずに直接使用すると問題が発生する可能性があります。
したがって、上記の状況に対して、C++ はこの問題を解決する方法を提供します。
このメソッドがコンストラクターです。コンストラクターは、クラス名と同じ名前を持つ特別なメンバー関数であり、クラス型オブジェクトの作成時にコンパイラーによって自動的に呼び出され、各データ メンバーが適切な初期値を持つようにし、生涯に 1 回だけ呼び出されます。オブジェクトのサイクル。
2.2 特徴
コンストラクターは特別なメンバー関数であることに注意してください。コンストラクターの名前はコンストラクターと呼ばれていますが、コンストラクターの主なタスクは、オブジェクトを作成するためのスペースを開くことではなく、オブジェクトを初期化することです。
その特徴は次のとおりです。
- 関数名はクラス名と同じです。
つまり、クラスを定義した後、そのコンストラクターの関数名が決定され、これは現在のクラスのクラス名と同じになります。
- 戻り値はありません。
void
ここで言う戻り値なしとは、戻り値の型が であるという意味ではなく、戻り値の型がまったく書かれていないことを意味することに注意してください。
- コンパイラは、オブジェクトがインスタンス化されるときに、対応するコンストラクターを自動的に呼び出します。
コンストラクターを通じて、手動初期化を行わずにオブジェクトを初期化します。オブジェクトがインスタンス化されると、コンパイラーは対応するコンストラクターを自動的に呼び出します。
- コンストラクターはオーバーロードできます。
次に、上記の Date クラスのコンストラクターを作成します。
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
- 引数のないコンストラクター
上図の実行結果から、今回は初期化関数を呼び出していないことがわかりますが、出力される値はランダムな値ではなく、コンストラクターで指定した値であり、オブジェクトをインスタンス化するときにそれを示しています。、確かにコンストラクターは初期化のために自動的に呼び出されます。
- パラメータ付きのコンストラクタ
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
知らせ:オブジェクトが引数なしのコンストラクターを通じて作成された場合、オブジェクトの後にかっこを続ける必要はありません。そうでない場合、オブジェクトは関数宣言になります。
- クラスにコンストラクターが明示的に定義されていない場合、C++ コンパイラーはパラメーターのないデフォルトのコンストラクターを自動的に生成します。ユーザーがコンパイラーを明示的に定義すると、コンパイラーは生成されなくなります。
つまり、コンストラクターを自分で記述する必要はなく、自分でコンストラクターを定義しなくても、コンパイラーが自動的にコンストラクターを生成します。それは無駄です。
コンパイラが自動的に生成するのであれば、将来的には自分でコンストラクタを書く必要はないのでしょうか?
答えはノーです。
上記で自分で作成したコンストラクターをコメントアウトし、プログラムを直接実行します。
コンパイラーの自動生成コンストラクターの呼び出しがランダムな値であることがわかります。
実はこの場所は C++ の設計上の問題として誰もが考えることができます。
C++ では、型を組み込み型 (基本型) とカスタム型に分類します。組み込み型は、次のような言語によって提供されるデータ型です (
int/char...
さまざまなポインター型を含む)。カスタム型は、class/struct/union を使用して独自に定義する型です。
ただし、コンパイラによって自動生成されるコンストラクターは組み込み型を処理せず、カスタム型を処理します。カスタムタイプに対応するデフォルトのコンストラクターを呼び出します
別の例を見てみましょう。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
ここの Date クラスには、そのメンバー変数に組み込み型とカスタム型の両方があります。
ただし、現時点では Date クラスのコンストラクターを作成していないため、Date を直接使用して main 関数内でオブジェクトを作成し、コンパイラーによって自動的に生成されたコンストラクターを自然に呼び出します。組み込み型は処理されません。カスタム型もありますTime _t;
。カスタム型の場合、コンパイラは対応するデフォルトのコンストラクターを自動的に呼び出します。
実行して結果を確認してみましょう。
ということは、組み込み型はコンストラクターを書かないと初期化できないということでしょうか?
注: C++11 では、組み込み型のメンバーが初期化されない、つまり、クラスで宣言されたときに組み込み型のメンバー変数にデフォルト値が与えられるという欠陥に対してパッチが適用されました。
- パラメーターなしのコンストラクターとデフォルト コンストラクターは両方ともデフォルト コンストラクターと呼ばれ、デフォルト コンストラクターは 1 つだけです。注:引数のないコンストラクター、完全なデフォルト コンストラクター、およびデフォルトでコンパイラーによって生成されるように記述されていないコンストラクターはすべて、デフォルト コンストラクターと見なすことができます。
3. デストラクター
3.1 コンセプト
前のコンストラクターの研究を通じて、オブジェクトがどのようにして誕生したのか、そしてそのオブジェクトがどのようにして消滅したのかが分かりました。
デストラクター:コンストラクターの機能とは異なり、デストラクターはオブジェクト自体の破棄を完了しません。ローカル オブジェクトの破棄はコンパイラーによって行われます。オブジェクトが破棄されると、自動的にデストラクターが呼び出され、オブジェクト内のリソースのクリーンアップが完了します。
理解を助けるために例を挙げてみましょう。
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Push(1);
s.Push(2);
return 0;
}
ここにあるオブジェクトは自分で破壊する必要がありますか?
s はスタック領域上に定義されたローカル変数であり、プログラム終了時に main 関数のスタックフレームとともに自動的に破棄されるため、答えは「いいえ」です。
デストラクターの役割は何ですか? オブジェクト内のリソースのクリーンアップを完了するとはどういう意味ですか?
スタックなどのオブジェクトの場合、ヒープ上に動的に開かれた領域があり、C 言語を学習した後は、これらの領域を手動で解放する必要があることは誰もが知っています。そうしないとメモリ リークが発生する可能性があります。
したがって、デストラクターはこれを支援するためにここにあります。
3.2 特徴
デストラクターは、次のような特性を持つ特別なメンバー関数です。
- デストラクター名には、クラス名の前に文字 ~ が付加されます。
クラスが定義されると、そのデストラクターの関数名も決定されます。つまり、クラス名の前に「~」が追加されます。
「~」は C 言語のビット反転であり、コンストラクタの機能と逆の機能を意味します。
- パラメータなし、戻り値の型なし
- クラスにはデストラクターを 1 つだけ含めることができます。明示的に定義されていない場合、システムはデフォルトのデストラクターを自動的に生成します。注: デストラクターはオーバーロードできません。
- オブジェクトのライフサイクルが終了すると、C++ コンパイル システムは自動的にデストラクターを呼び出します。
次に、先ほどのスタックのデストラクターを書きます。
~Stack()
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
デストラクターが自動的に呼び出されるかどうかを確認するには、コードに出力する行を追加します。
~Stack
現時点では、呼び出し関数はメイン関数には表示されません。
操作の結果は次のようになります。
- コンパイラによって自動的に生成されたデストラクタについて、何かを実現しますか? 次のプログラムでは、コンパイラによって生成されたデフォルトのデストラクタが、カスタム型メンバーのデストラクタを呼び出していることがわかります。
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack" << endl;
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
private:
DataType* _array;
int _capacity;
int _size;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Stack _s;
};
int main()
{
// Stack s;
// s.Push(1);
// s.Push(2);
Date d1;
return 0;
}
ここでは、Date のデストラクターを明示的に定義しません。d1 の宣言サイクルが終了すると、コンパイラーによって生成されたデフォルトのデストラクターが呼び出されます。その中の組み込み型は処理されず、カスタム型
Stack _s;
によって要求されたリソースは処理されませんコンパイラー自体によって生成されるデフォルトのデストラクターは、Stack クラスのデストラクターを呼び出します。
- クラスにリソース アプリケーションが存在しない場合、デストラクターを記述することはできません。Date クラスなど、コンパイラによって生成されたデフォルトのデストラクターが直接使用されます。リソース アプリケーションがある場合は、それを記述する必要があり、存在しない場合は、 Stack クラスなどのリソース リークの原因となります。
4. コンストラクターのコピー
4.1 コンセプト
前に作成した日付クラスを確認してみましょう。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
return 0;
}
さて、次のような問題があります。今、別のオブジェクトを作成して、このオブジェクトが d1 または d1 のコピーと同じになるようにしたい場合、どうやってそれを実現すればよいでしょうか。
上記の研究を経て、退役軍人は次のように考えるのが簡単だと思います。d1 のような新しいオブジェクトを作成したい場合は、d1 を使用して、作成された新しいオブジェクトを初期化できます。コンストラクターのパラメーターの型をクラス オブジェクトの型に設定する必要がありますか?
これがコピー構築です。
コピー コンストラクター: 単一のパラメーターのみ。このクラス型のオブジェクトへの参照(一般的には const 修飾がよく使われます)、既存のクラス型オブジェクトで新しいオブジェクトを作成するときにコンパイラによって自動的に呼び出されます。
4.2 特徴
コピー コンストラクターも、次の特性を持つ特別なメンバー関数です。
- コピー コンストラクターは、コンストラクターのオーバーロードされた形式です。
それでは、コピー コンストラクターを作成しましょう。
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
それでいいですか?
ここで機能しない理由は次のとおりです。前のコピー コンストラクターの概念では、パラメーターの型はクラス型オブジェクトへの参照でなければならないと述べました。
では、なぜクラス型オブジェクトへの参照でなければならないのでしょうか?
- コピー コンストラクターのパラメーターは 1 つだけで、クラス型オブジェクトへの参照である必要があります。値渡しメソッドが使用されると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。
なぜここで無限再帰が起こるのでしょうか?
さらに、オブジェクトのコピー構造は次のように記述することもできます。
さらに、次の点にも注意する必要があります。
コピー コンストラクターの仮パラメーターは通常、const で変更されます。
これは誰にとっても理解するのは難しくないと思いますが、仮パラメータは新しく作成したオブジェクトを初期化するために使用され、const を追加して仮パラメータ d は変更されません。さらに、const で渡されたパラメータが const で変更された場合でも、それを受け取ることができます。
- 明示的に定義されていない場合、コンパイラはデフォルトのコピー コンストラクターを生成します。デフォルトのコピー コンストラクター オブジェクトは、メモリ ストレージに従ってバイト オーダーでコピーされます。この種のコピーは、浅いコピー、または値のコピーと呼ばれます。
では、デフォルトで生成されるコピー コンストラクターは信頼できるのでしょうか?
ここでもう一度 date クラスを見てみましょう。
まず、実装したコピー コンストラクターをコメント アウトします。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d)
// {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
// Date d1(2023,9,5);
// Date d2(2023,8,18);
//d1.Init(2023,9,5);
Date d1;
Date d2(d1);
Date d3 = d1;
d1.Print();
//d2.Init(2023,8,18);
d2.Print();
d3.Print();
return 0;
}
結果を見てみましょう
ここを見ると、コピー コンストラクターは常に信頼できるのでしょうか?
もう一度 Stack クラスを見てみましょう。
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack" << endl;
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Push(1);
s.Push(2);
Stack s2(s);
return 0;
}
Stack クラスについては、コピー コンストラクターを作成しませんでした。実行して結果を見てみましょう:
ここでプログラムがハングしていることがわかります。では、ここでハングする理由は何でしょうか?
実際、ここでの根本的な原因は、機能 3 に表示されることです。
特集3にはこんな一文があります。
ここでは、実際にはメンバー変数を 1 つずつ順番にコピーし、そこに格納されているものをすべてコピーしています。
Date クラスと Stack クラスのコピーを比較してみましょう。
Date クラスの場合、浅いコピーは問題ありません。
合計 12 バイトのコンテンツが順番にコピーされます
Stack クラスの浅いコピーには問題があります。
ここでは、スコープがスコープ外にあるときにデストラクターが呼び出され、2 つの s1 と s2 のポインターが指すスペースが 2 回解放されます。同様に、データをスタックにプッシュすると、その中にデータが存在しs1
ますs2
。 (両方とも同じ空間を使用しているため)、その後、st2 を使用してデータを再度スタックにプッシュすると、s1
前_size
のデータはすでに++
渡されていますが、s2
前_size
のデータはまだ 0 であるため、s2 によって入力されたデータがデータを上書きします。前にs1によって入力されました。注:
ここでは、s2 が最初に破棄されます。s1 と s2 の両方がスタック (スタック領域) 上にあることがわかります。スタック領域をスタック領域と呼ぶ理由は、この場所でのスタック フレームの確立も最初の破棄に従うためです。 -in-last-out ルール。順次、つまり、後で定義されたものが最初に破棄されます。先にs2が破棄され、ヒープ上の空間が解放されますが、その後stも破棄されますが、このときs1はこの空間のアドレスを保持していますが、この空間は解放されているため、s1はワイルドポインタとなります。
では、なぜプログラムがクラッシュしたのかというと、ここでワイルドポインタを解放したためです。。
したがって:
クラスにリソース アプリケーションが関与していない場合は、コピー コンストラクターを記述してもしなくても構いません。リソース アプリケーションが関与すると、
コピー コンストラクターを記述する必要があります。それ以外の場合は、コピー コンストラクターは浅いコピーになります。
- コピー コンストラクターの一般的な呼び出しシナリオ:
既存のオブジェクトを使用して新しいオブジェクトを作成する
関数パラメータの型がクラス型オブジェクトである
関数の戻り値の型がクラス型オブジェクトである
5. 代入演算子のオーバーロード
5.1 演算子のオーバーロード
C++ では、コードの可読性を高めるために演算子のオーバーロードが導入されています。演算子のオーバーロードは、特別な関数名を持つ関数です。, にも戻り値の型、関数名、パラメータのリストがあり、戻り値の型とパラメータのリストは通常の関数と同様です。
関数名は次のとおりです。キーワード演算子の後には、オーバーロードする必要がある演算子記号が続きます。
関数プロトタイプ:戻り値の型 演算子 演算子(パラメータリスト)
以下では、例として date クラスを取り上げます。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2023,9,5);
Date d2(2023,8,18);
d1.Print();
d2.Print();
return 0;
}
今、2 つのオブジェクト d1、d2 があり、誰もが質問について考えています。これら 2 つのオブジェクトが等しいかどうかを比較したいと考えています。どうすればそれを達成できるでしょうか? これを関数で実装することは誰でも簡単に思いつくと思います。
bool Equal(const Date& x1, const Date& x2)
{
//...
}
しかし、C++ に演算子のオーバーロードを導入した後は、これを実現できるようになりました。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1.day == d2.day;
}
ここで小さな問題が発生します。
この問題の原因は次のとおりです。
Date クラスのこれら 3 つのメンバー変数はプライベート (プライベート) であるため、クラス外からアクセスすることはできません。
どうやって解決すればいいでしょうか?
クラス内に Get メソッド (関数) を記述したり、Get メソッドを通じてアクセスしたり、プライベート アクセス修飾子を直接削除したりできます。
まずここに private に注釈を付けてみましょう。
では、次のように呼び出してみましょう。
知らせ:ここでは<<の方が優先度==
が高い括弧を付けています。
しかし、ここではグローバルに直接オーバーロードされており、すべてのメンバー変数をパブリックにしていますが、カプセル化はどのように反映できるでしょうか?
したがって、より良い方法は次のとおりです。これをクラスに直接オーバーロードします。つまり、メンバー関数にオーバーロードします。
しかし、ここには別の小さな問題があり、関数をクラスに直接カプセル化すると、次のようになります。
==
ここでは演算子をオーバーロードしています。通常の状況ではオペランドは 2 つだけなので、パラメーターは 2 つだけで十分です。
ここのパラメータは 2 つだけではないでしょうか?
ここには隠しパラメータもあることを忘れないでください。このポインタはどの隠しパラメータですか。
C++ コンパイラーは、各「非静的メンバー関数」に隠しポインター パラメーターを追加し、ポインターが現在のオブジェクト (関数の実行時に関数を呼び出すオブジェクト) を指すようにするため、ここで指定する必要があるパラメーターは 1 つだけです。
。
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
知らせ:
- 他のシンボルを連結して新しい演算子を作成することはできません: 例:operator@
- オーバーロードされた演算子にはクラス型のパラメータが少なくとも 1 つあります
- 組み込み型に使用される演算子。その意味はオーバーロードによって変更できません。たとえば、組み込み整数 + 意味は変更できません。
- クラスのメンバー関数としてオーバーロードされた場合、メンバー関数の最初のパラメーターは非表示のパラメーターであるため、その仮パラメーターはオペランドの数より 1 つ少ないように見えます。
.* :: sizeof ?: .
これら 5 つの演算子はオーバーロードできないことに注意してください。これは、筆記試験の多肢選択問題でよく出題されます。
5.2 代入のオーバーロード
パラメータの型: const クラス オブジェクトの参照、参照を渡すとパラメータの受け渡しの効率が向上します。 戻り
値の型: クラス型 &、参照を返すと戻りの効率が向上します。戻り値の目的は連続代入をサポートすることです。
自分に与えられたものであるかどうかを確認する 代入して何らかの処理を行う
Return *this: 返された結果は、継続的な代入をサポートするために使用されます
この場合、日付クラスの代入オーバーロードは次のようになります。
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
ただし、誰かが自分自身を自分自身に割り当てる可能性を排除できない場合があるため、無駄にコピーを作成する関数が呼び出されます。これは改善します。
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
5.3 代入演算子のオーバーロード機能
- ユーザーが明示的に実装しない場合、コンパイラはデフォルトの代入演算子オーバーロードを生成します。これは値の形式でバイトごとにコピーされます (浅いコピー)。
知らせ:デフォルトで生成される代入オーバーロードは、組み込み型メンバー変数に直接割り当てられますが、ユーザー定義型メンバー変数は、代入を完了するために、対応するクラスの代入演算子オーバーロードを呼び出す必要があります。
それでは、次の状況はコピー構築または代入のオーバーロードと呼ばれるのでしょうか?
ここでは代入を使っています=
が、コピー構築です。
割り当てが過負荷になるのはどのような場合ですか?
インスタンス化されたオブジェクトを使用して相互に割り当てる場合を、割り当てオーバーロードと呼びます。そして、既にインスタンス化されているオブジェクトを使用して新しいオブジェクトを初期化すると、コピー構築が呼び出されます。
- 代入演算子はクラスのメンバー関数としてのみオーバーロードでき、グローバル関数としてオーバーロードできません。
理由:代入オーバーロードがクラスに明示的に実装されていない場合、コンパイラはデフォルトのオーバーロードを生成します。このとき、ユーザーがグローバル代入演算子オーバーロードをクラス外で実装すると、クラス内のコンパイラによって生成されるデフォルトの代入演算子オーバーロードと競合するため、代入演算子オーバーロードはクラスのメンバー関数のみにすることができます。
6. const メンバー
const 修飾された「メンバー関数」は、 const メンバー関数 と呼ばれます。const 修飾されたクラス メンバー関数は、実際にはメンバー関数の暗黙的な this ポインターを変更し、クラスのメンバーがメンバー関数内で変更できないことを示します。
例として、先ほどの代入オーバーロード関数を取り上げます。
Date& operator=(const Date& d) const
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
7. address および const アドレス演算子のオーバーロード
これら 2 つのデフォルトのメンバー関数は通常、再定義する必要はなく、コンパイラによってデフォルトで生成されます。
これら 2 つの演算子は通常、オーバーロードする必要はなく、コンパイラーによって生成されたデフォルトのアドレス オーバーロードを使用するだけです。オーバーロードが必要になるのは、指定されたコンテンツを他の演算子に取得させるなどの特別な場合のみです。