static (静的) | Programming Place Plus C++編【言語解説】 第15章

C++編【言語解説】 第15章 static (静的)

先頭へ戻る

この章の概要

この章の概要です。


static (静的)

この章では、C++ における static指定子の使い道をまとめてみます。

知っての通り、static指定子はC言語にも存在しており、C言語編では、ローカル変数(第22章)、グローバル変数(第23章)、関数(第23章)に付ける使い方を説明しています。これらの意味合いは、基本的に C++ でも同様ですが、オブジェクトが絡むと注意事項が増えるので、改めて確認していきます。

静的ローカル変数

static指定子を付けて宣言されるローカル変数は、静的ローカル変数になります。このような変数は、静的記憶域期間を持つようになります(C言語編第23章参照)。

void func(int num)
{
    static MyClass c;

}  // オブジェクト c は削除されないので、MyClassクラスのデストラクタは呼び出されない

このように書いた場合、func関数を抜け出しても MyClassクラスのデストラクタは呼び出されません。静的ローカル変数として作られたオブジェクトは、一度作られればずっとメモリ上に存在することになるため、プログラム終了時までデストラクタが呼び出されることがありません。

静的ローカル変数が使用するメモリの確保は、プログラム開始時に行われています。また、ゼロ初期化という方法で初期化済みになります。ゼロ初期化は、非常に簡略化していえば、すべて 0 で初期化するというものです。

明示的な初期化子による初期化が行われるタイミングは、C言語とは異なっています。C++ では、静的ローカル変数の定義箇所が初めて実行されるときに、初期化子を使った初期化が行われます。つまり、コンストラクタは、このタイミングで呼び出されます。

このため、次のようなコードはC言語ではエラーになりますが、C++ では問題ありません。

void func(int num)
{
    static int s = num;  // C言語ではプログラム開始時点で初期化しようとするが、そのタイミングでは num の値が分からないのでコンパイルエラー
}

staticメンバ変数

メンバ変数にも static指定子を付けることができ、これはstaticメンバ変数と呼ばれます。

通常のメンバ変数が、オブジェクト1つごとに別個に存在しているのに対して、staticメンバ変数は、クラスに対して1つだけしか存在しません。そのため、オブジェクトを生成せずに使用することができます。また、オブジェクトを複数生成したとしたら、staticメンバ変数はそれぞれで共有されます。

このような特徴を持つため、staticメンバ変数は、オブジェクトそれぞれが必要とする共通情報を管理することに利用できます。例えば、以下の例では、現在インスタンス化されているオブジェクトの総数を管理しています。

// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
    MyClass();
    ~MyClass();

private:
    static int msObjectCount;  // staticメンバ変数の宣言
};

#endif
// MyClass.cpp

#include "MyClass.h"

// staticメンバ変数の実体
int MyClass::msObjectCount = 0;

MyClass::MyClass()
{
    ++msObjectCount;
}

MyClass::~MyClass()
{
    --msObjectCount;
}

staticメンバ変数は、クラス定義内に記述しただけでは定義したことになりません。このサンプルプログラムで、MyClass.cpp の側に書いているように、実体となる定義を別のところに書く必要があります。

初期値を与えたければ、実体の方に記述します。初期値を明示的に与えなかった場合には、デフォルトコンストラクタ(第13章)が使用されます。

また、staticメンバ変数の名前の先頭に「ms」を付けましたが、「s」があることで「static」であることを示しています。もちろん、こういった命名方法は強制されたものではありませんが、当サイトではこれで統一します。

クラスの外から、staticメンバ変数にアクセスするには、「クラス名::staticメンバ変数の名前」という形で、スコープ解決演算子を使って記述します(もちろん、「公開」されている必要があります)。

クラス定義内や、メンバ関数の定義内などから、自クラスの staticメンバ変数へアクセスする場合は、単に staticメンバ変数の名前だけを使って記述できます

ところで、staticメンバ変数のメモリ領域ですが、static でないメンバ変数とは別のところに取られています。先ほどのサンプルプログラムのように、staticメンバ変数の実体は、クラス定義の外側にあるのです。オブジェクトの個数とは無関係に、プログラム全体で1つしか存在しないので、オブジェクトの大きさに影響を与えません。

staticメンバ関数

メンバ関数にも static指定子を付けることができ、これはstaticメンバ関数と呼ばれます。

staticメンバ関数は、オブジェクトを生成せずに呼び出すことができます。ですから、「公開」されていれば、事実上、通常の関数と同条件になりますが、クラスに含まれていることによって、スコープを限定できるという点が特徴的です。

オブジェクトが生成されていなくても呼び出せるので、staticメンバ関数から、static でないメンバをアクセスすることはできません。static でないメンバ関数から、staticメンバ関数を呼び出すことには問題ありません。

