【C++】クラス

クラスについて一通り言及しているわけじゃない。俺がちゃんと分かっていないことだけ。


UE5勉強の備忘録は何となく気分で全部消しちゃったけど、開発を諦めたわけじゃない。

UE5の勉強を始めてみたらC++17程度までは勉強しないとダメみたいだってことになって、本を一冊買ったはいいが、UE5の方が勉強しなきゃならないことが多すぎてC++は放っておいた。

で、とりあえずUE5を使って組み始めたのはいいんだが、やはり早くも躓いた。抽象クラスなるものを扱った事がなくて、定義もできないしキャストもできない。なのでこれからそのお勉強だ。

これだとブログになっちゃうんだと思うけど、今後ここではその程度のことをメモするだけになるかもしれない。


昨日まではタイトルが「【C++】抽象クラス?」で、本文は上の区切り線で終わっていたんだが、追記することにした。クラス定義だけでもかなり変更されているようなので、こうして書かないと覚えられない。UE5の備忘録も本当は備忘録と言うよりも書いた方が頭に入るから付けていただけ。後で何だったっけ?となった時には自分の備忘録を読み返すよりも公式ドキュメントを読みに行った方が確実だし、他のことも覚えられるかもしれない。

参考書

参考書は翔泳社の「独習C++新版」
本来なら第1章の「C++の基本的な言語機能①」から勉強するべきなんだろうけど、昨日の続きでクラスから始める。

constメンバー関数とmutable

いきなり驚いた。こんなの以前は無かったはずだ。UE5ソースを眺めていて、なんか余計なconstがやたらと目に付くなぁ、とは思っていたんだが。constを使うと本当に速度が上がるのかねぇ。中高年者からするとconstはROMみたいなイメージがあって、ROMとRAMならROMのが遅いだろ、みたいなのがある。パソコンで動かすソフトならROM化されることもないわけだし、もしかしたら386のパイプライン処理がどうたらから、constのアクセス速度は本当に向上するようになっているのかもしれない(適当)。だいぶ前だが、人間がアセンブリ言語でいろいろ切り詰めるコードを必死ぶっこいて書くよりも、C++コンパイラの方がCPUにとって最適化したコードが作れるからアセンブリ言語を使うメリットが無くなった、もう人の手による最適化じゃ敵わない、と聞いたことがあった。
前置きが長くなったが、話を戻すと言うか始める。

constメンバー関数は、クラスをconstでインスタンス化された時に呼び出されるメンバー関数を定義できる。(メモ書きなのでサンプルコードは書かない。参考書を見るように)

mutableは、constメンバー関数から変数を書き換えたい場合に変数宣言でmutableを指定すると書き換えられるようになる。
(参考書のサンプルコードはクラスがconstでインスタンス化されていなかった。その場合にどうなるのか不明。流石にエラーになるのか?)
たぶんmutableは一生使わないと思う。

コンストラクターと他の関数との違い

省略しようと思ったんだが、これまた知らない話があった。
「メンバー初期化リスト」
コンストラクタを次のように記述できるらしい。
classname::classname() : member(initial-value), member(initial-value)・・・
{
    constractor-body・・・
}

俺が知らなかっただけでC++98時点でもあったんだろうか?

委譲コンストラクター

メンバー初期化リストにメンバー変数と初期値を書くのではなくて、他のコンストラクタを呼び出して、そいつに初期化をやらせることができるらしい。
いろいろ使い回しができてコードを短くできるのかもしれないけど、こういう真似をしまくるとコードを追うのも大変になりそう。だけど同じコードをあちこちで書くよりはマシってわけか。

コピーコンストラクター

これは昔からあったのかもしれないが、意識したことがなかった。
クラスをコピーをする場合のコンストラクタはプログラマーが書くべきらしい。そもそもクラスのコピーだと言って同じアドレスを使う時点でおかしいと思うんだが、そうした場合でも問題が出ないようにコピー用のコンストラクタを呼び出せるようになっているんだと思う。これだけじゃなんのこっちゃ分からないと思うけど詳細は参考書を見るべし。(実はまだ俺もよく分かっていない)

=を使った初期化とexplicit指定子

クラスにint型の1つの引数があるコンストラクタがある場合は次が同じになる。
classabc x(42); // 明示的なコンストラクタ呼び出し
classabc y = 42;  // 暗黙のコンストラクタ呼び出し

このclassabcのコンストラクタの宣言を次のようにすると暗黙のコンストラクタ呼び出しをエラーにすることができるらしい。
explicit classabc(int);

コンストラクタが複数ある場合に暗黙のコンストラクタ呼び出しをさせると実際には何が呼び出されているかよく分からない場合もあるから、それを防ぎたい時に使うらしい。

デフォルトの初期値(メンバー変数の初期値、メンバー初期化リストと初期値)

確かC言語では構造体の宣言で初期値を決めておくことができなかったと思うんだが、C++ではC++98の頃からできるようになっていたんじゃなかったっけか?enumとかもよく使った覚えがある。まあとにかく変数の初期化は代入文などで普通にできる。非静的メンバー変数の初期化子(Non Static Data Member Initializer)で普段はNSDMI(エヌエスディーエムアイ)と呼ばれているんだそうだ。さすが本を書くだけのことはある人だ。俺はNSDMIなんて初めて聞いた。本当にそんな呼ばれ方をしているのか?と気になってググってみたら9070件ヒットした・・。ついでに言うと非静的メンバー変数の初期化子なんて言い方もしたことがないし聞いたこともなかった。ただ、この参考書の著者はその辺りの「歴史的な事情」というのを知っているらしくて相当詳しいようだ。俺なんかとは天と地の差なんだろう。と思わず拗ねてしまうが、実際この本、とても読み易い。当然、俺は関係者でも何でもないがマジでおススメさせてもらう。
随分脱線したが、なんでこんな話になるかというと、クラス内のメンバー変数をNSDMIで普通に初期化できるわけだが、コンストラクタにメンバー初期化リストなる機能というか仕様ができたから、どっちで初期化されるのか?という話になるわけだ。

