静的メンバ | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、静的メンバを取り上げます。通常のデータメンバやメンバ関数は、クラスのオブジェクトを作ってから使用するものであり、言い換えるとオブジェクト単位に存在するものでした。一方ここで取り上げる静的メンバはクラスごとに1つだけ存在するというものです。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



クラスごとに1つだけ存在するメンバ

通常のデータメンバやメンバ関数は、クラスのオブジェクト単位で存在するものです。そのため、まずクラスのオブジェクト(たとえば obj という名前の変数)を作り、obj.f(); とか obj.m = 10; といったように、対象のオブジェクトの指定とともにアクセスします。同じクラスから複数のオブジェクトを作ったとすれば、それぞれのオブジェクトのデータメンバは、メモリ上で別々のところに存在していることになります。メンバ関数の場合は、関数のコード自体はオブジェクト全てで同じものですが、thisポインタが指すオブジェクトが決定できなければならないので、呼び出し時には対象のオブジェクトの指定が必要です。

一方、クラス単位で存在するデータメンバやメンバ関数を作ることもできます。そのようなメンバを、静的メンバ (static member) と呼びます。オブジェクトの存在とは無関係に存在することができるため、オブジェクトを作らずに使用できます。そのため、グローバル変数や、非メンバ関数と同じような存在のようでもありますが、静的メンバはたしかにクラスの一員ではあるという点で異なっています。同じクラス内からは自由に使え、クラスの外側からはクラス名による修飾が必要かつ、アクセス指定子の影響を受けます。

また、列挙型やほかのクラス型、using による型の別名を、クラス内で定義することができます。このような、メンバとして定義されている型を、型メンバ (type member) とかメンバ型 (member type) と呼ぶことがあります。このような型もまた、同じクラス内からは自由に使え、クラスの外側からはクラス名による修飾が必要かつ、アクセス指定子による影響を受けます。

静的データメンバ

データメンバを宣言するときに static指定子を付加すると、静的データメンバ (static data member) になります。静的データメンバはクラス単位で存在するデータメンバです。

class クラス名 {
    static 型名 識別子;
};

これは宣言であり、定義をクラス定義の外側に記述する必要があります。このとき、どのクラスに属する静的データメンバなのかが分かるように「クラス名::」による修飾が必要です。定義のほうには static指定子を付けません。

class C {
    static int ms_object_count;  // 宣言
};

int C::ms_object_count {0};  // 定義

クラス定義をヘッダファイルに記述しているとすれば、静的データメンバの宣言はヘッダファイル側に、定義はソースファイル側に記述することになります。

なお、新C++編では、静的データメンバの名前の先頭に ms を付加します。ただし、const や constexpr を使う場合や、public である場合は除くものとします。

【C++17】インライン変数を用いることで、変数の定義をヘッダファイル側に記述できるようになりました。1

例外的に、const にする場合には、定数式の初期化子による定義が可能です。この場合はクラス定義の外側に定義を書く必要はありません。

class Canvas {
    static const unsigned int default_width {320};  // これだけで OK
    static const unsigned int default_height;       // これも OK だが定義は別途必要
};

const unsigned int Canvas::default_height {240};    // 定義

さらに、constexpr を使う場合には、宣言時に初期化することが必須となります。

class Canvas {
    static constexpr unsigned int default_width {320};
    static constexpr unsigned int default_height {240};
};

静的データメンバをメンバ初期化子(「コンストラクタ」のページを参照)で初期化することはできません。コンストラクタの本体で代入を行うことはできますが、そのような方法で初期化しないように注意してください。静的データメンバは、そのクラスのオブジェクトの個数とは無関係に存在するものです。一方、コンストラクタはオブジェクトを1つ生成するたびに呼び出されます。そのため、コンストラクタで静的データメンバへ代入すると、オブジェクトが生成されるたびに値を上書きしてしまいます

次のプログラムでは、クラスのオブジェクトが作られた回数をカウントするために、静的データメンバを用いています。

#include <iostream>

class C {
public:
    C();

private:
    static int ms_object_count;
};

