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

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


static (静的) 🔗

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

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

静的ローカル変数 🔗

static指定子を付けて宣言されたローカル変数は、静的ローカル変数になります。

static 型名 変数名;
static 型名 変数名 = 初期化子;

静的ローカル変数は、静的記憶域期間を持ちます(C言語編第24章参照)。つまり、プログラム開始時から終了時までずっと存在し続けます。

void func(int num)
{
    static MyClass c;

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

静的ローカル変数がクラス型の場合に、関数を抜け出してもデストラクタは呼び出されません。デストラクタはプログラム終了直前に呼び出されます

静的ローカル変数は、まずゼロ初期化(第7章)されます。そして、静的ローカル変数の定義箇所が初めて実行されるときにあらためて初期化されます。コンストラクタが呼び出されるのはこのタイミングです。

事前のゼロ初期化が保証されているので、static int s; のように、クラスでない型に初期値を与えなかったとしても、確実に 0 になっています。

初期化のタイミングは、C言語とは異なっています。C言語の場合は、プログラム開始時に行われるため、次のようなコードはコンパイルエラーになりました。C++ では問題ありません。

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

staticメンバ変数 🔗

メンバ変数^にも static指定子を付けられます。このようなメンバ変数は、staticメンバ変数と呼ばれます。

class X {
    static 型名 メンバ変数名;  // 宣言
};

型名 X::メンバ変数名;  // 定義

staticメンバ変数は、クラス定義内に記述しただけでは定義したことになりません。実体となる定義を別のところに書く必要があります。なお、クラス定義と同じスコープに書かなければなりません。

staticメンバ変数は、オブジェクトを生成せずとも、プログラム開始時にすでに存在しています。そして、その時点でゼロ初期化(第7章)されます。

ゼロ初期化のあとにあらためて初期化を行えます。staticメンバ変数の型が、const な整数型か、const な enum型の場合に限っては、宣言と同時に初期化子を与えられます。

class X {
private:
    enum E { e1, e2 };

    static const int ci = 100;     // OK
    static const E ce = e1;        // OK
    static const double cf = 1.0;  // コンパイルエラー
    static int i = 100;            // コンパイルエラー
    static E e = e1;               // コンパイルエラー
};

コンパイルエラーになるパターンでは、定義の方で初期化子を与える必要があります。

class X {
private:
    enum E { e1, e2 };

    static const int ci = 100;     // OK
    static const E ce = e1;        // OK
    static const double cf;
    static int i;
    static E e;
};

const double X::cf = 1.0;     // OK
int X::i = 100;               // OK
X::E X::e = X::e1;            // OK

int main()
{
}

通常のメンバ変数が、オブジェクト1つごとに別個に存在しているのに対して、staticメンバ変数は、クラスに対して1つだけしか存在しません。オブジェクトが複数あっても、それぞれから、たった1つの変数を共有します。

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

// MyClass.h
#ifndef MYCLASS_H_INCLUDED
#define MYCLASS_H_INCLUDED

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

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

#endif
// MyClass.cpp

#include "MyClass.h"

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

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

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

クラスの外から、staticメンバ変数にアクセスするには、「クラス名::staticメンバ変数の名前」という形で、スコープ解決演算子を使って記述します

一般的ではありませんが、static でないメンバ変数と同様に、「オブジェクト名.staticメンバ変数の名前」といったアクセスも可能です。

もちろん、アクセス指定子の影響を受けるので、クラスの外からアクセスするのなら「公開」されていなければなりません。いつものように、メンバ変数は「非公開」であった方が良いです。ただし、const が付いている場合には「公開」しても問題ないですし、よく使う手法です。

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

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


staticメンバ関数 🔗

メンバ関数にも static指定子を付けられます。このようなメンバ関数は、staticメンバ関数と呼ばれます。

class X {
    static 戻り値の型 メンバ関数名(仮引数の並び);
};

戻り値の型 X::メンバ関数名(仮引数の並び)
{
}

staticメンバ関数を constメンバ関数にはできません。

staticメンバ関数をインライン関数にすることは可能です。デフォルト実引数も使えます。

【上級】関数テンプレートにすることも可能です(第33章

関数オーバーロードも可能ですが、staticメンバ関数と、static でないメンバ関数とのあいだで同じ名前を使うことはできません

staticメンバ変数と同様に、staticメンバ関数はクラスに属します。そのため、staticメンバ関数は、オブジェクトを生成せずに呼び出せます。呼び出しは、「クラス名::関数名()」の形になります。

オブジェクトから呼び出さないので、staticメンバ関数内では thisポインタを使うことはできません

「オブジェクト.staticメンバ関数()」の形で呼び出せないわけではありませんが一般的ではありません。いずれにしても、この形で呼ばないことがあるので、thisポインタを定義できません。

staticメンバ関数から、static でないメンバをアクセスできません。static でないメンバ関数から、staticメンバ関数を呼び出すことには問題ありません。

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

// MyClass.h
#ifndef MYCLASS_H_INCLUDED
#define MYCLASS_H_INCLUDED

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メンバ変数という専用のデータの置き場が使えることが特徴的です。

名前空間スコープの場合 🔗

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

内部結合にすることが目的であれば、無名名前空間を使う方法もあります第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;  // 内部結合
    }
}

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

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

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

たとえば、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();
}

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

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

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_INCLUDED
#define MYCLASS_H_INCLUDED

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メンバ変数を利用して、このような割り当てを自動化できるようにクラスを設計してください。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 第15章と内容を入れ替えた(以下の履歴は、旧第15章のもの)。
さらに全体的に見直し修正。

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

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

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

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

 新規作成。



前の章へ (第17章 コピー)

次の章へ (第19章 演算子オーバーロード)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る