C++ static 修飾子 入門
Copyright (C) 2014 by Nobuhide Tsuda

 

※ イラスト提供:かわいいフリー素材集「いらすとや」様

static 修飾子とは

「static」は静的という意味で「dynamic(動的)」の対義語である。
下記の様に変数宣言または関数宣言時に「static」を付加することで、付加された変数または関数が静的であることを宣言する。

    static int var;
    static int func() { return 0; }

ここで言う「静的」の具体的な意味は変数、関数の種類によってかなり異なる。なので、static は初級者にとってマスターしづらいもののひとつではないかと思う。

本稿では、それぞれの種類について具体的に解説し、お約束の演習問題も用意している。 理解しづらい概念も、手を動かして演習問題を解いていけば誰でもマスターできるものなので、ちゃんと演習問題をクリアーしてほしい。

static 関数

下記のように、同じプロジェクトに含まれる複数のファイルに、同じ関数名・引数の関数があると、リンク時にエラーになる。

"file1.cpp":

int func()
{
    return 0;
}

"file2.cpp":

int func()
{
    return 1;
}

このエラーを避けるには、どちらかの関数の前に「static」を付ける。

"file1.cpp":

static int func()
{
    return 0;
}

このように記述しておくと、int func() はそのファイル内だけのローカルな関数となり、他のファイルの同一関数名・引数の関数とバッティングすることはなくなる。

が、個人的には同じ関数名・引数のものを複数定義することはお薦めしない。
コール部分を読んでいるときは、それが static なのかどうかがわからないので、どれが呼ばれているのかがすぐに解らない。
なにより、同じ関数名・引数の関数が複数あるのは混乱の元だ。

適切な関数名を付け、同一関数名にはしないことを強くお薦めする
ただし、下記の例の様に引数が異なっていれば同一名称の関数を定義してもOKだ。

int add(int a, int b) {
    return a + b;
}
int add(int a, int b, int c) {
    return a + b + c;
}

演習問題:(解答例は省略)

  1. 複数のファイルで、同一関数名・引数の関数を定義し、リンクエラーが出ることを確認しなさい。
  2. どちらかに static を付け、エラーが無くなることを確認しなさい

static グローバル変数

関数と同様に、グローバル変数も、下記の様に複数のファイルで同一名称のものが定義されていると、リンク時にエラーとなる。

"file1.cpp":

int g_var = 0;

"file2.cpp":

int g_var = 1;

このエラーを無くすには、関数の場合と同様に、どちらかの宣言の前に static を付けるとよい。

"file2.cpp":

static int g_var = 1;

が、関数の場合と同様に、そもそも複数のグローバル変数に同じ変数名を付ける意味は無いだろうから、そのような変数を宣言しないことを強く薦める。

演習問題:(解答例は省略)

  1. 複数のファイルで、同一型、名称のグローバル変数を定義し、リンクエラーが出ることを確認しなさい。
  2. どちらかに static を付け、エラーが無くなることを確認しなさい
  3. 複数のファイルで、同一名称だが型の異なるグローバル変数を定義し、リンクエラーが出るかどうかを確認しなさい。

static ローカル変数

通常関数定義、グローバル変数宣言に static 修飾子を付けると、その識別子がそれを記述したファイルでのみ有効になることを前章までに説明した。
つまり、static は識別子の有効範囲を指定するものだったわけだ。
が、ローカル変数宣言に static 修飾子を付けた場合の static の意味は前章までとは異なる。

ローカル変数識別子の有効範囲(スコープ)は、そもそも当該ブロック内だけである。
static を付けてもその点はなんら変化しない。

    {
        static int a = 0;      // static なローカル変数
        int b = 0;              // 通常ローカル変数
        .....
    }  // ブロック終了
    a;     // スコープを抜けているので参照不可
    b;     // スコープを抜けているので参照不可

では、ローカル変数に static を付けた効果は何かというと、「スコープを抜けても変数の値がそのまま保持される」という点である。

そのスコープに入るのが一度っきりであれば、その違いに意味・意義は無いが、関数やループ内ブロックであれば何度もスコープに出入りすることがあるので違いが出てくる。

int func()
{
    int a = 0;
    return ++a;
}

例えば、上記関数は常に 1 を返す。なぜなら関数を抜けると a の情報は破棄され、再度呼ばれた時に 0 に再初期化されるからだ。

int func()
{
    staic int a = 0;
    return ++a;
}