C::C()
{
    ++ms_object_count;
    std::cout << ms_object_count << "\n";
}

int C::ms_object_count {0};

int main()
{
    C c {};
    C c_array[5] {};
}

実行結果:

1
2
3
4
5
6

静的データメンバは、静的ストレージ期間(「メモリとオブジェクト」のページを参照)を持ちます。そのため、プログラムの実行開始時点で存在しており、プログラムが終了するまで生き続けています。

すでに「メモリとオブジェクト」のページで取り上げていますが、静的ストレージ期間を持った変数(静的変数)を複数定義した場合に、それらが初期化される順序に注意しなくてはなりません。重要なポイントは以下の2点です。

静的変数は最初にいったんゼロ初期化されるため、初期化が先に行われる保証がないほかの静的変数の値を使うと、‘0’ を使ってしまいます。

静的データメンバを、クラスに関係した定数を表現するために使うことができます。この項の冒頭で、Canvasクラスにデフォルトの大きさを定義する例を示しましたが、これはキャンバスを作成するときに大きさを指示するために利用できそうです。定数なので値を書き換えられる心配はありませんから、データメンバは private にするという原則を破って public にして構いません(クラス内でしか使わないのなら private であるべきです)。

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Canvas {
public:
    // コンストラクタ
    //
    // width: 横方向のピクセル数。省略時は default_width
    // height: 縦方向のピクセル数。省略時は default_height
    // color: 初期状態の色。省略時は白
    Canvas(unsigned int width = default_width, unsigned int height = default_height, Color color = {255, 255, 255})
    {
        // ...
    }

public:  // クラスの利用者側から使えるように公開する
    static constexpr unsigned int default_width {320};
    static constexpr unsigned int default_height {240};
};

クラスの外から静的データメンバにアクセスするときには、スコープ解決演算子(::)を使って、クラス名::静的データメンバ のように記述します。

Canvas basic_canvas {};  // すべてデフォルトの設定で作成
Canvas big_canvas {Canvas::default_width * 2, Canvas::default_height * 2};  // デフォルトの2倍の大きさで作成

あまり使われることはないですが、クラスのオブジェクトを指定して静的データメンバにアクセスすることも許されています。この場合は :: ではなく . を使います。オブジェクトを指すポインタを経由するのなら -> を使用します。

Canvas basic_canvas {};
Canvas big_canvas {Canvas::default_width * 2, Canvas::default_height * 2};

std::cout << basic_canvas.default_width << ", " << basic_canvas.default_height << "\n";
std::cout << big_canvas.default_width << ", " << big_canvas.default_height << "\n";

この記法は、オブジェクト単位で存在するデータメンバを使っているように見えてしまうので、コードを読む人が誤解する可能性があります。このコードから出力される値は同じものです。

静的メンバ関数

メンバ関数を宣言するときに static指定子を付加すると、静的メンバ関数 (static member function) になります。

class クラス名 {
    static 戻り値の型 メンバ関数名(仮引数);    
};

定義はクラス定義の外と中のいずれにでも書けます。外に書く場合は、関数名をクラス名で修飾するうえ、static指定子は付けません。中に書く場合は、静的でないメンバ関数のときと同じく、インライン関数になります(「クラス」のページを参照)。

class C {
public:
    static int f1()   // static inline int f1() としたことになる
    {
        // ...
    }

    static void f2(int x); // 宣言
};

// 定義
void C::f2(int x)
{
    // ...
}

静的メンバ関数はオブジェクトではなくクラスに結びついた存在なので、呼び出しの際には、クラス名とスコープ解決演算子を使います。

C::f1();
C::f2(100);

静的メンバ関数から、同じクラスのほかの静的メンバ関数を呼び出すときは、クラス名とスコープ解決演算子は不要です。

void C::f2(int x)
{
    f1();
    return x;
}

静的データメンバのときと同じで、オブジェクト名を使った呼び出しも可能ではありますが、混乱を招く恐れがあります。

C c {};
c.f1();
c.f2(100);

