Programming Place Plus トップページ – C言語編
以下は目次です。
ここまでの章では、何らかの出力を行うときには、標準出力に対して行っていました。実はこれ以外に、標準で使うことができる出力先があります。それが、標準エラーです。
標準エラーは標準出力と同様に、具体的に何であるかは環境によって異なりますが、やはり画面であることが多いです。
標準エラーは、何らかのエラーや問題が発生した際に、その情報を出力するために使われます。標準出力と標準エラーには動作上の違いがあって、標準出力では、出力処理がただちに実行されないことがあります。これは、ある程度のデータが集まってから、まとめて実行するという方式を採ることがあるからです。
このような仕組みを、バッファリングといいます。第43章で扱います。
これに対して、標準エラーは、出力処理がただちに実行されます。そのため、万が一、プログラムの実行中に問題が起きて、強制終了せねばならない事態に陥ったとしても、それまでに出力しようとしていたものは、きちんと出力済みになっていることが保証できます。一般に、エラーに関する情報は失われてはならない(無視されては困る)ので、標準エラーを使って出力します。
標準エラーへ出力するには、これまでに使っていた出力関数(puts関数、printf関数)に代わって、fputs関数や fprintf関数を使います。fputs関数、fprintf関数は、<stdio.h> で宣言されています。
fputs関数は、第1引数に出力したい文字列を、第2引数に出力先を指定します。
第2引数に stderr を指定すると標準エラーへ出力し、stdout を指定すると標準出力へ出力できます。それぞれ、<stdio.h> で定義されているマクロです。
【上級】本来、fputs関数の第2引数には FILE*型で表される、ストリームを指定します(第39章)
なお、puts関数と違って、fputs関数は自動的に改行してくれないことに注意してください。
( "Hello\n", stderr ); // 標準エラーへ出力
fputs( "Hello\n", stdout ); // 標準出力へ出力 fputs
fprintf関数の方は、第1引数に出力先を指定します。第2引数以降は、printf関数と同じで、変換指定と、対応する引数の並びが続きます。こちらは、出力先の指定以外は printf関数と同じと考えて構いません。
( stderr, "%d %f\n", a, b ); // 標準エラーへ出力
fprintf( stdout, "%d %f\n", a, b ); // 標準出力へ出力 fprintf
次のサンプルプログラムは、標準出力と標準エラーの両方を使っています。
#include <stdio.h>
#include <assert.h>
void printDivAnswer(int num, int d);
int main(void)
{
( 100, 10 );
printDivAnswer( 100, 0 );
printDivAnswer( 100, 5 );
printDivAnswer}
void printDivAnswer(int num, int d)
{
if( d == 0 ){
( "0 で除算できない。\n", stderr );
fputs}
else{
( "%d\n", num / d );
printf}
}
実行結果:
10
0 で除算できない。
20
標準出力と標準エラーが、同じ場所(画面など)になっている環境では、両方の出力が混ざります。
【上級】バッファリングの設定次第で結果の混ざり方は変わります。たとえば、標準出力がフルバッファリング、標準エラーがバッファリング無しになっていたら、先ほどのサンプルプログラムの実行結果は「0 で除算できない。、10、20」の順番に並ぶでしょう(第43章)
assert というマクロは、デバッグの助けとして非常に有益です。assert は「アサート」と読み、「表明する」という意味です。
assert は、<assert.h> に定義されています。
アサートは、プログラム内のある箇所で、「処理がこの位置に到達したとき、こうなっていなければならない」という予定を表明します。プログラムを実行したとき、もし、予定どおりの状態になっていなければ、プログラムの実行をその場で停止して、標準エラーにメッセージを出力します。
どのように停止するのか確認しておいた方が良いでしょう。実際に試してみます。
#include <stdio.h>
#include <assert.h>
int divide(int num, int d);
int main(void)
{
( "%d\n", divide( 100, 0 ) );
printf}
int divide(int num, int d)
{
( d != 0 ); // 0 で除算できない
assert
return num / d;
}
実行結果:
Assertion failed: d != 0, file c:\main.c, line 15
assert の実引数が真であれば何も起きません。偽であれば、プログラムは abort関数によって強制的に停止されます。このサンプルプログラムは、偽になるため、すぐに停止します。
abort関数は、<stdlib.h> で宣言されています。
void abort(void);
abort関数はプログラムを異常終了させる関数です。異常終了という言葉が示しているように、これは異常時の強制終了を行うための関数です。
異常ではなく、正常な意味でプログラムを終了させることが目的であれば、main関数から return文で抜けるか、exit関数や _Exit関数を使います。C11 では quick_exit関数という選択肢もあります。
なお、assert による停止時には、ソースファイルの名前(__FILE__ によるもの)や行数(__LINE__ によるもの)、関数名(__func__ 、どんな条件式が与えられていたかといった情報が、標準エラーへ出力されます。
Visual Studio 2017 では、__func__ の内容が出力されないようです。
また、assert は、NDEBUGというオブジェクト形式マクロが定義されていない場合にだけ有効です。
具体的には、次のような感じになっています。
#ifdef NDEBUG
#define assert(b)
#else
#define assert(b) 実装コード
#endif
このような作りなので、NDEBUG を定義するのなら、assert.h をインクルードするよりも前で行う必要があります。
#define NDEBUG
#include <assert.h>
NDEBUG を、デバッグ作業を行っている開発段階では定義せず、完成品をビルドするときに定義するようにすれば、デバッグ作業を行っている段階では assert は有効になり、完成品では無効(空定義)になります。あえて完成品で無効にすることには、無駄な処理をなくして性能を向上させる意味があります。
Visual Studio の場合は、ビルド構成を「Release」に変更すると自動的に NDEBUG が定義されます。「Release」というビルド構成は、完成品としてビルドすることを意味しています。デフォルトでは「Debug」になっていると思いますが、こちらはデバッグ作業中のビルド構成です。ビルド構成については、「Visual Studio編>ビルド構成について」で説明しています。
アサートを使うにあたって注意しなければならないことがあります。たとえば、次のような使い方には問題があります。
( func() != 0 ); assert
この assert は、func関数の戻り値が 0以外であることを想定したものです。
問題は、assert に与えた式は評価(第27章)されるということです。評価されるので、func関数が実際に呼び出されます。当たり前といえば当たり前のようですが、NDEBUG が定義されると、assert そのものが消えてしまうところに罠があります。
NDEBUG が定義されると、func関数を呼び出す式ごと assert が消失してしまいます。もし、func関数の内部で、副作用(第27章)がある処理をしていたら(静的記憶域期間を持つ変数の値を変更したり、何らかの出力を行ったり)、プログラムの実行結果自体が変わってしまうでしょう。これは避けなければならない事態です。
そこで、式の中で、副作用のある部分を抜き出して、assert の手前で行うようにします。先ほどのケースでは、func関数の呼び出しを assert の手前で独立して行い、戻り値をチェックする部分だけを assert の実引数にします。
= func();
ret ( ret != 0 ); assert
自分で都合の良い仕様のアサートマクロを作ることも可能ですし、実際、よく行われています。
標準の assert で出力される内容は、条件式、ファイル名、行数の3つの情報ですが、ここにさらに自分の好きなメッセージを追加できるようにしてみましょう。そのためには、次のように定義します。
#ifdef NDEBUG
#define myassert(b,str)
#else
#define myassert(b,str) \
{if(!(b)){fprintf(stderr, "assert: %s \n%s\n%sの%d行目 (%s)\n", #b, str, __FILE__, __LINE__, __func__); abort(); }}
#endif
この myassertマクロは、2つの引数をとります。1つ目は、標準の assert と同じく条件式で、これが偽になったときに停止します。2つ目の引数は、停止した場合に、標準エラーへ出力する文字列です。
さて、この myassertマクロを使って、先ほどのプログラム例を書き換えるとこうなります。
#include <stdio.h>
#include <stdlib.h>
#ifdef NDEBUG
#define myassert(b,str)
#else
#define myassert(b,str) \
{if(!(b)){fprintf(stderr, "assert: %s \n%s\n%sの%d行目 (%s)\n", #b, str, __FILE__, __LINE__, __func__); abort(); }}
#endif
int divide(int num, int d);
int main(void)
{
( "%d\n", divide( 100, 0 ) );
printf}
int divide(int num, int d)
{
( d != 0, "0 で除算できない" );
myassert
return num / d;
}
実行結果:
assert: d != 0
0 で除算できない
c:\main.cの21行目 (divide)
実行結果にあるように、実引数で指定したメッセージが、標準エラーに出力されるので、停止した理由を分かりやすく示せます。
今回の例は、非常に単純な拡張ですが、単なる文字列ではなく printf関数の形式で文字列を渡せるようにしたり、すぐに停止させてしまうのではなく、停止してもいいか確認してから停止させるようにしたりできます。停止させないことを選択すると、そのアサートを無視して実行を続行させます。
たまに、真のときに停止するアサートを独自で作っている人や、そもそも条件式のないものを作っている人(必ず停止する)がいますが、それはもはやアサートとは呼べません。そういうマクロを作っていけないわけではありませんが、その場合、アサートという名前を付けないようにします。
アサートの応用として、コンパイル時アサート(静的アサート)というものがあります。次のマクロは、コンパイル時アサートを実現したものです。
#define STATIC_ASSERT(exp) typedef char static_assert_dummy[exp ? 1 : -1]
このマクロは、問題があるコードをコンパイル時に検出して、コンパイルエラーを強制的に起こします。assert はプログラムの実行時に問題を検出するので、検出するタイミングに違いがあります。
コンパイル時に問題を検出できなければならないので、コンパイル時アサートに与える式は定数式(第27章)です。
STATIC_ASSERTマクロの仕組みはなかなか技巧的です。
マクロ内でダミーの配列型を typedef で定義しています。その際の要素数の指定の仕方がポイントで、条件式が真であれば 1、偽であれば -1 としています。要素数に負数を指定した配列は宣言できないため、条件式が偽のときにだけコンパイルエラーが起こるという仕組みです。
コンパイル時アサートを使う事例として、型の大きさが想定どおりかどうかを調べるだとか、配列の要素数がきちんと指定されているかを調べることなどがあります。たとえば、第26章で、次のような typedef の使い方をしました。
typedef int int32;
これは、int型の大きさが 32ビット (4バイト) であることを想定したものです。しかし、この想定が真ではない環境でこの型を使ってしまったら、間違った結果を生んでしまうことでしょう。このような事故を、コンパイル時アサートを使って防げます。
#define STATIC_ASSERT(exp) typedef char static_assert_dummy[exp ? 1 : -1]
typedef int int32;
(sizeof(int32) == 4);
STATIC_ASSERT
int main(void)
{
}
sizeof(int32) == 4
が真になる環境では、このプログラムは問題なくコンパイルできます。一方、偽になる環境では、次のようなコンパイルエラーが出力されます。
main.c(4): error C2118: 添字が負の数です。
エラーの文面が、本当のエラーの内容を表すものになりませんが、少なくともソースファイル上の位置が分かるので、そこを見に行けば起きていることが分かるはずです。
C11 になって、標準のコンパイル時アサートである _Static_assert が追加されました。
int main(void)
{
_Static_assert(sizeof(int) == 4, "");
}
_Static_assert は標準機能なので、ヘッダのインクルードも必要ありません。第1引数の条件式が偽になるとき、コンパイルが失敗し、第2引数に指定した文字列を含んだエラーメッセージを出力します。
また、<assert.h> をインクルードすると、static_assert という名前で使用できます。
【上級】static_assert という名前は、C++(C++11以降)に標準機能として存在しているコンパイル時アサートと同じです。
#include <assert.h>
int main(void)
{
(sizeof(int) == 4, "");
static_assert}
Visual Studio 2017 では、_Static_assert が使えません。static_assert については、Visual Studio 2017 でも使用できますが、これは <assert.h> に含まれているものではなく、拡張機能のようです。
問題① 標準エラーに、末尾での改行付きで文字列を出力するマクロを、デバッグ作業中にだけ有効になるように作成してください。
問題② assert の次のような使い方には問題があります。理由を説明して、正しく修正してください。
int n = -2;
( ++n >= 0 ); assert
return 0;
を削除(C言語編全体でのコードの統一)’2019/2/12 VisualStudio 2015 の対応終了。
’2018/8/27 VisualStudio でビルド構成を変更する方法を、「開発ツールの情報」のページでサポートするように修正。
’2018/6/4 新規作成。
標準エラーは、第39章から一部移動してきて再構成。
アサートやコンパイル時アサートは、第28章から移動してきて再構成。
Programming Place Plus のトップページへ