こんにちは。田原です。
前回まででクラスの基本の重要部分の解説が終わりましたが、あともう少し説明しておくべきことが残ってます。今回はその残りのstaticメンバとクラス宣言の外でメンバを定義する方法などについて解説します。
1.クラスとオブジェクトとインスタンス
今まで、クラス型の変数などと呼んでましたが、クラス型の変数やnewで獲得したクラス実体のことを、インスタンスやオブジェクトと呼ぶことが多いようです。
クラスも含めてオブジェクトと呼ばれることもあります。例えば「オブジェクト指向プログラミング」ですね。
01 02 03 04 05 06 07 08 09 10 11 12 13 | class Foo { int mData; public : Foo() : mData(123) { } }; int main() { Foo mFoo; Foo* pFoo= new Foo(); delete pFoo; } |
というプログラムがあるとき、mFooはFooクラスのインスタンスでありオブジェクトです。
pFooが指す先もFooクラスのインスタンスでありオブジェクトです。
当講座では
クラス型、基本型を問わず実体のことを「インスタンス」と呼ばせて頂きます。(*1)
また、オブジェクトと呼ぶ時はクラス型(class/struct)の実体です。
なお、例えばFoo型へのポインタと呼んでますが、厳密にはFoo型のインスタンスへのポインタと呼ぶべきです。長いので省略させて頂いています。
(*1)
int型等の基本型やenum型の実体を示す言葉が欲しいのですが、あまり見かけません。なので、これらも含めて「インスタンス」と呼びます。
2.staticメンバ
第20回目で解説したようにメンハには、非staticメンバ変数と関数、staticメンバ変数と関数があります。
2-1.staticメンバ変数
通常のメンバ変数定義の頭にstaticと書くと、staticメンバ変数になります。
通常の(非staticな)メンバ変数はそのクラスのインスタンスを生成する度に確保されますが、staticメンバ変数はクラス1つ毎に1つだけ定義されます。そして、その寿命はプログラムの開始~終了までです。つまり、staticメンバ変数はグローバル変数やstaticなローカル変数と同様に静的変数用メモリに確保されます。
また、staticメンバ変数は初期化子リストでは初期化できません。初期化子リストはクラスのインスタンスを生成するためにコンストラクタが呼ばれた時に使われます。staticメンバ変数はそれより前に生成されますから、初期化子リストでは初期化できないのです。
代わりにグローバル変数と同じように初期化することができます。
グローバル変数は型 変数名=初期値;
の構文で初期化します。
staticメンバ変数も同様です。ただし、メンバ変数名だけでは複数のクラスに同じ名前のメンバ変数があると混乱しますので、型 クラス名::非staticメンバ変数名=初期値;
の構文で初期化します。
01 02 03 04 05 06 07 08 09 10 11 | class Foo { static int sData; // staticメンバ変数 int mData; // 非staticメンバ変数 public : Foo() : // sData(0), // staticメンバ変数は初期化子リストでは初期化できない mData(0) }; int Foo::sData=123; // staticメンバ変数はクラス宣言の外で初期化します |
staticメンバ変数はクラス宣言の外で初期化します
実はこれは初期化だけでなくstaticメンバ変数の実体定義でもあります。
クラス宣言は通常ヘッダ・ファイルに記述し、複数のcppファイル(コンパイル単位)からインクルードされます。そのため、もし、クラス宣言の「中」に初期化を記述した場合、その実体をどのコンパイル単位で生成すれば良いのかコンパイラが判断できません。
そこで、クラス宣言の外で定義することとし、どのコンパイル単位で実体を確保するのかプログラマが指定することになっています。そのついでに初期化します。グローバル変数はcppファイルで定義しヘッダでextern宣言することが多いのですが、それと同じ関係です。クラス宣言に含まれるstaticメンバ変数はグローバル変数のextern宣言と同じく使うことの宣言だけで実体は定義されないのです。
staticメンバ変数定義の例外
constやconstexprが付いて「定数」として定義されたstaticメンバ変数は上記とはちょっと異なりますが、結構複雑です。
constについては後日解説しますが、上記使い方も可能ですので現在はあまり気にしないで下さい。
constexprについては、入門の範囲をかなり超えますので、当講座では扱いません。ボレロ村上氏が非常に詳しく解説されています。興味のある方は「ボレロ村上 constexpr」で検索すると多数の解説記事が見つかります。(そもそもかなり難しい概念ですので、歯ごたえあります。)
staticメンバ変数は「クラス名::メンバ変数名
」の構文でアクセスできます。
もちろん、当該クラスの変数を指定してアクセスすることもできます。「当該クラスの変数.メンバ変数名
」、および、「当該クラスへのポインタ->メンバ変数名
」です。
2-2.staticメンバ関数
staticメンバ関数は、通常のメンバ関数定義の頭にstaticと記述して定義します。
これは、第20回で解説した非staticなメンバ関数に似ていますが、隠しパラメータであるthisが渡って来ない点が異なります。感の良い方は気がついたと思いますが、要するにグローバル関数とほぼ同じものです。
メンバ関数との相違点は次の通りです。
- thisがないのでクラスの非staticなメンバ変数とメンバ関数に直接アクセスできません
クラスは複数の変数を確保したり、newしたりします。thisが無いということは複数の変数のどれにアクセスするべきか判断できないため、staticメンバ関数は非staticなメンバ変数とメンバ関数にアクセスできません。 -
当該クラスの変数が無くても呼び出せます
「クラス名::メンバ関数名(引数)
」の構文でアクセスできます。
もちろん、当該クラスの変数を指定してアクセスすることもできます。
「当該クラスの変数.メンバ関数名(引数)
」、および、「当該クラスへのポインタ->メンバ関数名(引数)
」です。
なお、後者の構文の場合もstaticメンバ関数にthisが渡らないため、メンバ変数に直接アクセスすることはできません。
また、普通のグローバル関数との相違は、指定されたクラスのprivateメンバやprotectedメンバへアクセス出来る点です。privateメンバ変数にアクセスできるってちょっと不思議ですが、直接ではなくクラス変数を介してアクセスできると言う意味です。
2-3.staticメンバ変数と関数のサンプル・ソース
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | #include <iostream> class Foo { static int sData; // staticメンバ変数 int mData; // 非staticメンバ変数 static void FuncStatic() // staticメンバ関数 { std::cout << "FuncStatic() sData=" << sData << "\n" ; } void Func() // 非staticメンバ関数 { std::cout << "Func() sData=" << sData << ", mData=" << mData << "\n" ; } public : Foo( int iData) : mData(iData) { } static void PublicFuncStatic(Foo& iFoo) { // staticメンバ・アクセス std::cout << "--- static member ---\n" ; std::cout << "sData=" << sData << "\n" ; std::cout << "[1] " ; FuncStatic(); // クラス名省略可 std::cout << "[2] " ; Foo::FuncStatic(); // 変数無しでアクセス可能 std::cout << "[3] " ; iFoo.FuncStatic(); // 変数有りでもアクセス可能 // 非staticメンバ・アクセス std::cout << "\n--- non static member ---\n" ; // std::cout << "mData=" << mData << "\n"; // 直接アクセスできない std::cout << "mData=" << iFoo.mData << "\n" ; // 間接的にアクセスできる // Func(); // 直接アクセスできない iFoo.Func(); // 間接的にアクセスできる } }; int Foo::sData=123; // staticメンバ変数実体定義 int main() { std::cout << "=== aFoo0 ===\n" ; Foo aFoo0(456); Foo::PublicFuncStatic(aFoo0); std::cout << "\n=== aFoo1 ===\n" ; Foo aFoo1(789); aFoo0.PublicFuncStatic(aFoo1); } |
3.メンバのクラス外定義について
今まで、クラスの各メンバ関数をクラス宣言の内部で定義していました。
これはこれでメリットがあるのですが、もう一つの方法があります。クラス宣言の内部ではメンバ関数の宣言のみ行い、実体の定義をクラス宣言の外部で行う方法です。
非staticメンバ変数はクラス内でのみ定義できます。
staticメンバ変数はクラス外でのみ定義できます。(const staticメンバ変数は例外で内と外の両方可)
クラス外で定義する時の構文は原則としてクラス内定義する時と同じですが、下記の点が異なります。
- クラス自体の宣言を先にしておくこと
- メンバ関数名の前に「クラス名::」を記述し、どのクラスのメンバ関数を定義するのか指定する
- static指定を外す
- コンストラクタの初期化子リストは定義側に記述する
(クラス宣言のコンストラクタの宣言には書かない)
ODR(One Definition Rule)
クラス外定義は1つのプログラム内で原則として1つだけ許されます。
複数回定義するとどれが正しいのか判らなくなる危険があるため、禁止されています。
これをODR(One Definition Rule)と呼びます。ODRにはいくつかの例外があります。その一つがinline関数です。後述しますが、例えば上記のvoid Foo::Func() { … }の頭にinlineと記述した場合、全く同じ記述であり、かつ、異なるコンパイル単位であれば同じプログラムの中で複数回出現することが許されます。
また、クラス宣言が複数のコンパイル単位に出現してよいこともODR例外です。もちろん、全く同じものにすることはプログラマの責任です。異なった時の振る舞いは未定義です。
ですので、クラス宣言をヘッダ・ファイルに記述し、#if等で意図的に変更しない限り全く同じにするのが一般的なのです。
3-1.メリットとデメリット
- クラス外定義する時のメリット
- 複数のクラスで相互参照を解決できます
クラスFooとBarがあり、Barの関数がFooを用い、Fooの関数がBarを使うことができます。 - cppにて実体定義した場合にメリットが2つあります
使う側は実体定義をインクルードしないで済むのでコンパイルが速いです。
提供する側は使う側から実体定義を隠すことができます。商用のライブラリ等で実体を隠したいようなケースでは特に有用です。
- クラス内定義する時のメリット
- 関数宣言を2回書かなくて良いです
DRY(Don’t Repeat Yourself)原則(同じことを何度も書かない)はなかなか重要です。同じことを2度書くと、それを修正する手間が2倍です。関数の引数を変更したい時は意外にあります。更にオーバーロード関数群全てに同じような変更をしたい時、手間が倍になるのは結構嫌なものです。 - 関数呼び出しがインライン展開されると高速です
インライン展開とはスタックに戻り番地を積んでサブルーチン・コールではなく、直接呼び出部に関数の中身が展開されることです。サブルーチン・コール処理が省かれるので微妙に高速です。特にメンバ変数の値を返却するなどの比較的小さな関数では効果的です。
インライン展開するかどうかはコンパイラが独自の基準で行いますが、cppにてクラス外定義した場合、他のコンパイル単位からの呼び出しがインライン展開されることはありません。
クラス内定義 vs クラス外定義
どちらを選択するのか結構悩ましく、人によってそれぞれと思います。
私は、概ね以下のような基準で選択しています。1. できるだけクラス内定義します。
メンテナンスをできるだけ楽にしたいためDRY原則を重視するからです。2. 例外は以下の通りです。
相互参照が発生する時やstaticメンバ変数等、クラス外定義するしか無い時。
複数のコンパイル単位からインクルードされ、かつ、大きなメンバ関数。逆に「できるだけクラス外定義し、必要な時や小さな関数のみクラス内定義」する方針の人も少なくありません。
どちらが良いか決定しようとすると、恐らく「宗教戦争」が勃発すると思います。皆さんは皆さんの経験によって皆さんにとって適切な方針を作り上げて下さい。
3-2.「cppで定義した場合」の補足説明
クラス外定義の場合、メンバ関数をcppファイルで定義することが多いのですが、ヘッダで定義することも可能です。
ヘッダは複数のコンパイル単位からインクルードされることが多いですが、通常通り定義し、かつ、複数のコンパイル単位からインクルードされるとODR違反となり、リンカにて多重定義エラーが発生します。
しかし、上記のODRのところでちょっと触れましたが、inline関数として定義することでODR違反を回避できます。
ただし、この例のケースでは素直にクラス内で定義した方が解りやすいです。
相互参照のためにクラス外定義が必要で、かつ、ヘッダで定義したい時に使うテクニックです。
あまり頻繁に使うことはないですが、お手軽に使いたいツールを提供する時など(ヘッダだけならインクルードするだけで使えるので)便利な場面が幾つかあります。また、クラス・テンプレートを使う時、必須になる場合もあります。
3-3.クラス外定義による相互参照解決の例
言葉だけでは分かりにくいと思いますので、サンプル・ソースです。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <iostream> struct Bar; struct Foo { int mData; Foo( int iData) : mData(iData) { } void Func(Bar& iBar); }; struct Bar { int mData; Bar( int iData) : mData(iData) { } void Func(Foo& iFoo); }; inline void Foo::Func(Bar& iBar) { std::cout << "Foo::Func(Bar& iBar) iBar.mData=" << iBar.mData << "\n" ; } inline void Bar::Func(Foo& iFoo) { std::cout << "Bar::Func(Foo& iFoo) iFoo.mData=" << iFoo.mData << "\n" ; } int main() { Foo aFoo(123); Bar aBar(456); aFoo.Func(aBar); aBar.Func(aFoo); } |
4.クラス内定義のクラスとenum型
クラスの中でクラスやenum型を定義することができます。
なお、クラスAの中で定義したクラスBのメンバ関数をクラス外定義できますが、最も外側のクラスの更に外側でのみ可能です。つまり、クラスBの直ぐ外のクラスAの中では定義できません。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> struct Outer { enum InnerEnum { sym0, sym1, sym2 }; struct Inner { static void Func(); }; // void Inner::Func() { } // ここでは定義できません。 }; void Outer::Inner::Func() // ここで定義できます。 { std::cout << "Outer::Inner::Func()\n" ; } int main() { Outer::Inner::Func(); std::cout << "Outer::InnerEnum::sym0=" << Outer::InnerEnum::sym0 << "\n" ; std::cout << "Outer::sym1=" << Outer::sym1 << "\n" ; // std::cout << "sym2=" << sym2 << "\n"; // Outer::が必要 } |
Wandboxで試してみる(コメントアウトしている行を有効にしてみて下さい。エラーになります。)
InnerEnumはunscoped Enum型なのでenum型名無しでシンボルを使えます。しかし、Outerクラス内で定義されていますので、Outer::を記述しないと使えません。他のクラスの中で同じInnerEnumが定義することも可能だからOuterで定義されていることを指定する必要があるためです。
5.まとめ
今回でクラスの基本的な使い方を一通り解説できたと思います。ここまでを使いこなすことで基本的なC++プログラムを作ることが可能な筈です。ここまで学んでやっと「基本かよ」という感じがするかもしれませんね。
でも、基礎は大事です。基本的知識があればプログラム全体の80%位(体感ですが)をコーディングできます。疎かにすると痛い目に合いやすいですし、逆に基礎さえマスターすればプログラムの大半を記述できるのです。
さて、次回から入門としては大詰めに入り、C++の少し応用的な部分について解説します。
次回は、例外的なエラーを返却する「例外」です。プログラムの「異常系」を担う機能です。エラーチェック用のif文を大幅に減らせる頼もしいやつです。でも、ちょっと危険な機能てす。
お楽しみに。