『知っておきたい C標準ライブラリ』無料サンプル | Programming Place Plus e-Book Project

トップページe-Book Project「知っておきたい C標準ライブラリ」


このページでは、『知っておきたい C標準ライブラリ』の無料サンプル版を公開しています。

目的

真を 1、偽を 0 で表現して、以下のような条件式を記述しているC言語のプログラムがあります。

if (error == 1) {  // エラーが起きているか?
}

if (debug_mode == 0) {  // デバッグモードでは無い?
}

しかし、0 や 1 という値は、論理値の意味だけではなく、 何かの個数や ID などを表現するときにも使われる可能性があるため、区別が付きにくい欠点があります。
上記の例で、変数debug_mode は本当に真か偽の2択なのでしょうか? 3通り以上のデバッグモードが存在していて、2 や 3 といった数も異なる意味を持っているかも知れません。

多くのプログラミング言語では、真や偽を表す専用の型、および専用の名前付き定数が用意されています。 そういったものを使った方が、プログラムは分かりやすくなり、間違いを減らす効果もあります。

C言語の場合、C99 になって、_Bool型という型が追加されました (VisualStudio 2010/2012 では使用できません)。 これは、標準ライブラリではなく、int型のような、いつでも使える型として用意されています。 ところが、真や偽に対応する名前付き定数は用意されていません。

また、_Bool型は整数型の一種として定義されたものであるため、「_Bool b = 3;」のように、 0 と 1 以外の値を受け付けてしまい、理想からは遠いものになっています(ただし、0以外の値は、自動的に 1 に変換されます)。 もう少し良い手段が無いでしょうか?

実装① (stdbool.h を使う)

C99 で追加された stdbool.h という標準ヘッダをインクルードすると、 論理型を表す bool、真を表す true、偽を表す false がそれぞれ使用できるようになります。 これらの名前は、C++ が標準的に備えている機能と同じになっています。

#include <stdbool.h>
#include <stdio.h>

int main(void)
{
    bool debug_mode = true;

    if (debug_mode) {
        puts("debug mode");
    }

    debug_mode = false;
    if (!debug_mode) {
        puts("release mode");
    }

    return 0;
}

実行結果:

debug mode
release mode

bool、true、false は、以下のように定義された #define に過ぎません。

#define bool _Bool
#define true 1
#define false 0

冒頭で説明した通り、_Bool型は完璧な論理型ではなく、単なる整数型の一種です。 bool の正体が _Bool である以上、やはり完璧を求めることはできません。 例えば、以下のコードは問題なくコンパイルできてしまいます。

bool debug_mode = 3;  // true や false 以外で初期化、代入できてしまう (true へ変換される)

とはいえ、完璧な論理型が提供されていないC言語では、この辺りには目を瞑らざるを得ません。

実装② (自前で実装する)

VisualStudio 2010/2012 には stdbool.h がありませんが、bool、true、false はごく単純なマクロなので、 同じものを定義すれば、ほぼ同じことができます。 ただし、これらのコンパイラには _Bool型も無いので、int型で代替することになります。

#include <stdio.h>

#define bool int
#define true 1
#define false 0

int main(void)
{
    bool debug_mode = true;

    if (debug_mode) {
        puts("debug mode");
    }

    debug_mode = false;
    if (!debug_mode) {
        puts("release mode");
    }

    return 0;
}

実行結果:

debug mode
release mode

この場合の bool型は、論理値を扱うための特殊な能力を何も持っていない、ただの int型ですので、 2 や 3 といった値を代入できますし、その数値のままで保持されます (_Bool型の場合は、2 や 3 でも代入できるが、自動的に 1 に変換される)。

この方法を採る場合、使っている外部のライブラリなどが、同じ目的の定義を行っていないかどうか注意して下さい。 置換結果も含めて完全に同一であれば問題ありませんが、置換結果が異なっていれば混乱の元ですし、 違った名前で同じ目的の定義が用意されていたら、混ぜて使うことが混乱の元になります。

スタイルとして望ましいかどうかは別として、 マクロは内容がまったく同じであれば、複数の箇所で定義されていても問題ありません。

補足

真かどうかを確認する際には、「b == true」のように、明示的に論理値の比較を行うのではなく、 「b」のように書いた方が良いです。 C言語においては、「0以外」のすべてを「真」として扱わなければなりませんが、 true は 1 であるため、b の値が 2 とか 3 とかだった場合に、誤って偽であると判定されてしまいます。

同じ考え方を、偽かどうかの判定にも適用するかどうかは意見が分かれるところです。 「偽」の方は「0」しかあり得ないので、「b == false」と書いても問題はありませんし、 「!b」のように書くと「!」を見落としやすく、かえって間違いに気付きにくいという意見もあります。

対応状況

この章で登場した関数やマクロなどの対応状況について、まとめました。

目的