この書き方ではオブジェクトが対象になっているように見えますが、静的メンバ関数には呼び出しの対象になったオブジェクトは存在しないのですから、静的メンバ関数の本体で thisポインタを使うことはできません。thisポインタが使えないので、静的メンバ関数内から、静的でないデータメンバや、静的でないメンバ関数を使うことはできません。書き換える対象のオブジェクトがないのですから、constメンバ関数にすることもできません。

【上級】virtual や volatile を付加することもできません。

静的メンバ関数は、メンバ関数でない通常の関数とほとんど同じ存在であるともいえますが、アクセス指定子による制限をかけられる利点があります。

また、静的でないメンバ関数との比較では、呼び出し元から thisポインタを渡すコストが削減できるメリットがあります。thisポインタが不要な場合、つまり静的でないメンバを使う必要がない関数は、静的メンバ関数にすることで実行効率の向上が見込めます。たとえば「ファイルシステム」のページで作成した .bmpファイルの読み書きを行う関数は、引数で渡した情報だけを使って処理を行うことができるため、データメンバが不要であり、thisポインタも不要ということになります。

//bmp.cpp
#include "bmp.h"
#include <cassert>
#include <fstream>
#include "color.h"

bool Bmp::save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels)
{
    std::ofstream ofs(path, std::ios_base::out | std::ios_base::binary);
    if (!ofs) {
        return false;
    }

    // ----- ファイルヘッダ部 -----
    // Windows API の BITMAPFILEHEADER構造体にあたる。
    // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader

    // ファイルタイプ
    // 必ず 0x4d42 ("BM" のこと)
    constexpr std::uint16_t file_type {0x4d42};
    ofs.write(reinterpret_cast<const char*>(&file_type), sizeof(file_type));

    // ファイルサイズ
    // 縦横のピクセル数 * 1ピクセル当たりのバイト数(PaintScript では 4バイト固定) + ヘッダ部のバイト数。
    // ヘッダ部の大きさは、sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) より。
    const std::uint32_t file_size {width * height * 4 + 54};
    ofs.write(reinterpret_cast<const char*>(&file_size), sizeof(file_size));

    // 予約領域
    const std::uint32_t reserved {0};
    ofs.write(reinterpret_cast<const char*>(&reserved), sizeof(reserved));

    // ファイル先頭から、ピクセル情報までの距離
    const std::uint32_t offset_to_pixels {54};
    ofs.write(reinterpret_cast<const char*>(&offset_to_pixels), sizeof(offset_to_pixels));


    // ----- ビットマップ情報ヘッダ部 -----
    // Windows API の _BITMAPINFOHEADER構造体にあたる。
    // https://learn.microsoft.com/ja-jp/windows/win32/wmdm/-bitmapinfoheader

    // ビットマップ情報ヘッダ部のサイズ
    const std::uint32_t bitmap_info_header_size {40};
    ofs.write(reinterpret_cast<const char*>(&bitmap_info_header_size), sizeof(bitmap_info_header_size));

    // 横方向のピクセル数
    const std::int32_t w {static_cast<std::int32_t>(width)};
    ofs.write(reinterpret_cast<const char*>(&w), sizeof(w));

    // 縦方向のピクセル数
    const std::int32_t h {static_cast<std::int32_t>(height)};
    ofs.write(reinterpret_cast<const char*>(&h), sizeof(h));

    // プレーン数。必ず 1
    const std::uint16_t planes {1};
    ofs.write(reinterpret_cast<const char*>(&planes), sizeof(planes));

    // 1ピクセル当たりのビット数。PaintScript では 24 に固定
    const std::uint16_t bit_count {24};
    ofs.write(reinterpret_cast<const char*>(&bit_count), sizeof(bit_count));

    // 圧縮形式。無圧縮は 0
    const std::uint32_t compression {0};
    ofs.write(reinterpret_cast<const char*>(&compression), sizeof(compression));

    // 画像サイズ。無圧縮であれば 0 で構わない
    const std::uint32_t image_size {0};
    ofs.write(reinterpret_cast<const char*>(&image_size), sizeof(image_size));

    // メートル当たりの横方向のピクセル数の指示。不要なら 0 にできる
    const std::int32_t x_pixels_per_meter {0};
    ofs.write(reinterpret_cast<const char*>(&x_pixels_per_meter), sizeof(x_pixels_per_meter));

    // メートル当たりの縦方向のピクセル数の指示。不要なら 0 にできる
    const std::int32_t y_pixels_per_meter {0};
    ofs.write(reinterpret_cast<const char*>(&y_pixels_per_meter), sizeof(y_pixels_per_meter));

    // カラーテーブル内の色のうち、実際に使用している個数。パレット形式でなければ無関係
    const std::uint32_t clr_used {0};
    ofs.write(reinterpret_cast<const char*>(&clr_used), sizeof(clr_used));

    // カラーテーブル内の色のうち、重要色である色の個数。パレット形式でなければ無関係
    const std::uint32_t clr_important {0};
    ofs.write(reinterpret_cast<const char*>(&clr_important), sizeof(clr_important));


    // ----- ピクセル情報 -----
    // 各ピクセルは RGB の各8bit である想定。
    for (std::int32_t y {h - 1}; y >= 0; --y) {
        for (std::int32_t x {0}; x < w; ++x) {
            const Color pixel {pixels.at(y).at(x)};
            ofs.write(reinterpret_cast<const char*>(&pixel.blue), sizeof(pixel.blue));
            ofs.write(reinterpret_cast<const char*>(&pixel.green), sizeof(pixel.green));
            ofs.write(reinterpret_cast<const char*>(&pixel.red), sizeof(pixel.red));
        }
    }

    return true;
}