「staticメンバ変数」の項で取り上げたサンプルプログラムに、staticメンバ関数を追加してみます。

// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
    MyClass();
    ~MyClass();

    static int GetObjectCount();

private:
    static int msObjectCount;  // staticメンバ変数の宣言
};

#endif
// MyClass.cpp

#include "MyClass.h"

// staticメンバ変数の実体
int MyClass::msObjectCount = 0;

MyClass::MyClass()
{
    ++msObjectCount;
}

MyClass::~MyClass()
{
    --msObjectCount;
}

int MyClass::GetObjectCount()
{
    return msObjectCount;
}
// main.cpp
#include <iostream>
#include "MyClass.h"

int main()
{
    std::cout << MyClass::GetObjectCount() << std::endl;

    MyClass c1;
    MyClass* c2 = new MyClass();
    std::cout << MyClass::GetObjectCount() << std::endl;

    delete c2;
    std::cout << MyClass::GetObjectCount() << std::endl;
}

実行結果:

0
2
1

関数宣言時に、先頭に static指定子を付ける以外は、通常のメンバ関数と同じように宣言・定義すれば良いです。

なお、staticメンバ関数をオーバーロードしたり、デフォルト実引数を持ったり、インライン関数にしたりすることにも制限はありません。

テンプレート化することも可能です(第19章

名前空間スコープの場合

グローバル名前空間も含めて、名前空間スコープ内の名前は、デフォルトで外部結合ですが、static指定子を付けて宣言すると内部結合になります。

const が付加されている場合にも内部結合になります(第18章

内部結合にすることが目的であれば、無名名前空間を使う方法もあります第3章)。static指定子は使用箇所によって意味が異なるため、無名名前空間の方が明確で良いでしょう。

// グローバル名前空間は外部結合
int X1 = 0;          // 外部結合
static int X2 = 0;   // 内部結合

// 名前空間は外部結合
namespace N1 {
    int X3 = 0;      // 外部結合
    
    namespace {
        int X4 = 0;  // 内部結合
    }
}

// 無名名前空間は内部結合
namespace {
    int X5 = 0;      // 内部結合
    
    namespace N2 {
        int X6 = 0;  // 内部結合
    }
}


非ローカルの静的変数の初期化順序

異なる翻訳単位に、非ローカル変数の静的変数が定義されている場合、初期化順序に関する注意すべき点があります。該当する静的変数は、以下のものです。

異なる翻訳単位内に、これらの静的変数の定義がある場合、どの変数から初期化されるかは不定です。そのため、初期化順序に依存するような使い方に注意しなければなりません。

例えば、a.cpp にある静的変数 a の初期値として、b.cpp にある静的変数 b を使うようなプログラムは、結果が不定です。もし、a の方が先に初期化されてしまうと、その時点ではまだ b に正しい初期値が入っていないため、恐らく a の初期値は想定外のものになるでしょう。

この問題を回避するために、a や b を関数内で静的ローカル変数として定義し、その関数が参照を返すようにする方法があります。

// a.cpp
A& GetA()
{
    static A a(GetB());
    return a;
}

// b.cpp
B& GetB()
{
    static B b(123);
    return b;
}

静的ローカル変数の初期化は、実行時にその定義に初めて行き着いたときに行われるため(前述)、順序の問題は起こり得なくなります。先に GetA() が呼び出されたとすれば、

  1. GetA() が呼び出され、a を初期化しようとして、GetB() を呼び出す
  2. b が初期化される
  3. b の参照が返され、これを使って a が初期化される

という順序で実行されますし、先に GetB() が呼び出されたのなら、その段階で b が初期化済みになるので、いつ b が使われても問題ありません。

  1. GetB() が呼び出され、b が初期化される
  2. GetA() が呼び出され、a を初期化しようとして、GetB() を呼び出す
  3. すでに b は初期化済みなので、即座に参照が返され、これを使って a が初期化される

となり、やはり問題なく初期化できます。

staticクラス

メンバがすべて static になっているクラスのことを、staticクラス(静的クラス)と呼ぶことがあります。別段特別な機能ということではなく、クラスの特徴的な使い方という程度のものです。

staticメンバはオブジェクトを生成せずに利用できるため、staticクラスはインスタンス化の必要性が無いという点が特徴的です。そのため、staticクラスを作るのであれば、インスタンス化を禁止するように設計するのが親切でしょう。

C++ でインスタンス化を禁止するには、コンストラクタを「非公開」にします。

class MyClass {
private:
    MyClass();  // 唯一のコンストラクタが「非公開」なら、インスタンス化できない
};

なお、このコンストラクタが呼び出される可能性は無いので、実は実装を書く必要もありませんし、書くべきでもありません。もし、次のように MyClassクラスをインスタンス化しようとしたとします。

int main()
{
    MyClass* c = new MyClass();
}

この場合、コンストラクタが「非公開」ですから、アクセスできずにコンパイルエラーになるので、問題ありません。問題なのは、MyClassクラス自身のメンバ関数の中で、コンストラクタを使おうとしてしまった場合です。

void MyClass::func()
{
    MyClass* c = new MyClass();
}

この場合、自分のクラスが持っているメンバへのアクセスですから、「非公開」であっても関係ありません。そのため、アクセスできないことを理由にコンパイルエラーになることはありません。しかし、インスタンス化しないようにしたいという目的があるので、何とかエラーにしたいところです。

そこで、コンストラクタの実装をあえて書かないという手段を採ります。すると、コンパイル自体は通ってしまいますが、リンクの段階で、関数の実装が見つからないという意味合いのリンクエラーが起こります。

C++11 の場合、コンストラクタを delete で消してしまう方が良いです(第13章)。この方法ならば、上述のどちらの場合でもコンパイルエラーになります。

staticクラスの価値は、ある種の機能群を1つのスコープにまとめることにあります。1つのヘッダファイルに機能群をまとめ、何かのスコープに限定されない形で表現することは可能ですし、C言語であればそうするでしょうが、C++ ならば、staticクラスを用いて「クラス名::メンバ」のようにスコープを限定することができます。

例えば、ファイルをコピーしたり削除したりするような機能は、それぞれ1つの関数で完結できるため、メンバ変数を持つ必要もありません。そのため、静的でない通常のクラスで表現する意味があまりありません。そこで、staticクラスを使って、次のように定義できます。

class FileSystem {
public:
    static void Copy(const char* src, const char* dest);
    static void Delete(const char* path);

private:
    FileSystem();
};

しかし、スコープを限定するという目的ならば、名前空間を使う方が素直であるとも言えます。実際、C++ でstaticクラスを表現する場合は、クラスではなく、名前空間を使うのも良い方法です。これなら、コンストラクタを「非公開」にするという、ある種の小細工も不要です。

namespace FileSystem {
    void Copy(const char* src, const char* dest);
    void Delete(const char* path);
}

クラスで表現しても、名前空間で表現しても、それぞれの関数の使い方が変わらないことに注目しましょう。

FileSystem::Copy("test.txt", "test_copy.txt");
FileSystem::Delete("test.txt");

名前空間の方なら、using宣言、usingディレクティブによって、「FileSystem::」の部分を省略できます。


練習問題

問題① staticメンバ変数が、オブジェクトの大きさに影響を与えないことを確認して下さい。

問題② 次のプログラムの実行結果を答えて下さい。

// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
    MyClass(int value);
    ~MyClass();

    inline int GetValue() const
    {
        return mValue;
    }

    static inline int GetObjectCount()
    {
        return msObjectCount;
    }

private:
    int mValue;
    static int msObjectCount;
};