上記のように staic を付けると、関数を抜けても a の情報はそのまま残り、再度呼ばれたときも再初期化は行われない。 (※ 何故再初期化を行わないかと言うと、再初期化をしてしまうと、a の情報を残しておいた意味が無くなるからだ)

関数を抜けても情報はそのまま残るが、スコープの範囲外から変数を参照することは出来ない。

つまり、static なローカル変数は永続的という意味でグローバル変数と同じで、識別子の有効範囲はローカル変数と同様にそのスコープ内だけということだ。

※ 細かいことを言うと、グローバル変数は main() が呼ばれる前に初期化されるが、static なローカル変数はそのスコープに最初に入ったときに初期化される。
したがって、一度も呼ばれない関数内の static 変数は一度も初期化されない。

static なローカル変数を使うと、関数が何度呼ばれたかをカウントするとかが実現出来る(先の int func() がまさにそう)。

ちなみに、下記のように、入れ子になったブロック内であっても static なローカル変数にすることが可能だぞ。 (※ これを何に利用するとよいのかは筆者にはわからないけどね)

    int sum = 0;
    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 5; ++k) {
            static int a = 0;
            sum += ++a;
        }
    }

演習問題:

  1. 関数の最初に static なローカル変数を宣言し、値を設定・更新しなさい。関数を複数回コールし、値が保持されていることを確認しなさい。
  2. int func()
    {
        static int a = 0;
        return ++a;
    }
    int main()
    {
        std::cout << func() << "\n";
        std::cout << func() << "\n";
        return 0;
    }
    
  3. static なクラスオブジェクトローカル変数を宣言し、そのスコープに入った時点でコンストラクタが呼ばれることを確認しなさい。
  4. class Hoge
    {
    public:
        Hoge()
        {
            _a = 1;         // ここにブレークポイントを設定するとよい
        }
        int _a;
    };
    int main()
    {
        ....
        static Hoge obj;
        .....
    }
    

static メンバ変数

前章までは C/C++ での話だったが、ここからは class がらみの話なので C++ だけの話だ。

C++ の class 宣言では、メンバ変数を宣言することが出来る。

class Hoge
{
    .....
private:
    int    m_a;         // int 型のメンバ変数
};

メンバ変数は、クラスから作られたオブジェクト(=インスタンス)が個々に保持する変数だ。オブジェクトが異なれば、異なる値を保持することが出来る。

グローバル、ローカル変数の場合と同じように、宣言の最初に static を付加することで、メンバ変数を static にすることが出来る。

class Hoge
{
    .....
private:
    static int    m_a;         // static な int 型のメンバ変数
};

この場合の static の機能はこれまで(ファイル内固有、値の永続化)とはまったく違う。
メンバ変数に static を付加すると、そのメンバ変数はオブジェクトが保持するのではなく、クラスが保持することになるのだ。

※ C/C++ が難しいと言われる理由のひとつは、識別子や記号の多義性にあるのではないかと考える。 文脈で意味が異なるのは初学者にとっては理解しづらいことこの上ない。

しかも、やっかいなことにこれまでの static 変数のように宣言時に初期値の指定が出来ないのだ。

class Hoge
{
    .....
private:
    static int    m_a = 123;         // コンパイルエラーとなる
};

ただし、static に加えて const を付加すると、初期化することが可能になる。これはクラス固有の変数ではなくクラス固有の定数となる。

では、どうやって初期化するかと言うと、グローバル変数のように、クラス外で初期化を行う。

int Hoge::m_a = 123;        //  初期化

結局、private で static な(非const)メンバ変数は、アクセスがクラスからに限られるグローバル変数と同じと考えるとよい。

これまでの例は、メンバ変数を private にし、クラス以外からのアクセスを不可にしていたが、public に置いて、クラス外からのアクセスを許可することも出来る。

class Hoge
{
    .....
public:
    static int    m_a;         // 外部からアクセス可能
};
int Hoge::m_a = 123;    //  初期化
int main()
{
    std::cout << Hoge::m_a << "\n";       // 123 が表示される
}

クラスのメンバ変数を static const を付けて宣言した場合は、クラス固有の定数となる。非const の場合と異なり、値を宣言時に指定し、初期化のための文を書く必要はない。

class Hoge {
    .....
public:
    static const int C = 314;      // クラス固有の定数宣言
};
//int Hoge::C = 314;      // 外部での初期化文はいらない