bool Bmp::load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels)
{
    assert(width);
    assert(height);
    assert(pixels);

    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (!ifs) {
        return false;
    }

    // 不要なところを読み飛ばす
    ifs.seekg(18, std::ios_base::beg);

    // 横方向のピクセル数
    std::int32_t w {};
    ifs.read(reinterpret_cast<char*>(&w), sizeof(w));
    if (w < 1) {
        return false;
    }
    *width = static_cast<unsigned int>(w);
    
    // 縦方向のピクセル数
    std::int32_t h {};
    ifs.read(reinterpret_cast<char*>(&h), sizeof(h));
    if (h < 1) {
        return false;
    }
    *height = static_cast<unsigned int>(h);

    // 不要なところを読み飛ばす
    ifs.seekg(28, std::ios_base::cur);

    // ピクセル情報
    // vector は、ビットマップの大きさに合わせて resize する。
    pixels->resize(h);
    for (auto& row : *pixels) {
        row.resize(w);
    }
    for (std::int32_t y {h - 1}; y >= 0; --y) {
        for (std::int32_t x {0}; x < w; ++x) {
            std::uint8_t b {};
            ifs.read(reinterpret_cast<char*>(&b), sizeof(b));
            std::uint8_t g {};
            ifs.read(reinterpret_cast<char*>(&g), sizeof(g));
            std::uint8_t r {};
            ifs.read(reinterpret_cast<char*>(&r), sizeof(r));

            pixels->at(y).at(x) = Color{r, g, b};
        }
    }

    return true;
}
//bmp.h
#ifndef BMP_H_INCLUDED
#define BMP_H_INCLUDED

#include <string>
#include <vector>

struct Color;

class Bmp {
public:
    // .bmpファイルに書き出す
    //
    // path: ファイルパス
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    // pixels: ピクセル情報
    // 戻り値: 成否
    static bool save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels);

    // .bmpファイルを読み込む
    //
    // path: ファイルパス
    // width: 横方向のピクセル数を受け取るポインタ。ヌルポインタ不可
    // height: 縦方向のピクセル数を受け取るポインタ。ヌルポインタ不可
    // pixels: ピクセル情報を受け取るポインタ。ヌルポインタ不可
    // 戻り値: 成否
    static bool load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels);
};

#endif
// color.h
#ifndef COLOR_H_INCLUDED
#define COLOR_H_INCLUDED

// 色
struct Color {
    unsigned char  red;         // 赤成分
    unsigned char  green;       // 緑成分
    unsigned char  blue;        // 青成分
};

