コース内容#
タイプと変数#
クラスとオブジェクトの別名と考えられる
- C++ ではタイプの最小ビット数のみが規定されているため、一部のコンパイラはより多くのビット数を実現できる
- 参考 cppreference——基本タイプ
タイプ#
= タイプデータ + タイプ操作
- 例えば:int=4 バイトサイズのデータ + 基本操作(
+-*/%
)、データ構造を連想できる。int、double タイプは本質的にデータ構造である - タイプデータ + タイプ操作 ➡️メンバー属性 + メンバーメソッド
- C 言語の struct の強化版と考えられる(属性を持つことはできるが、メソッドを持つことはできない)
アクセス権限
クラス内とクラス外の概念を区別する
アクセス権限はクラス内で設定され、クラス外がクラス内にアクセスできるかどうかを制御する
- public:公共
- クラス内とクラス外の両方からアクセスできる
- private:プライベート
- クラス内のメソッドのみがアクセスできる
- protected:保護された
- クラス内に加え、継承されたクラス内でもアクセスできる
- friendly:フレンド
- 修飾された関数はクラス内のプライベートメンバーと保護されたメンバーにアクセスできる
コンストラクタとデストラクタ#
オブジェクトのライフサイクル:構築→使用→破棄
3 種類の基本的なコンストラクタ#
ローカル変数の初期化を連想させる。オブジェクトも初期化が必要
コンストラクタタイプ | 使用方法 | ⚠️注意 |
---|---|---|
デフォルトコンストラクタ | People bob; プロトタイプ:People (); | 1、ゼロ引数コンストラクタ 2、コンパイラが自動生成する |
変換コンストラクタ | People bob("DoubleLLL"); プロトタイプ:People (string name); | 1、1 つの引数を持つコンストラクタ 2、その引数はクラスのメンバー変数に渡され、かつ本クラスの const 参照ではない [PS] いわゆる暗黙の型変換に似ている |
コピーコンストラクタ | People bob(hug); プロトタイプ:People (const People &a); | 1、特殊な引数を持つコンストラクタで、本クラスのオブジェクトを受け取る 2、代入演算子 "=" とは等価ではない [PS] const & として処理する方が便利 |
デストラクタ#
オブジェクトを破棄する
プロトタイプ:~People ();
⚠️注意:
1、引数はなく、戻り値もない
2、リソース回収時に使用する
小結#
どちらも戻り値がなく、関数名はクラス名と一致する
- プロジェクト開発では、コンストラクタとデストラクタの機能は非常にシンプルに設計されることが多い
- ❓なぜコンストラクタ内で大量のリソースを要求しないのか?
- 理由:コンストラクタのバグは、コンパイラが検出しにくい
- 解決方法:擬似コンストラクタ、擬似デストラクタ;ファクトリーデザインパターン
- [+] 移動コンストラクタ(別の重要なコンストラクタ、後で学ぶ)
- C++ 11 標準から ——C++ が再び神壇に戻った標準
- それ以前は、STL の性能が低下していた。C++ の言語特性が悪く、左辺値と右辺値の概念を区別しなかったため、STL 使用中に発生する大量のコピー操作、特に深いコピー操作が性能に大きく影響した
- その後、右辺値の概念が導入され、移動コンストラクタが追加された
戻り値最適化(RVO)#
コンパイラはデフォルトで RVO を有効にしている
導入#
オブジェクトaはfun()を通じて戻り値を構築する
出力結果:
- 理論的には:transform が 1 回、copy が 2 回出力されるべきである
- 詳細は具体的な分析を参照👇
- 実際には:copy は出力されず、a.x の値はオブジェクト temp の x 値、つまりローカル変数 temp のアドレスがオブジェクト a のアドレスを直接使用している(先に開かれたオブジェクト a のデータ領域を参照)
- temp はより参照のように見える
- コンパイラ最適化が存在し、すなわち戻り値最適化 RVO🌟
具体的な分析#
オブジェクト初期化プロセス:1、オブジェクトデータ領域を開く➡️2、コンストラクタをマッチさせる➡️3、構築を完了する
- オブジェクト初期化プロセスを理解した後、上記コードの構築プロセスを分析する:
- まず、オブジェクト a のデータ領域を開く
- 次に、func 関数に入る。オブジェクト temp のデータ領域を開き、「変換コンストラクタ」を通じてローカル変数 temp を初期化する ——A temp (69);
- さらに、「コピーコンストラクタ」を通じて temp を一時的な匿名オブジェクトにコピーし、オブジェクト temp を破棄する ——return temp;
- 最後に、「コピーコンストラクタ」を通じて一時的な匿名オブジェクトを a にコピーし、一時的な匿名オブジェクトを破棄する ——Aa= func ();
- 👉このように、プロセスには 1 回の変換コンストラクタと 2 回のコピーコンストラクタが含まれる
- コンパイラ最適化
- 第 1 回の最適化では、最初の「コピーコンストラクタ」をキャンセルし、temp を直接 a にコピーする(Windows での最適化)
- 第 2 回の最適化では、2 回目の「コピーコンストラクタ」をキャンセルし、temp が直接 a を指す(Mac、Linux での最適化)
RVO を無効にした場合#
g++ -fno-elide-constructorsでソースファイルをコンパイルすることで、RVOを無効にできる
- 2 回の追加の「コピーコンストラクタ」が発生した。具体的な分析は上記を参照
注意点#
- コンパイラは本質的に this ポインタを置き換えることで RVO を実現している
- コンパイラは一般的にコピーコンストラクタを最適化するため、プロジェクト開発ではコピーコンストラクタの意味を変更しないこと
- つまり:コピーコンストラクタではコピー操作のみを行い、他の操作を行わないこと
- 例えば:コピーコンストラクタ内でコピーされた属性に 1 を加えると、コピーコンストラクタの意味に合わず、コンパイラがコピーコンストラクタを最適化した後、結果が最適化前と一致しなくなる
+ 代入演算の最適化#
RVOも存在する——1回のコピーコンストラクタを最適化し、ローカル変数を一時的な匿名オブジェクトにコピーする
- 赤枠部分のコードを追加した
- 最適化前の結果:
- 1 回の「変換コンストラクタ」 + 1 回の「コピーコンストラクタ」
- 最適化後の結果:
- 1 回の「コピーコンストラクタ」
+ コピーコンストラクタの呼び出し分析#
さまざまな書き方で、コピーコンストラクタはどのように呼び出されるのか?
シナリオ:クラス A にカスタムクラス Data のオブジェクト d が含まれ、赤枠が追加されたコード
「主に 29 行目に注目;RVO を無効にしてコンパイルしないと、コピーコンストラクタをスキップする」
- カスタムコピーコンストラクタを定義し、各メンバー属性を明示的にコピーする。つまり、29 行目はそのまま
- オブジェクト d を構築する際、Data クラスのコピーコンストラクタが呼び出される
- カスタムコピーコンストラクタを定義せず、各メンバー属性を明示的にコピーしない。つまり、",d (a.d)" を削除する
- オブジェクト d を構築する際、Data クラスのデフォルトコンストラクタが呼び出される(カスタム時に明示的にコピーしなかったため、デフォルトコンストラクタにマッチする)
- コピーコンストラクタをカスタマイズせず、コンパイラが自動的にデフォルトのコピーコンストラクタを追加する。つまり、29〜31 行目を削除する
- オブジェクト d を構築する際、Data クラスのコピーコンストラクタ(コンパイラのデフォルト)が呼び出される
結論:
❗️期待される結果を得るためには、カスタムコピーコンストラクタを定義する際に、各メンバー属性を明示的にコピーする必要がある
- カスタムコピーコンストラクタに何も書かない場合、その関数は何も行わない
その他の知識点#
参照#
参照はそのバインドされたオブジェクトの別名である
- ❗️参照は定義時に初期化する必要があり、つまりバインドされたオブジェクトのように:
- People a;
- 定義時に初期化:People &b = a;
- そうでなければ:People &b; b = c; という曖昧さが生じる —— バインドされたオブジェクトか、代入か?
クラス属性とメソッド#
static キーワードが付加されている
メンバー属性(各オブジェクトに特有)やメンバーメソッド(this ポインタが現在のオブジェクトを指す)とは異なる
- クラス属性:そのクラスのすべてのオブジェクトに共通の属性
- グローバルにユニークで、共有される
- 例えば:全人類の数 —— 人類のすべてのオブジェクトの数
- クラスメソッド:特定のオブジェクトに属さないメソッド
- オブジェクトにバインドされず、this ポインタにアクセスできない
- 例えば:特定の身長が合法的な身長かどうかをテストする
const メソッド#
オブジェクトのメンバー属性を変更せず、非 const メンバ関数を呼び出すことはできない
- const オブジェクトに使用される(その属性は変更できない)
⚠️:
mutable:可変の、その修飾された変数は const メソッド内で可変である
default と delete#
デフォルト関数の制御、C++ 11
- default:コンパイラがデフォルトで提供するルールを明示的に使用する
- 特殊メンバー関数(デフォルトコンストラクタ、デストラクタ、コピーコンストラクタ、コピー代入演算子)のみが適用され、かつその特殊メンバー関数にはデフォルト引数がない
- ❗️この特性には機能的な意味はなく、C++ の設計哲学である:可読性、保守性などに注目する
- この理念を理解することで、C++ における自分の美的基準を向上させることができる
- delete:特定の関数を明示的に無効にする
struct と class#
- struct
- デフォルトのアクセス権限:public(ブラックリストメカニズム、private メンバーを明示的に定義する必要がある)
- クラスを定義するためにも使用され、メンバー属性とメンバーメソッドを持ち、C 言語と区別する必要がある
- class
- デフォルトのアクセス権限:private(ホワイトリストメカニズム:public メンバーを明示的に定義する必要がある)
❓:C++ はなぜ struct キーワードを保持するのか?デフォルトの権限はなぜ public なのか?
- すべては C 言語との互換性のためであり、普及の難易度を下げるためである
PS:フロントエンド言語 JavaScript は普及の難易度を下げるために、名前を Java の熱に乗っかっているが、本質的には Java とは無関係である
コードデモ#
クラスの例#
- クラス内の属性とメソッドの宣言と定義は分けることを推奨する
- this ポインタはメンバーメソッド内でのみ使用され、現在のオブジェクトのアドレスを指す
cout の簡単な実装#
- cout はオブジェクトであり、高度な変数である
- 自身の参照を返すことで連続した cout を実現でき、参照を使用する理由は後で理解する
- 文字列は const 型変数で受け取る必要があり、そうでなければ警告が出る。なぜなら文字列はリテラルだから
- 名前空間の本質:同じオブジェクト名が異なる名前空間に存在できる
コンストラクタとデストラクタ#
実行結果:
1)デストラクタの順序の考察#
- 構築順序:オブジェクト a、オブジェクト b
- デストラクタの順序:オブジェクト b、オブジェクト a
- ❓なぜデストラクタの呼び出し順序が逆になるのか?それはコンパイラが生成した特例なのか、それとも正常な言語特性なのか?👉言語特性
- オブジェクト b の構築はオブジェクト a の情報に依存する可能性がある➡️デストラクタの際にオブジェクト b もオブジェクト a の情報を使用する可能性がある➡️オブジェクト b はオブジェクト a より先にデストラクタを呼び出す必要がある
- ❗️誰が先に構築されるか、それが後にデストラクタを呼び出す
- PS
- これはオブジェクトがヒープ領域にあるかスタック領域にあるかに関係なく、実験によってデストラクタの順序は常に逆であることが示されている
- トポロジー順序の一種と考えられる
2)変換コンストラクタ#
- ❓なぜ変換コンストラクタ(単一引数のコンストラクタ)と呼ばれるのか?
- 変数をそのタイプのオブジェクトに変換するから
- 🌟a=123 は演算子オーバーロードを含む:暗黙の型変換➡️代入➡️デストラクタ、詳細はコードを参照
3)コピーコンストラクタ#
1、参照を追加:無限にコピーコンストラクタを呼び出すのを防ぐ
- ❗️もしコピーコンストラクタが:A (A a) {} であれば、A b = a; の時、
- [形参 a] が参照でない(値渡し)ため、[オブジェクト a] を [形参 a] にコピーして一時オブジェクトを生成する必要がある
- この時、再びコピーコンストラクタが呼び出され、このコピーコンストラクタも同様のプロセスを経る
- したがって無限再帰が発生する
- PS:参照は何のコピー行為も生じず、ポインタよりも便利である
2、const を追加:const 型オブジェクトが非 const 型のコピーコンストラクタを使用するのを防ぎ、エラーを報告する
- コピーされるオブジェクトが変更されるのを防ぐ
注:
- オブジェクトを定義する際、"=" はコピーコンストラクタを呼び出し、代入演算ではない。例えば、A b = a
4)考察#
- オブジェクトはいつ構築が完了したのか?
- 「コードを参考に、デフォルトコンストラクタの例を挙げる」
- 機能的な構築 [論理的に]:46 行目に到達した時、コンストラクタは表面的に実行を完了した
- コンパイラの構築[実際には]:39 行目に到達した時、すでにオブジェクトメンバーを呼び出すことができる❗️🌟
- 考察部分のコードを通じて理解できる:
- シナリオ:クラス Data に引数付きコンストラクタを追加し、コンパイラが自動的に追加したデフォルトコンストラクタを削除する
- プロセス:クラス A オブジェクトを生成する際、メンバー属性 Data クラスオブジェクト p、q はすでに構築を完了している必要があり、この時 Data クラスのデフォルトコンストラクタはすでに削除されている
- 結果
- 初期化リストを使用して p、q を初期化しない場合、エラーが発生する
- 初期化リストを使用すれば、成功する。初期化リストはコンパイラが言うところの構築に属する
- ❗️これはコンパイラの構築が関数宣言後(39 行目)に完了することを示している
- ⚠️:
- コンパイラはデフォルトコンストラクタとコピーコンストラクタを自動的に追加する
- 構築行為はすべてコンパイラが言うところの構築に置くべきであり、初期化リストを使用すること
+)左値参照#
- 後で学ぶ
+)フレンド関数#
- クラス内で宣言(同時にクラスの管理者の承認を保証する)
- クラス外で実装され、本質的にはクラス外の関数だが、クラス内のプライベートメンバー属性にアクセスできる
深いコピー、浅いコピー#
コピーオブジェクト:配列
- コンパイラがデフォルトで追加するコピーコンストラクタは浅いコピーである
- ポインタに対しては、アドレスのみをコピーするため、コピー後のオブジェクトの変更は元のオブジェクトを変更する
- カスタム深いコピー版のコピーコンストラクタ
- ポインタに対しては、その指すアドレスの値をコピーする
- PS:コンストラクタは初期化時にのみ呼び出され、自分自身をコピーする行為は存在しない
⚠️:
- オーバーロード "<<" 時の const パラメータに適応するため、const 版の "[]" オーバーロードを実装する必要がある
- Array クラスの end と配列の終わりのデータは関係がなく、配列の越境状況を監視するために使用される
new、malloc の違い分析#
実行結果:
- malloc と new はどちらもメモリを要求でき、free と delete(配列の場合は delete [])に対応してメモリを解放する
- new は自動的にコンストラクタを呼び出し、対応する delete は自動的にデストラクタを呼び出す。一方、malloc と free はどちらも呼び出さない
- malloc + new は原地構築を実現でき、深いコピーに一般的に使用され、その際、new は異なるクラスのコンストラクタにも対応できる
クラス属性とメソッド、const、mutable#
- クラス属性:クラス内で static を付けて宣言し、クラス外で static を付けずに初期化する
- クラスメソッド:オブジェクトまたはクラス名の 2 つの方法で呼び出すことができる
- したがって、オブジェクトが呼び出すメソッドは必ずしもメンバーメソッドであるとは限らず、クラスメソッドである可能性もある
- const:const オブジェクトは const メソッドのみを呼び出すことができ、const メソッド内では const メソッドのみを呼び出すことができる
- mutable:その修飾された変数は const メソッド内で変更可能である
default、delete#
機能要件:特定のクラスのオブジェクトがコピーできないようにする
- コピーコンストラクタと代入演算子を無効にする
- コピーコンストラクタ:=delete に設定するか、private 権限に置く
- 代入演算子オーバーロード関数:同様に = delete に設定する(決して代入演算を使用できない)、または private 権限に置く(クラス内メソッドのみが代入演算を使用できる)
- PS:代入演算子は const 版と非 const 版の両方を考慮する必要がある
- ❓オブジェクトが本当にコピーできないようにするには、コピーコンストラクタと代入演算子オーバーロードの両方を = delete に設定する必要がある。そうでなければ、クラス内メソッドはまだそのオブジェクトをコピーできる
追加知識点#
- コンストラクタ後の波括弧
- 波括弧を追加すると、関数実装を示す
- 波括弧を追加しない場合は、単なる関数宣言であり、専用の実装を書く必要がある
考察点#
- コンパイラがデフォルトで行う多くのことに注目することが重要であり、これが C++ の複雑な部分である
ヒント#
- C++
- 学習方法:プログラミングパラダイムに従って分類して学ぶ
- 学習の重点:プログラムの処理フロー [C よりもはるかに複雑]