スコープと名前空間 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、スコープと名前空間を取り上げます。いずれも、あまり意識しないようにしてきましたが、これまでのページの中ですでに登場している概念です。スコープの概念により、宣言した名前を使用できるコード上の範囲が決まります。これはメモリ上にあるかどうかとはまた別の話であり、正しく理解することが必要です。名前空間は、関連のあるいくつかの機能の集合に名前を付け、ほかのコードから分離することで、変数や関数などの名前が衝突することを防ぎます。

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



スコープ

宣言された変数名や関数名といった名前(識別子)には、それを使用できるソースコード上の範囲という考え方があって、スコープ (scope) という用語で表現します。名前が使用できる場合、「スコープ内にある」などといいます。

スコープは、名前が使える範囲を意味するものであって、変数(オブジェクト)がメモリ上に存在しているかどうかを意味するものではないことに注意してください。後者を決めるのはストレージ期間でした(「メモリとオブジェクト」のページを参照)。つまり、スコープ内にない名前でも、その実体(オブジェクト)はメモリ上に存在している場合があります。静的ローカル変数(「メモリとオブジェクト」のページを参照)が分かりやすい例で、スコープは宣言を行ったブロック内に限定されますが、そのブロックを抜け出したあとでもメモリ上に存在し続けています。

名前が宣言されて、使用可能になるソースコード上の場所のことを、宣言場所 (point of declaration) といいます。厳密なルールは意外と複雑ですが1、基本的に、宣言を行っている文が終わったところが宣言場所と考えて問題ありません。

スコープにはいくつかの分類があるので、ここからはそれぞれを順番に紹介していきます。

ブロックスコープ

ブロックの内側で宣言された名前や、for文などの初期化文で宣言した名前、関数定義やラムダ式の仮引数は、ブロックスコープ (block scope) を持ちます。

【上級】そのほか、関数tryブロックや、例外ハンドラで宣言した名前もブロックスコープを持ちます。

ブロックスコープは、宣言場所ではじまり、そのブロックの末尾で終了します。関数定義の仮引数の場合、宣言場所は関数本体の直前とみなされ、関数全体がスコープになります。

// a はこの位置で宣言されているとみなされる
void func(int a)
{
    int x {5};  // この文の直後から x が使える

    {
        int y {10};    // この文の直後から y が使える

        // y はここまで使える
    }

    // a と x はここまで使える
}

int main()
{
    int x {10};  // この文の直後から x が使える
    func(x);

    // x はここまで使える
}

入れ子になったブロックで同じ名前を宣言できます。この場合、内側のブロックで宣言した名前によって、外側で宣言された名前を隠すことになります。これを、名前の隠蔽 (name hiding) といいます。隠蔽された名前を使うことはできません。

#include <iostream>

int main()
{
    int x {10};

    {
        int x {20};  // 外側の x が隠蔽される

        x *= 2;      // 内側の x
        std::cout << x << "\n";  // 内側の x
    }

    std::cout << x << "\n";  // 外側の x
}

実行結果:

40
10

関数プロトタイプスコープ

関数宣言の中で宣言される名前(仮引数の名前)は、関数プロトタイプスコープ (function prototype scope) を持ちます。

関数プロトタイプスコープは、その宣言を書いたところではじまり、その関数宣言が終わるまでです。

たとえば、関数の戻り値の型を後ろに置く構文(「関数を作る」のページを参照)で宣言するときに、戻り値の型を仮引数の型を使って決定できます。

auto func(int x, double y) --> decltype(x * y);

decltype

decltype は、指定した式の最終的な型をコンパイル時に判断し、その結果でコードを置き換えます。

decltype()

コンパイル時に判断を行うだけなので、「式」は実際には実行されることはありません。上記のような戻り値型を記述する場面以外であっても、型名を必要とする箇所でなら多くの場面で使用できます。

decltype(a + b) x {};      // a + b の結果と同じ型の変数x を定義する
decltype(f()) y {};        // 関数f の戻り値と同じ型の変数y を定義する
using T = decltype(f());   // 関数f の戻り値型に T という別名を定義する

関数スコープ

ラベル名は、関数スコープ (function scope) を持ちます。

関数スコープは、宣言場所とは無関係に、その関数の本体すべてです。つまり、宣言場所よりも手前であっても使用できます。

ラベルはこれまで、switch文で使うものしか登場していませんが、ラベル名を宣言する次の構文があります。

ラベル名:

ラベルとは、文に対して付ける名前です。そのため「文」の存在が必要です。場合によっては空文を補わなければなりません。