#endif
//main.cpp
#include <cstdlib>
#include <iostream>
#include "bmp.h"
#include "color.h"

int main()
{
    unsigned int width {};
    unsigned int height {};
    std::vector<std::vector<Color>> pixels {};
    if (!Bmp::load("test.bmp", &width, &height, &pixels)) {
        std::cerr << "load error.\n";
        std::quick_exit(EXIT_FAILURE);
    }

    if (!Bmp::save("result.bmp", width, height, pixels)) {
        std::cerr << "save error.\n";
        std::quick_exit(EXIT_FAILURE);
    }
}

なお、静的メンバ関数には対象のオブジェクトがないので、ポインタで指し示すときにはメンバ関数ポインタ(「クラス」のページを参照)ではなく、通常の関数ポインタ(「関数ポインタとラムダ式」のページを参照)を使います。

#include <iostream>

class C {
public:
    static void f(int v)
    {
        std::cout << v << "\n";
    }
};

int main()
{
    using func_ptr_t = void (*)(int);

    func_ptr_t pf = C::f;
    pf(123);
}

実行結果:

123

型メンバ

クラス定義内には、型の定義を記述することもできます。たとえば、using や typedef による型の別名や、列挙型、構造体やクラスの定義を記述できます。このようなメンバを、型メンバ (type member) とかメンバ型 (member type) と呼ぶことがあります。

クラス定義の内側に定義されたクラス(構造体)は、入れ子クラスと呼ばれます。これについては改めて取り上げます

型メンバも、クラスのオブジェクトを作らずに使用できます。クラス内から使用するときには、ごく普通にその名前を使えばいいだけですが、クラスの外から使用するときには「クラス名::」による修飾が必要です。もちろん、アクセス指定子の影響も受けます。

class C {
public:
    class enum E {  // public なので、クラスの外側からでもアクセスできる
        e1,
        e2,
    };

private:
    using T = E;  // E は同じクラスのメンバなので、:: は不要
};

C::E e {};  // E は C の型メンバであり、外側からアクセスするには :: が必要

入れ子クラス

クラス定義内で定義されたクラスは、入れ子クラス (nested class) と呼ばれます。

もちろん struct を使っても同様です。

class Outer {
public:
    explicit Outer(int value)
    {
        mInner.print(value);
    }

private:
    class Inner {
    public:
        void print(int value)
        {
            std::cout << value << "\n";
        }
    };

    Inner mInner;
};

入れ子クラスも通常のクラスのルールと基本的には同じであり、静的メンバでないメンバは、オブジェクトが作られて初めて存在します。何となく、外側のクラスのメンバのようにみえたり、外側のクラスのオブジェクトを作った時点で、入れ子クラスのオブジェクトも作られていそうに思えたりして、直接入れ子クラスのメンバを使おうとするかもしれませんが、それはできません。

class Outer {
public:
    explicit Outer(int value)
    {
        // エラー。value1、value2 は存在しない。
        // 入れ子クラスである Data のオブジェクトが必要。
        value1 = value;
        value2 = value * 2;
    }

    struct Data {
        int value1;
        int value2;
    };
};

静的メンバであればアクセスできます。入れ子クラスの静的データメンバの定義を記述するときには、Outer::Data::value1 のように、スコープごとに :: を重ねていきます。

class Outer {
public:
    explicit Outer(int value)
    {
        // OK
        Data::value1 = value;
        Data::value2 = value * 2;
    }

private:
    struct Data {
        static int value1;
        static int value2;
    };
};

// 静的データメンバの定義
int Outer::Data::value1;
int Outer::Data::value2;

また、変な感じがするかもしれませんが、外側のクラスのオブジェクトがなくても、入れ子クラスのオブジェクトだけが存在することは可能です。

#include <iostream>

class Outer {
public:
    Outer()
    {
        std::cout << "Outer()\n";
    }

    class Inner {
    public:
        Inner()
        {
            std::cout << "Inner()\n";
        }
    };
};

int main()
{
    Outer::Inner in {};  // OK. Outer::Inner のオブジェクトを作ったが、Outer のオブジェクトは作られていない。
}