なお、クラス固有の定数を宣言する方法としては enum を用いる方法もある。

演習問題:

  1. メンバ関数から static なメンバ変数を参照・代入し、それがオブジェクト固有ではなく、クラス固有であることを確認しなさい。
  2. class Hoge {
    public:
        void setVal(int val) { m_val = val; }
        int    getVal() const { return m_val; }
    private:
        static int m_val;
    };
    int Hoge::m_val = 0;
    int main()
    {
        Hoge obj1, obj2;    // 2つのオブジェクトを生成
        cout << obj1.getVal() << "\n";
        cout << obj2.getVal() << "\n";
        obj1.setVal(1);
        cout << obj1.getVal() << "\n";
        cout << obj2.getVal() << "\n";
        obj2.setVal(2);
        cout << obj1.getVal() << "\n";
        cout << obj2.getVal() << "\n";
        return 0;
    }
    

static メンバ関数

メンバ変数と同様に、メンバ関数も static にすることが出来る。

class Hoge {
.....
public:
    static void foo();         // スタティックなメンバ関数
};

static なメンバ関数は、static なメンバ変数と同様に、オブジェクト固有ではなく、クラス固有なものとなる。
これはどういうことかと言うと、各オブジェクトが保持する通常のメンバ変数にはアクセス出来ないということだ。

メンバ変数にアクセスできなくて、何の意味があるのかと思われるかもしれないが、 メンバ変数にアクセス出来ない代わりに、オブジェクトを生成しなくてもコール出来る、というメリットがあるのだ。

上記の例であれば、下記のように、クラス名::スタティックなメンバ関数(); とどこからでも呼ぶことが可能になる。

main() {
    Hoge::foo();      //  スタティックなメンバ関数をコール
}

また、static メンバ関数内で、そのクラスオブジェクトを生成し、それのメンバ変数にアクセスすることは可能。
その具体例は、次章を参照されたし。

シングルトン

よく出てくる例であるが、スタティックなメンバ関数を使ってシングルトン(アプリケーションでインスタンス数をひとつだけに制限したもの)を実装することが出来る。

class Singleton {
private:
    Singleton() {};
    ~Singleton() {};
    Singleton(const Singleton &x) { };
    Singleton &operator=(const Singleton &) { return *this; };
public:
    static Singleton &getInstance();    // シングルトンなオブジェクトを返す static なメンバ関数
};
Singleton &Singleton::getInstance()
{
    static Singleton obj;        // スタティク変数として Singleton オブジェクトを生成
    return obj;                    //  それを返す
}
int main() {
    Singleton &s = Singleton::getInstance();       // シングルトンインスタンスをゲット
    .....
}

Singleton オブジェクトを勝手に生成されることがないように、(デフォルト)コンストラクタ、コピーコンストラクタ、代入演算子をプライベートにしておく。
シングルトンオブジェクトを生成するメンバ関数はスタティックとし、その定義中でスタティック変数を生成し、その参照を返す。

getInstance() の型は、Singleton への参照ではなくポインタにしてもよい。
ポインタにする場合は以下のように記述する。

class Singleton {
    .....
public:
    static Singleton *getInstance();    // シングルトンなオブジェクトへのポインタを返す static なメンバ関数
};
Singleton *Singleton::getInstance()
{
    static Singleton obj;        // スタティク変数として Singleton オブジェクトを生成
    return &obj;                    //  それへのポインタを返す
}
int main() {
    Singleton *s = Singleton::getInstance();       // シングルトンインスタンスへのポインタをゲット
    .....
}

上記のコードでは、シングルトンオブジェクトを getInstance() の static なローカル変数としていたが、クラスの static なメンバ変数として実装することも出来る。

class Singleton {
private:
    .....
public:
    static Singleton *getInstance();    // シングルトンなオブジェクトを返す static なメンバ関数
private:
    static Singleton *m_singleton;
};
Singleton *Singleton::m_singleton = nullptr;      // ヌルポインタで初期化しておく
Singleton *Singleton::getInstance()
{
    if( m_singleton == nullptr )         //  ヌルポインタだった場合は
        m_singleton = new Singleton();   //  シングルトンオブジェクトを生成
    return m_singleton;                    //  シングルトンポインタを返す
}

このような実装方法は冗長なのでいかがなものかと考える。素直に最初に示した実装方法を採用するのがよいだろう。

参考