void func()
{
label:    // エラー。続く文がない
}
void func()
{
label:    // OK
    ;     // 空文
}

goto文

宣言したラベル名は、goto文 (goto statement) で使います。

goto ラベル名;

実行中、goto文のところにやってくると、指定のラベル名が宣言されている箇所へ実行を移動します。

次のプログラムは、goto文を使って、for文のような動きを実現したコードです。

#include <iostream>

int main()
{
    int i {0};
loop:
    std::cout << i << "\n";
    ++i;
    if (i < 5) {
        goto loop;
    }
}

実行結果:

0
1
2
3
4

ラベル名と goto文を使ったこのコードは、非常に原始的な手法でループを実現したものです。これをより簡潔にしたものが for文によるループであるといえます。もちろん、for文で書いたほうが分かりやすく、間違いも起こりづらいので、goto文をこのような用途で使う必要はありません。実際のところ、goto文自体が使用を避けられる・避けた方がいいとされますが、例外的に深い入れ子から一気に一番外側にまで抜け出す用途では利用価値があります2

【上級】【C言語プログラマー】C言語では、goto文が有効活用できるもう1つの場面として、エラー処理の共通化がありますが(C言語編第17章)、C++ ではデストラクタを使ってより安全に書けるので、この場面での goto文の利用は良くないコードです。

たとえば、「while文」のページで作った「1~9 の整数同士を掛け合わせた結果の表を出力する。ただし、答えが 50以上になったらやめる」というプログラムは、goto文を使って次のように書けます。

#include <iostream>