実行結果:

Inner()

入れ子クラスのスコープは、外側のクラスのスコープと一致します。そのため、入れ子クラスのメンバ関数から、外側のクラスのメンバが使えます。しかし、何度も繰り返している通り、静的でないメンバを使うには対象のオブジェクトの存在がなければなりません。

class Outer {
public:
    explicit Outer(int value) : mValue {value}
    {
    }

private:
    class Inner {
    public:
        void f2()
        {
            f1();     // OK. Outer::f1 のことであり、これは静的メンバなので問題ない
            mValue++; // エラー。Outer::mValue を見つけるが、Outer のオブジェクトがない
        }
    };

    static void f1()
    {
        std::cout << "OK.\n";
    }

    int mValue;
};

静的メンバだけで構成されるクラス

まれに、クラスのすべてのメンバが静的メンバになるときがあります。たとえば、前に挙げた Bmpクラスはすべて静的メンバです。静的メンバだけしか存在しないクラスは、オブジェクトを生成することに意味がありません。生成してしまうことは無駄で無意味なことですし、オブジェクトごとに異なる情報を持っているかのように誤解させることにもなります。

Bmp bmp1 {};  // OK だが不要
Bmp bmp2 {};  // OK だが不要。bmp1 とは別々に情報を持っているかのようにもみえる

// 実際にはこうやって呼び出せばいいので、オブジェクトは不要である。
Bmp::save(/**/);
Bmp::load(/**/);

プログラミング言語によってはこのようなクラスを、静的クラス (static class) と呼び、自動的にオブジェクトの生成を禁止します。C++ にはそのようなものは用意されていませんが、代わりの方法で実現します。代表的な方法を2つ挙げます。

  1. クラスを使うのをやめて、名前空間で実現する
  2. コンストラクタを削除する(=delete を使う)

名前空間で代用する

1つ目の方法は、クラスを使わなければオブジェクトを作る行為自体がなくなるという発想で、名前空間に変えてしまうというものです。

namespace Bmp {
    // .bmpファイルに書き出す
    bool save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels);

    // .bmpファイルを読み込む
    bool load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels);
}

// Bmp は型ではないので、以下は不可
Bmp bmp {};

// 以下のように呼び出せる
Bmp::save(/**/);
Bmp::load(/**/);

関数を呼び出すときの記述が、静的メンバ関数を呼び出すときのそれとまったく同じになるため、クラスだったときと同じ感覚で使用できます。

名前空間なので usingディレクティブ(「スコープと名前空間」のページを参照)や using宣言(「スコープと名前空間」のページを参照)が使えることには注意が必要です。

using namespace Bmp;

// 分かりづらくなってしまうかもしれない
save(/**/);

また、private なメンバに相当するものが必要な場合は、ヘッダファイルに宣言を公開せず、ソースファイルの側だけコードを隠すことで実現できます。

コンストラクタを削除する (=delete)

2つ目の方法は、コンストラクタを削除してしまうというものです。削除といっても、普通にソースコード上から消すだけでは、コンパイラがコンストラクタを自動生成してしまいます。そこで明示的に関数を削除する =delete を使用します。

=delete」は、関数の定義を明示的に削除する機能で、記法は次のとおりです。

class クラス名 {
    関数宣言 = delete;
};

良く似た =default はコンストラクタなどの特殊なメンバ関数に対してしか使えませんが(「コンストラクタ」のページを参照)、=delete はほかのメンバ関数や、通常の関数にでも使用できます。オーバーロードされているのなら、その一部の関数に対してだけ指定することも可能です。

関数宣言 = delete;

=delete が付加された記述を、削除された定義 (deleted definition) と呼び、その関数のことを、削除された関数 (deleted function) と呼びます3

削除された関数を使おうとする行為はコンパイルエラーとなるので、次のようにコンストラクタを削除すれば、オブジェクトの生成を禁止できます。

class Bmp {
public:
    Bmp() = delete;  // コンストラクタの定義を削除

    // .bmpファイルに書き出す
    static bool save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels);

    // .bmpファイルを読み込む
    static bool load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels);
};