class A
{
    int temp1 = 1; // 普通の初期化(NSDMI)
    int temp2 = 2; // 普通の初期化(NSDMI)
    A();
};
A::A() : temp1(3) // 初期化リスト
{
};
A test;

この時のtest.temp1の値は3、コンストラクタのメンバー初期化リストが優先されるそうだ。test.temp2は普通に2になるらしい。

仮想関数とオーバーライド

とりあえず継承&派生については問題ないはずとして仮想関数の知識は怪しいので真面目に勉強しておく。

基底クラスで変更されても良いメンバー関数にvirtualを付けて宣言すると仮想関数になる。
virtual return-type function-name(parameters・・・); // 仮想関数
実際の関数定義にはvirtualを付けない。

派生クラスで仮想関数を上書きする関数にoverrideを付けて宣言すると基底クラスの仮想関数をオーバーライドできる。
return-type function-name(parameters・・・) override; //オーバーライド
実際の関数定義にはoverrideを書かない。
※override指定子は省略可能だそうだ。しかしオーバーライドしているつもりが基底クラスの仮想関数を変更してそのままとなってしまってオーバーライドじゃなくて、ただのオーバーロードになっちゃったりすることもあるから、そういうのを防ぐために必ずオーバーライドするんだって場合は省略しない方が良かったりするらしい。

もっと複雑な何かがあるかと思っていたんだが、実はシンプルなものだったのね。継承の継承でオーバーライドするとかになっても同じことなんだろう。

名前の隠蔽

基底クラスのメンバー関数と同じ関数名を派生クラスに追加すると名前の隠蔽が起こる。そういう場合は派生クラスで基底クラスのメンバ関数をusing宣言してやればいい。詳しくは参考書。

純粋仮想関数と抽象クラス

純粋仮想関数は最初からあった気がするなぁ。仮想関数が上記した程度のシンプルなものじゃなかったはず。だとしたら抽象クラスのことは忘れていただけなのか。よく分からない。最初から仮想関数のことはよく分からなかった。使いどころからして分からない。
virtual return-type function-name(parameters・・・) = 0; // 純粋仮想関数。
純粋仮想関数は実体がない。

参考書に純粋仮想関数の分かり易いサンプルコードがあった。クラスや関数を処理単位で分けるのではなくて、機能単位で分けようって考えるからこうなるわけだ。図形の面積や周囲長の求め方は図形のカタチによって変わってくる。しかし根本の”図形の面積を求める”という機能は同じ。処理の仕方が違うだけ。だから基底クラスには面積を求める純粋仮想関数と周囲長を求めるそれの2つを定義しておいて、継承した派生クラスでそれぞれオーバーライドして算出できるようにするわけだ。
で、純粋仮想関数が宣言されているクラスを抽象クラスと呼ぶんだそうだ。純粋仮想関数には実体がないから抽象クラスはインスタンス化できない。

オブジェクトポインター(アロー演算子、thisポインター)

昔と変わってなさそうなので省略しようと思ったけど、constメンバー関数内のthisポインターの扱いだけ。
constメンバー関数の中のthisポインターはconstポインターになる。

クラスと構造体と共用体

構造体も共用体もクラスのように宣言して関数を持ったり、クラスから派生することもできるらしい。ただし普通のクラスと違って制限がある。共用体を基底クラスにできないだとか仮想関数を定義できないとか。詳しくは参考書。

無名共用体

クラス内に型も名前もない共用体を作っておくと、その中の共用体メンバーを普通のクラスのメンバー変数のようにアクセスしながら共用体として使えるらしい。
グローバルに無名共用体を宣言することもできて、その場合は普通のグローバル変数のようにアクセスすれば良いらしいが、static宣言する必要があるんだとか。まあ使わないだろうな。

フレンド関数

使ったことがないけど以前からあったはず。参考書には演算子のオーバーロードやファクトリ関数というのを提供するのに便利だと書いてある。

staticクラスメンバー

『クラスメンバーの一種ではあるのですが、特定のインスタンスと結び付かないメンバー変数やメンバー関数のことです』とのこと。

staticメンバー変数は、クラス内で宣言するけど、クラス外で定義してメモリを確保してやらないと使えないってことらしい。
この特性があるから複数のインスタンスで同じメモリにアクセスすることになる。グローバル変数に似ている。
カプセル化については普通のメンバー変数と同じ。privateのstaticメンバー変数ならグローバルにメモリを割り当てても、そのクラスからしかアクセスできない。

staticメンバー関数は、クラス名とスコープ解決演算子(コロン2つのやつ、これ→ ::)を使って呼び出すことができる。どのインスタンスにも結び付かないからstaticではないクラスメンバーにそのままアクセスできない。それとconstにはできないらしい。

使う時には忘れてそうだから参考書を見ながら作る。

また必要になったり気が向いたりしたら、こうして一章ずつ勉強しようと思う。

この記事が気に入ったらサポートをしてみませんか?