int main()
{
    for (int i {1}; i <= 9; ++i) {
        for (int j {1}; j <= 9; ++j) {
            int ans {i * j};

            if (ans >= 50) {
                goto loop_after;  // 一気に抜け出す
            }
            std::cout << ans << " ";
        }
        std::cout << "\n";
    }

loop_after:
    std::cout << "\n";
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48

break文を使う方法で実装すると、2段階で抜け出していくようにしなければならず、余計な変数や if文が必要になり、コードが複雑化します。

逆に、ループの外側から内側へ飛び込むような goto文の使い方もできますが、ループの条件式のチェックや変数の初期化など、重要な処理を飛ばしてしまうため、非常に悪いコードです。

名前空間スコープ

名前空間の中で宣言された名前は、名前空間スコープ (namespace scope) を持ちます。

名前空間についてはこのあと詳細に解説しますが、namespaceキーワード{} によって形成される範囲のことをいいます。

名前空間スコープは、宣言場所以降で、その名前空間の内側です。ただし、同じ名前の名前空間を複数、別の場所に記述することが可能であるため、名前空間スコープは、離れた場所にある同名の名前空間の中にまで及びます。

namespace n {  // 名前空間n の開始

    int x {};  // 名前空間スコープを持つ

}  // 名前空間n の終わり

// ...

namespace n {  // 再び、名前空間n の開始

    void f()   // 名前空間スコープを持つ
    {
        x = 10;  // 名前空間n の x
    }

}  // 名前空間n の終わり

名前空間の内側に関数を定義できます。そのため、名前空間スコープの内側にブロックスコープが存在することになります。もし同じ名前の宣言を、名前空間スコープとブロックスコープの両方で行った場合、より内側にあるのはブロックスコープの側なので、名前空間スコープ側の名前が隠蔽されることになります。ただし、隠蔽された名前空間内の名前にはアクセスする方法があります(あとで取り上げます)。

グローバルスコープ

何にも囲まれていない場所についても、見えない名前空間に囲まれているという扱いを受けます。このような名前空間をグローバル名前空間 (global namespace) といい、このような場所で宣言された名前は、グローバルスコープ (global scope) を持ちます。また、グローバルスコープを持つ名前を、グローバル名 (global name) と呼びます。

グローバルスコープは、宣言場所から翻訳単位(「ヘッダファイル」のページを参照)の終わりまでです。

グローバル名前空間は、ほかの名前空間や関数によりも外側にあるので、内側で宣言した同じ名前によって隠蔽されます。ただし、隠蔽されたグローバル名にはアクセスする方法があります(あとで取り上げます)。

クラススコープ

クラス(構造体)定義の内側で宣言された名前は、クラススコープ (class scope) を持ちます。

クラスの機能についてはまだあまり解説していませんが、取り上げておきます。

クラス」のページで取り上げます。

クラススコープは特殊で、範囲を簡単に表現できません。クラス定義の内側に限っていえば、基本的には宣言場所からクラス定義の終わりまでですが、メンバ関数の本体は例外的で、前後関係とは無関係に名前を使用できます。メンバ関数の定義をクラス定義の外側に記述する場合でも同様です。また、メンバにアクセスするための演算子(.->::)を使うことで、クラス定義の外側からでも使用できます。

【上級】派生クラスからでも使用できます。

struct C {
    int f1()
    {
        return X;  // OK
    }
    int f2();

    int array[X];  // エラー。X の宣言が後ろにある
    static constexpr auto X = 100;
};

// メンバ関数の定義をクラス定義外に書く
int C::f2()
{
    return X;  // OK
}

int main()
{
    C c {};
    C* p {&c};
    c.f1();         // OK
    p->f1();        // OK
    auto x = C::X;  // OK
}

列挙スコープ

scoped enum の列挙名は、列挙スコープ (enumeration scope) を持ちます。

列挙スコープは、宣言場所から scoped enum の定義の終わりまでです。scoped enum の定義の外から列挙名を使うときには、スコープ解決演算子(::) を使う必要があります(「列挙型」のページを参照)。

【C++20】using enum宣言を行うことで、スコープ解決演算子を用いずに列挙名を使えるようになりました3

unscoped enum の場合は、定義したスコープに準じます。

テンプレート仮引数スコープ

テンプレート仮引数は、テンプレート仮引数スコープ (template parameter scope) を持ちます。

テンプレート仮引数スコープは、宣言場所から、テンプレート関数やテンプレートクラスの終わりまでです。

まだテンプレート関連については解説を行っていないので、ここではこれ以上取り上げないことにします。

名前空間

名前空間スコープのところで登場した名前空間 (namespace) について解説します。

名前空間は、コードの集まりに対して名前を付ける機能です。どんな「集まり」にするかは自由ですが、何かしら共通の意味をもった集まりを作って、それと分かる名前空間名を与えるようにします。

名前空間は、namespaceキーワードを使って次のように定義します。

namespace 名前空間名 {
}

あとで取り上げますが、名前空間名のない名前空間を定義することもできます。

{} で囲まれた範囲が、「名前空間名」で指定した名前が付けられた名前空間になります。{} の内側を、名前空間の本体 (namespace body) と呼び、変数や関数の宣言や定義、型の定義が行えます。名前空間の本体で宣言された変数や関数、型定義などをまとめて、名前空間メンバ (namespace member) と呼びます。

名前空間名に使用できる文字のルールは、変数名などと同様です(「定数式と識別子」のページを参照)。命名規則としては、先頭を大文字にしたキャメルケース(パスカルケース)にするものと、小文字と _ を使うスネークケースにするものが見受けられますが、新C++編ではスネークケースで統一します

Google C++ Style Guide ではスネークケースを使っています4。C++ Core Guidelines には言及がありませんが、コード例ではパスカルケースになっています。標準ライブラリ内ではスネークケースが使われており、C++ の規格書のコードはパスカルケースになっています。

名前空間は、ほかの名前空間の内側あるいは、どこにも囲まれていないところで定義できます。関数の本体の内側とか、構造体定義の内側のようなところには書けません。ほかの名前空間の内側に書いた場合、名前空間はネストしていることになります。

また、明示的に名前空間に囲まれておらず、関数や構造体などの定義の内側でもない場所も、名前空間であるとみなされ、グローバル名前空間 (global namespace) と呼ばれます。

名前空間の主目的は、名前の衝突を防ぐことにあります。同じ名前の変数や関数を宣言しても、所属する名前空間が異なれば、それはまったく別のものとして扱われます。

namespace n1 {
    int x {10};  // n1 の x
}

namespace n2 {
    int x {20};  // n2 の x
}

int x {30};      // グローバル名前空間の x

int main()
{
    int x {0};   // main関数のローカル変数x (グローバル名前空間の x を隠蔽)
}

同名の名前空間を複数定義することができ、ソースコード上では離れた位置にあっても、同じ名前空間とみなされます。

namespace n {
    int x {};
}

// ...

namespace n {  // 再び名前空間n の開始
    int y {};
    int f() {
        return x;
    }
}

x、y、f はいずれも名前空間n のメンバです。そのため、f関数から x を使えます。

このルールがあるため、ヘッダファイルに関数宣言を書き、ソースファイルにその関数の定義を書くことが可能になります。

// a.cpp
#include "a.h"

namespace n {
    int f() {
        return 10;
    }
}
// a.h
namespace n {
    int f();
}

また、すでに名前空間定義とメンバの宣言があるなら、名前空間名::名前 の構文を使って、以下のように記述することもできます。

namespace n {
    int x {10};
    int f();
}

int n::f() {
    return x;
}

ほかの名前空間にある名前を使うときには、スコープ解決演算子(::)を使って、名前空間名::メンバ名 のように表記します。グローバル名前空間の場合、名前空間名を省略して ::メンバ名 のように書きます。

スコープ解決演算子を用いずに名前を使おうとした場合、一番近いスコープで宣言されている名前のことになります。ほかのスコープにある名前を使いたければスコープ解決演算子を用いて書きます。スコープ解決演算子を使えば、隠蔽されている名前でも使えます。

#include <iostream>

namespace n1 {
    int x {10};
}

namespace n2 {
    int x {20};
    int f() {
        return n1::x + n2::x;  // n1::x + x でも可
    }
}

int x {30};

int main()
{
    int x {0};  // グローバル名前空間の x を隠蔽
    std::cout << x << "\n"
              << n1::x << "\n"
              << n2::x << "\n"
              << ::x << "\n";  // 隠蔽された名前でもアクセスできる
}

実行結果:

0
10
20
30

これまでのページで std:: という記述を頻繁に書いてきましたが、これは標準ライブラリ内の名前は std名前空間 (std namespace) の中で宣言されているためです。

【C言語プログラマー】C言語の標準ヘッダの多くが C++ でも使えますが、宣言される名前はグローバル名前空間にあります。対応する C++版標準ヘッダでは、std名前空間で宣言されています5。対応する C++版標準ヘッダとは、C言語版標準ヘッダの名前から .h を取り除き、先頭に C を付けた名称のものです(stdio.h に対して cstdio のように)。

名前空間のネスト

名前空間の内側に別の名前空間があるという状態が許されています。ネストした名前空間内の名前を使うときは、:: を繰り返し使って、名前空間名::名前空間名::名前 のようにします。

#include <iostream>

namespace mylib {
    namespace n1 {
        int x {10};
    }

    namespace n2 {
        int x {20};
        int f() {
            return n1::x;
        }
    }

    int f() {
        return n2::x;  // mylib の内側にいるので、mylib:: はなくてもいい
    }
}

// 関数定義を以下のように記述しても構わない
// int mylib::n2::f() {
//     return n1::x;
// }

int main()
{
    std::cout << mylib::n1::x << "\n"
              << mylib::n2::x << "\n"
              << mylib::n2::f() << "\n"
              << mylib::f() << "\n";
}

実行結果:

10
20
10
20

【C++17】ネストした名前空間を、namespace mylib::n1 {} のように、:: で区切ることで一度に定義できるようになりました6。ただし、インライン名前空間が混ざる場合、この記法では記述できません(C++20 で可能になりました7)。

インライン名前空間

inlineキーワードを付加して定義された名前空間は、インライン名前空間 (inline namespace) になります。

inline namespace 名前空間名 {
}

インライン名前空間で定義された名前は、名前空間名の指定をせずに使えます。

#include <iostream>

namespace mylib {
    inline namespace n {
        int x {10};
    }
}

int main()
{
    std::cout << mylib::x << "\n"
              << mylib::n::x << "\n";
}

実行結果:

10
10

n はインライン名前空間であるため、mylib::x だけで使用できます。従来どおり mylib::n::x と書くことも許されます。

インライン名前空間の使いどころとして、複数バージョンの機能を維持するコードの例があります。たとえば、バージョン1 のコードを v1名前空間に囲み、バージョン2 のコードを v2インライン名前空間に囲みます。すると、ユーザー側は名前空間名を指定することなく、新しいほうの機能を使用できます。

// main.cpp
#include "lib.h"

int main()
{
    // 最新バージョンを使うときは簡潔に
    lib::f();

    // あえてバージョンを指定
    lib::v2::f();

    // 理由があって旧バージョンを使う
    lib::v1::f();
}
// lib.cpp
#include "lib.h"
#include <iostream>

namespace lib {
    namespace v1 {
        void f()
        {
            std::cout << "v1::f()\n";
        }
    }

    // 新しいバージョンだけインライン名前空間
    inline namespace v2 {
        void f()
        {
            std::cout << "v2::f()\n";
        }
    }
}
// lib.h
#ifndef LIB_H_INCLUDED
#define LIB_H_INCLUDED

namespace lib {
    namespace v1 {
        void f();
    }

    // 新しいバージョンだけインライン名前空間
    inline namespace v2 {
        void f();
    }
}

#endif

実行結果:

v2::f()
v2::f()
v1::f()

今後、さらにバージョン3 を追加するときには、v2 から inline を外し、新たに追加する v3名前空間をインライン名前空間にします。これで、使用者側のコードを書き換えることなく、新しい機能が使えます(コンパイルしなおす必要はある)。

【C++20】C++17 では、ネストした名前空間にインライン名前空間が混ざる場合、namespace lib::v2 {} のような方法では記述できませんでした。C++20 では、namespace lib::inline v2 {} のように inlineキーワードを挟み込むことで記述できるようになっています7

無名名前空間

名前のない名前空間を定義できます。

namespace {
}

このような名前空間を、無名名前空間 (unnamed namespace) と呼びます。

無名名前空間で宣言された名前は、内部結合(「分割コンパイル」のページを参照)になります。無名名前空間には名前がないので、名前の衝突を避けるという目的はありません。一方、名前がある名前空間は、名前の衝突を避けることが目的であり、名前を内部結合にする効力はありません。

無名名前空間のメンバへのアクセスには、名前空間名:: は不要です。

// main.cpp
#include <iostream>
#include "another.h"

namespace {
    int x {10};  // 内部結合。another.cpp の x と衝突しない
}

int main()
{
    std::cout << x << "\n"
              << another::f() << "\n";
}
// another.cpp
#include "another.h"

namespace {
    int x {20};  // 内部結合。main.cpp の x と衝突しない
}

namespace another {
    int f()
    {
        return x;
    }
}
// another.h
#ifndef ANOTHER_H_INCLUDED
#define ANOTHER_H_INCLUDED

namespace another {
    int f();
}

#endif

実行結果:

10
20

名前空間エイリアス

名前空間に別名(名前空間エイリアス (namespace alias))を付けることが可能です。

namespace 別名 = 既存の名前;

与えられた別名は、既存の名前とまったく同じ意味で使えるようになります。

#include <iostream>

namespace my_cpp_library {
    int f() {
        return 10;
    }
}
namespace mylib = my_cpp_library;

int main()
{
    std::cout << mylib::f() << "\n"
              << my_cpp_library::f() << "\n";
}

実行結果:

10
10

usingディレクティブ

usingディレクティブ (using directive) を使うと、ほかの名前空間のメンバを使うときに付ける 名前空間名:: を省略できます。

usingディレクティブは以下のように記述します。

using namespace 名前空間名;

この記述は名前空間スコープかブロックスコープの中に記述します。この記述よりうしろのスコープ内では、名前空間名:: を省略できます。

#include <iostream>

namespace n {
    int x {10};
}

int main()
{
    using namespace n;

    // n:: を省略できる
    std::cout << x << "\n";
}

実行結果:

10

これまでのページで std:: を何度も何度も書いていましたが、usingディレクティブを使うことですべて省略できるようになります。これは非常に便利ですし、鬱陶しい記述が減ることで可読性が良くなる価値もあります。しかし、名前空間名を省略すると、名前の衝突を起こす可能性も生まれることになります(あえて 名前空間名:: を省略しないことで、曖昧さをなくすことは可能です)。

そこで、usingディレクティブを使うときは、できるだけ狭い範囲内にだけ効果を及ぼすようにするのが良いです。特に、ヘッダファイルのグローバルスコープに記述すると、インクルードを行ったすべてのソースファイルに効果を及ぼすため、避けるべきです8

usingディレクティブのもう少し正確な意味は、名前空間名:: のように名前空間を明示せずに使おうとした名前が、どこにある名前なのかをコンパイラが探すとき(名前探索 (name lookup))、探索場所の候補に、指定した名前空間を追加するというものです。そのため、探すべき場所が増えることで、同じ名前がそれぞれの場所で発見されてしまうことがあります。

#include <iostream>

namespace n {
    int x {10};
}

int x {20};

int main()
{
    using namespace n;  // n を探索場所に追加する

    std::cout << x << "\n";  // エラー。n::x か ::x か分からない
}

一方で、次のプログラムはエラーになりません(ただし、警告されるかもしれません)。スコープ解決演算子を用いずに名前を使おうとした場合、一番近いスコープで宣言されている名前が使われるからです。

#include <iostream>

namespace n {
    int x {10};
}

int main()
{
    using namespace n;  // n を探索場所に追加する

    int x {20};
    std::cout << x << "\n";  // OK。近いスコープで宣言された x が優先されるので、曖昧ではない
}

実行結果:

20

この挙動は、一見してよく似ているもう1つの機能、using宣言(後述)とはまったく異なっていることは注目すべきです。

using宣言

using宣言 (using declaration) を行うと、指定した名前を、現在のスコープ内で宣言された名前のリストに追加できます。

using宣言は以下のように記述します。

using 名前空間名::名前空間メンバ名;

【C++17】複数の using宣言をまとめて記述できるようになりました9

using宣言は usingディレクティブとよく似ているように見えるため、その意味を誤解しがちです。実際、指定した名前を使うときに、名前空間名:: の部分を省略できるという点ではまったく同じに見えます。

#include <iostream>

namespace n {
    int x {10};
}

int main()
{
    using n::x;

    std::cout << x << "\n";
}

実行結果:

10

しかし実際に行われていることは違っています。using宣言の場合、using宣言を記述したスコープに、指定の名前を導入する(追加する)という意味になります。

#include <iostream>

namespace n {
    int x {10};
}

namespace n2 {
    using n::x;  // 現在のスコープに n::x を導入。
                 // 以降、このスコープ内で x という名前で使える。
    
    int f() {
        return x;
    }
}

int main()
{
    using n::x;  // 現在のスコープに n::x を導入。
                 // 以降、このスコープ内で x という名前で使える。
    
    std::cout << x << "\n"
              << n2::f() << "\n";
}

実行結果:

10
10

この違いから、usingディレクティブではコンパイルエラーになる使い方が、using宣言ではエラーになりません。

#include <iostream>

namespace n {
    int x {10};
}

int x {20};

int main()
{
    using n::x;  // n::x を導入する

    std::cout << x << "\n";  // OK. x は 導入された n::x のこと
}

実行結果:

10

この場合、導入された n::x は、main関数のブロックスコープで宣言されているかのように扱われることになります。そのため、出力しようとしている x は、現在のスコープにある(導入された)n::x のことを意味します。グローバルスコープにも x が宣言されていますが、スコープ解決演算子を伴わない x という使い方では、近いスコープの名前が選ばれるため、曖昧さはありません。

一方、usingディレクティブではコンパイルできる使い方が、using宣言ではコンパイルエラーになります。

#include <iostream>

namespace n {
    int x {10};
}

int main()
{
    using n::x;  // n::x を導入する

    int x {20};  // エラー。現在のスコープにすでに x がある
    std::cout << x << "\n";
}

n::xx としてスコープに導入済みであるため、同じ名前の x を新たに宣言しようとしてコンパイルエラーとなります。

まとめ


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


参考リンク


練習問題

問題の難易度について。

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

問題1 (確認★)

次のプログラムの実行結果はどうなりますか?

#include <iostream>

namespace n1 {
    int x {5};
    int f()
    {
        return x;
    }
}

namespace n2 {
    int f()
    {
        return n1::f() * 2;
    }
}

int x {100};

int main()
{
    int x {10};

    {
        std::cout << x << "\n";
        int x {20};
        std::cout << x << "\n";

        std::cout << n1::f() << "\n";
        std::cout << n2::f() << "\n";
    }

    std::cout << x << "\n";
}

解答・解説

問題2 (確認★)

次のプログラムの実行結果はどうなりますか?

#include <iostream>

namespace code {
    namespace n1 {
        int x {5};
    }

    namespace n2 {
        int f()
        {
            using n1::x;
            return x;
        }
    }

    int f()
    {
        using namespace n1;

        return x + n2::f();
    }
}

int x {100};

int main()
{
    using namespace code;
    std::cout << f() << "\n";

    using n1::x;
    std::cout << x << "\n";
    std::cout << ::x << "\n";
}

解答・解説

問題3 (確認★)

次のプログラムはコンパイルエラーになります。理由を説明してください。

#include <iostream>

namespace n {
    #define X  (100)
}

int main()
{
    std::cout << n::X << "\n";
}

解答・解説

問題4 (応用★★★)

バイナリエディタのプログラムを次のように修正してください。

  1. 今後、汎用的に使えるように改造していくため、コマンドライン引数を解析する関数を、別のソースファイル・ヘッダファイルに分離する
  2. コードを名前空間に入れる
  3. std:: を何度も書かずに済ませる

最新のバイナリエディタのプログラムは、「コマンドライン引数」のページの練習問題にあります。

解答・解説


解答・解説ページの先頭



更新履歴




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