// コンストラクタが削除されているため、コンパイルエラー
Bmp bmp {};

// 以下のように呼び出せる
Bmp::save(/**/);
Bmp::load(/**/);

削除された関数のアクセス指定子は public にしておくと、エラーの理由が伝わりやすくなることがあります。4コンパイラは、呼び出そうとしているメンバ関数が削除されているかどうかよりも先に、アクセスが可能かどうかを調べるため、private になっていると「その関数は非公開でありアクセスできない」という趣旨のコンパイルエラーを出します。一方 public にしておくと「その関数は削除されている」という内容のエラーを出せます。=delete を使っているのですから、利用者に伝えるべきことは「削除された関数である」ということなので、public にしておいたほうが、エラーになった意味が伝わりやすいといえます。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のプログラムの中から、エラーになるところをすべて挙げてください。

#include <iostream>

class C {
public:
    explicit C(int v) : m_v(v)
    {
        ms_v += v;
    }

    void f()
    {
        std::cout << "called f()\n"
                  << m_v << "\n"
                  << ms_v << "\n";
    }

    static void sf()
    {
        std::cout << "called sf()\n"
                  << m_v << "\n"
                  << ms_v << "\n";
    }

private:
    int         m_v;
    static int  ms_v;
};

int C::ms_v {100};

int main()
{
    C c(5);
    c.f();
    C::sf();
}

解答・解説

問題2 (確認★)

静的データメンバの存在が、オブジェクトの大きさに影響を与えないことを確認してください。

解答・解説

問題3 (基本★★)

あるクラスのオブジェクトが作られるたびに、それぞれのオブジェクトに重複がない個別の ID (整数値) を割り当てたいとします。静的データメンバを利用して、このような割り当てを自動化できるようにクラスを作成してください。

解答・解説

問題4 (応用★★)

生徒に関する情報を管理する Studentクラスを作成してください。ここでは情報として、生徒の氏名、3教科(国語、数学、英語)の得点を扱うものとします。得点をあつかう部分を入れ子クラスにして実現してみてください。

解答・解説

問題5 (応用★★★)

ペイントスクリプトのプログラムで使う、コマンドの実行を行う CommandExecutorクラスを作成してください。

ペイントスクリプトのコマンドは、たとえば次のような形式のものでした(「多次元配列」のページを参照)。

fill 255 255 255
pen 255 0 0
line 100 100 200 200
pen 0 0 255
line 200 100 100 200
save test.bmp

つまり、コマンド名と、それに応じたいくつかのパラメータが1セットです。CommandExecutorクラスは、このセットを1つ渡すと、その内容を解読して実行を行うクラスです。今回は、以下のコマンドを用意してください。

キャンバス側で必要な実装コードは、「クラス」のページの練習問題で作成しました。

コンストラクタに Canvas の参照を渡し、execメンバ関数にコマンドとパラメータが入った std::vector を渡すことでコマンドが実行されるものとします。つまり、以下のコードを含むようにしてください。これ以外の仕様は自由で構いません。

namespace paint_script {
    class CommandExecutor {
    public:
        using command_params_t = std::vector<std::string>;  // コマンドとパラメータの型

        // 結果
        enum class ExecResult {
            successd,       // 成功
            failed,         // 失敗
            not_found,      // コマンドが見つからない
            exit_program,   // 成功。プログラムを終了させる
        };

    public:
        // コンストラクタ
        //
        // canvas: キャンバスの参照
        explicit CommandExecutor(Canvas& canvas);

        // コマンドを実行する
        //
        // command_vec: コマンドとパラメータを含んだ配列
        // 戻り値: 結果
        ExecResult exec(const command_params_t& command_vec);
    };
}

解答・解説

問題6 (発展★★★)

問題5で作ったプログラムに helpコマンドを追加実装してください。このコマンドは、存在するすべてのコマンドの一覧と、それぞれの使い方を出力します。

今後、コマンドがさらに追加されたときに、helpコマンドの解説に登場しないミスを防ぐことを考えて実装しましょう。

解答・解説


解答・解説ページの先頭



更新履歴




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