プログラムを書いているとき、プログラマはその場面ごとに何らかの想定をしています。 例えば、「この変数には、データの件数が入っている。つまり、1以上になっているはずだ」とか、 「この関数の引数には、呼び出し元にある変数を指すポインタが渡されてくるはずで、よってヌルポインタはあり得ない」といった具合です。

ところが、出来上がったプログラムがいつも想定通りに動くとは限りません。 同僚のプログラマが(あるいは自分自身が)、想定した通りの引数を渡さないかも知れません。

問題はできるだけ早い段階で見つけることが重要です。 想定外の状態であるとはいえ、その後しばらくは処理が正常に動いてしまう可能性はあります。 プログラムが異常停止するような致命的な問題は、ずっと先の方で起こるかも知れないのです。 そこから遡って根本原因を特定するのは大変です。

そこで、想定外の状態を検出したら、プログラムの実行をただちに止めて、問題を報告するという機能が必要になります。 一般に、このような機能をアサート(表明)と呼びます。 アサート機能は独自に作ることもできますし、高度なデバッグ機能を必要とするプロジェクトではそうしていることが多いですが、 C言語の標準ライブラリにも、必要最小限のアサート機能が用意されています。

実装① (assertマクロを使う)

C言語の標準ライブラリには、アサートの機能が用意されています。 assert.h という標準ヘッダに定義されている assertマクロを使用します。

次のサンプルプログラムは、渡されてきた引数を使って除算を行う関数において、 ゼロ除算を検出するために assertマクロを使っています。

#include <assert.h>

static int divide(int a, int b)
{
    assert(b != 0);  // 0 で割ることはできない
    return a / b;
}

int main(void)
{
    divide(10, 0);

    return 0;
}

実行結果 (例):

Assertion failed: b != 0, file c:\main.c, line 5

assertマクロに渡した式の結果が偽の場合には、標準エラーに問題が検出されたことを示すメッセージが出力されて、 プログラムの実行が停止します。 実引数の式が真になる場合には、何も起こりません。

停止した場合に出力されるメッセージには、実引数の内容、__FILE__、__LINE__、__func__ で得られる内容が含まれます。 なお、これらの中で __func__ については、C99 で追加されたものですが、 VisualStudio 2010/2012/2013/2015/2017、gcc 7.1 では、出力されないようです。

assertマクロがプログラムを停止させる際には、標準ライブラリ関数の abort関数が使用されます。

assertマクロの置換結果は、NDEBUG というマクロが定義されている場合には完全な空になります。 これを利用して、プログラムの開発中には有効な状態(NDEBUGマクロが定義されていない状態)にしておき、 リリース段階では空の状態にすれば、条件チェックに伴う余分なオーバーヘッドを除去できます。

NDEBUGマクロの定義は、assert.h のインクルードよりも前で行う必要があります。 ただソースファイルが複数あると、1つ1つに定義を書いていくのは大変なので、 コンパイラオプションなどで NDEBUG を定義することを伝える方法を使うことが多いでしょう。

VisualStudio の場合は、プロジェクトのプロパティのところで設定が行われており、 デフォルトのままにしていれば、Debugビルドでは NDEBUG は定義されず、 Releaseビルドでは定義されるようになっています。 clang や gcc の場合は、コンパイルオプション「-DNDEBUG」を指定してコンパイルします。

プログラム内で定義する場合の例を示します。

#define NDEBUG
#include <assert.h>  // assertマクロは空になっている

static int divide(int a, int b)
{
    assert(b != 0);  // b が 0 でも停止しない
    return a / b;  // ゼロ除算になるかも知れない
}

int main(void)
{
    divide(10, 0);

    return 0;
}

あくまでもアサートは「想定外の」問題を検出するためのものであることに注意して下さい。 先ほどのプログラムで言えば、divide関数の第2引数が 0 になることを許す(想定している)のであれば、 アサートを使うことはできません。

補足

assertマクロによるチェックは、リリース時には無効化するのが普通です。 もし、assertマクロの実引数の部分で関数呼び出しを行っていたとすると、リリース時にはその関数は呼び出されません。 プログラムの実行結果そのものが変わってしまう可能性があるので注意して下さい。

int f(int* p)
{
    *p += 10;
    return *p;
}

int x = 0;
assert(f(&x));      // f() はリリース時には呼ばれない
printf("%d\n", x);  // デバッグ時は 10、リリース時は 0

assertマクロの実引数のところに、関数呼び出しは記述しないのが無難です。 関数の戻り値が想定通りかどうかをチェックしたければ、 assert の前で変数に受け取り、変数の値をチェックしましょう。

int x = 0;
int result = f(&x);  // assert の前で呼ぶ
assert(result);

対応状況

この章で登場した関数やマクロなどの対応状況について、まとめました。





この本の紹介ページへ

Programming Place e-Book Project のトップページへ

Programming Place Plus のトップページへ



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