#endif
// MyClass.cpp

#include "MyClass.h"

int MyClass::msObjectCount = 0;

MyClass::MyClass(int value) :
    mValue(value)
{
    ++msObjectCount;
}

MyClass::~MyClass()
{
    --msObjectCount;
}
// main.cpp

#include "MyClass.h"

MyClass* func(int value)
{
    static MyClass c(value);

    MyClass* p = new MyClass(value);

    std::cout << MyClass::GetObjectCount() << " "
              << c.GetValue() << " "
              << p->GetValue() << " " << std::endl;

    return p;
}

int main()
{
    static const int LOOP_COUNT = 3;

    MyClass* c[LOOP_COUNT];

    for (int i = 0; i < LOOP_COUNT; ++i) {
        c[i] = func(i);
    }

    for (int i = 0; i < LOOP_COUNT; ++i) {
        delete c[i];
    }
}

問題③ あるクラスがインスタンス化されたときに、各オブジェクトに重複の無い個別の ID (整数値) を割り当てたいとします。staticメンバ変数を利用して、このような割り当てを自動化できるようにクラスを設計して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/7/23 章のタイトルを変更(「static の使い方」-->「static (静的)」)
非ローカルの静的変数の初期化順序」の項を追加。
静的ローカル変数」の項で、メモリ確保と初期化のタイミングに関する記述を修正。

'2018/7/21 「staticグローバル変数」「staticグローバル関数」の項を統合し、「名前空間スコープの場合」に改めた。

'2018/7/13 サイト全体で表記を統一(「静的メンバ」-->「staticメンバ」、「静的クラス」-->「staticクラス」、「静的グローバル**」-->「static**」)

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2014/5/24 新規作成。



前の章へ(第14章 動的なオブジェクトの生成)

次の章へ(第16章 コピー操作と参照)

C++編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS