標準ライブラリで提供される便利な機能:目指せ! Cプログラマ(14)
前回に引き続き標準ライブラリについて理解を深めましょう。今回は標準ライブラリで提供される便利な機能のうち、stddef.h、stdarg.h、signal.h、stdlib.h で用意されているものに注目して解説をします。見出しのカッコ内に関係するヘッダーファイルを示してあります。
型(stddef.h)
Cでは、intやcharといった基本的な型があることはこれまでに学びました。他にも色々な型がありますから、ここで紹介しましょう。プログラムでよく使われる便利な型はstddef.hで定義されています。これまでの中ですでに登場しているものとしては、sizeof演算子の結果の型であるsize_tと、ワイド文字を表現するための型であるwchar_tがあります。
size_t s = sizeof(int); wchar_t w = L'あ';
stddef.hには、ptrdiff_tという型も用意されています。これはポインターとポインターの差を表す型で、ポインター同士の減算の結果はこの型になります。
#include <stdlib.h> #include <string.h> #include <stdio.h> #include <stddef.h> int main ( void ) { char *decimal = "0123456789"; // 文字列 decimal から文字 3 と 7 の位置を探す。 char *three = strchr(decimal, '3'); char *seven = strchr(decimal, '7'); // 文字 3 と 7 のポインターの差を計算する。 ptrdiff_t diff = seven - three; // Visual C++では書式文字列を "diff = %Id\n" とする。 // diff = 4 printf("diff = %td\n", diff); // three + diff = 7 printf("three + diff = %c\n", *(three + diff)); return EXIT_SUCCESS; }
文字列decimalにおいて、文字'3'と'7'は4だけ離れています。つまり'3'の位置から4つ進むと'7'です。この差を表す型がptrdiff_tです。
ポインターthreeの値はdecimal + 3と同じになります。同様にポインターsevenの値はdecimal + 7と同じです。ここで登場する7と3という値の差が、ポインター同士の差を求めるseven - threeという式の計算結果と同じになるようになっています。
ただしポインター同士で減算できるのは、両方のポインターが同じオブジェクトの要素(厳密には、オブジェクトの要素か、あるいはオブジェクトの最後の要素を一つ超えたところ)を参照している場合だけであることに注意して下さい。まったく別のオブジェクトを参照するポインター同士で減算はできません。この例ではthreeもsevenも、ポインターdecimalが参照する同じオブジェクトの要素を参照していますのでseven - threeという演算が可能です。
また、stddef.hでは、NULLおよびoffsetofというマクロも定義されています。offsetofマクロは構造体に使い、その構造体の指定メンバーが先頭からどれくらい離れているかをバイト単位で取得します。
#include <stddef.h> #include <stdlib.h> #include <stdio.h> int main ( void ) { struct s { char member1; int member2; char member3; }; // Visual C++では書式文字列を "%Iu,%Iu,%Iu\n" とする。 printf("%zd,%zd,%zd\n", offsetof(struct s, member1), offsetof(struct s, member2), offsetof(struct s, member3)); return EXIT_SUCCESS; }
実行結果は環境ごとに異なります。手元の環境では0,4,8となりました。charのサイズは1ですが、それに対してメンバーの離れ方が大きいことから、パディングが付加されていることが分かります。
なお、offsetofマクロの結果はバイト単位ですので、構造体の先頭アドレスからポインターを求める際は、ポインターの型に注意して下さい。
struct s st; size_t of2 = offsetof(struct s, member2); // p2に member2 のポインター(つまり &(st.member2))を設定したい。 void * p2; // &st の型は struct s へのポインターなので、この方法は正しくない。 //p2 = &st + of2; // char のサイズは 1 バイトなのでOK。 p2 = (char *) &st + of2;
可変個数引数(stdarg.h)
- 可変個数引数を処理する
これまでに書いてきたサンプルコードの関数は引数の数が決まっていましたが、可変個数引数の関数を定義することができます。可変個数引数の関数は、呼び出すときに引数の数を決められるので大変便利です。
この代表例はprintf関数です。printf関数のプロトタイプは次のようになっています。
int printf ( const char * restrict format, ... );
2つ目以降の引数は...となっていて、呼び出すときには1つ以上の引数を渡せます。
// Length of 'abc' is 3. char *name = "abc"; printf("Length of '%s' is %d.", name, strlen(name));
つまり、関数を定義するにあたって引数(仮引数)の数を定義せず、関数を呼び出す時の引数(実引数)で引数の数を決めます。このように、数が可変である引数(可変個数の仮引数)は...で表します。...は必ず引数リストの最後に指定します。
可変個数引数の値はstdarg.hで定義されるマクロva_start、va_arg、va_endと型va_listを使って、次のようにして取り出します。
void func ( int n, ... ) { va_list ap; // (1) va_start(ap, n); // (2) for (int i = 0; i < n; i++) { char *a = va_arg(ap, char *); // (3) } va_end(ap); // (4) }
まずva_list型のオブジェクト(例ではap)を用意します。可変個数の仮引数には名前がありませんので、これを使ってアクセスします(1)。
次に可変個数の仮引数...の直前の仮引数(この例ではn)とva_list型のオブジェクトを引数にしてva_startマクロを呼び出します(2)。
これで可変個数引数にアクセスする準備ができました。va_argマクロを、先ほど定義したva_list型のオブジェクトに加えて、引数の型を指定して呼び出すことで、引数の値を並び順に取得することができます(3)。
最後にva_endマクロを呼び出して、可変個数引数へのアクセスを終了します(4)。
さて、(3)の処理では、n個のchar * 型の変数を受け取っている前提で処理をしています。勘の良い読者はここで関数宣言において可変個数の引数の型について指定がないことに気がつくでしょう。実は、可変個数の引数の型については、すべて同じでも構いませんし、複数の型が混ざっていても構いません。ですから、可変個数の引数を扱うときには、引数の数とその型を関数の作成者が意識して処理を記述する必要があります。あらかじめ仕様として決めておく、あるいは必要な情報を引数として渡すなどの方法が考えられます。この例では、可変個数の引数の数は最初の仮引数nで渡すこととし、可変個数の引数の型はすべてchar * としました。もしこれらが関数側と呼び出し側で違っていると、異常な動作を引き起こすことがありますので、必ず一致させる必要があります。
これは前回の記事でセキュリティの注意点としてあげた「fprintf関数やfscanf関数の書式指定に外部からの入力を使わない」に関連してきます。可変個の引数を扱うとき、関数へ渡す引数の数や型の解釈をプログラム外部から入力させると、不正な動作を引き起こし、セキュリティ上のリスクを発生させます。これはfprintfのような標準ライブラリだけの問題ではないので、それ以外の関数を使う際にも注意して下さい。
- 可変個数引数を別の関数に渡す
va_list型のオブジェクトは、他のオブジェクトと同じように、別の関数へ渡すこともできます。標準ライブラリでは、vfprintf関数などがそれにあたります。vfprintf関数のプロトタイプは次のようになります。
int vfprintf ( FILE * restrict stream, const char * restrict format, va_list arg );
vfprintfでは、va_argマクロを使って引数を処理します。va_argマクロを呼び出す前にva_startを、va_argを呼び出し終えたらva_endを呼び出す必要がありますので、vfprintfの処理はva_startとva_endとで囲みます。
// C99 に対応していない環境では restrict を外す。 void outlog ( FILE * restrict stream, const char * restrict level, const char * restrict format, ... ) { va_list ap; va_start(ap, format); // ここでは level だけ出力し、 // 可変個数引数の処理は vfprintf にまかせている fprintf(stream, "[%s] ", level); vfprintf(stream, format, ap); va_end(ap); } int main ( void ) { char *errorMessage = "XX error"; int errorCode = 3; // [ERROR] XX error (3) outlog(stdout, "ERROR", "%s (%d)\n", errorMessage, errorCode); }
シグナル(signal.h)
プログラムの実行中にシステムからイベントを受け取る方法として、シグナルが用意されています。例えばプログラムが不正な演算を行った、というような異常の通知があります。シグナルはプロセス間でも発行できますので、プログラムに終了要求を発行するなど、簡単なイベントを通知するためにも使われます。
シグナルを受け取るには、受け取るシグナルの種類とともに、シグナルを受け取る関数をsignal関数で登録します。signal関数のプロトタイプは次のようになっています。
void ( * signal ( int sig , void ( * func ) ( int ) ) ( int );
signal関数の仮引数は、1つ目がint、2つ目がvoid (*func)(int)です。2つ目は、intの引数が1つで戻り値のない関数のポインターですね。そしてsignal関数の戻り値は、int型の引数が1つで戻り値のない関数のポインターです。ぱっと見たところでは理解しづらい形ですが、次のように書くともう少しわかりやすいかもしれません。
// SIGFUNC は int の引数を1つとり、何も返さない関数ポインター typedef void ( * SIGFUNC ) ( int ); // signal は int と SIGFUNC の関数を1つずつとり、SIGUFNC を返す関数 SIGFUNC signal ( int sig , SIGFUNC func );
signal関数の1つ目の引数はシグナルの種類を表します。定義されているシグナルは次のとおりです。
- SIGABRT : 異常終了
- SIGFPE : 誤った算術演算(ゼロ除算やオーバーフロー)
- SIGILL : 不正な関数イメージの検出(不正命令)
- SIGINT : 対話的なアテンションシグナルの受取り
- SIGSEGV : 記憶域への不正なアクセス
- SIGTERM : プログラムへ送信された終了要求
この他にも、POSIX環境におけるSIGHUPやSIGKILLなどのように、システムごとに独自のシグナルが定義されている場合もあります。
プログラムの起動時は、シグナルに対して規定の動作をするようになっています。これを変更するには、1つ目の引数にシグナルの種類を、2つ目の引数にシグナルを処理する関数を指定してsignal関数を呼び出します。規定の動作に戻したい時は、関数の代わりにSIG_DFLを、シグナルを無視したい時はSIG_IGNを指定します。signal関数の戻り値は、正常にシグナル処理関数が設定された場合は仮引数func、設定に失敗した場合にはSIG_ERRになります。
シグナルを自分で発行することもできます。そのためにはraise関数を使います。
int raise ( int sig );
引数はシグナルの種類、戻り値は成功した時に0になります。
ここまでシグナルについて説明してきましたが、実際のところsignal関数とraise関数をそのまま使うのが良いかどうかはシステムごとによく調べる必要があります。特定のシグナルが処理されなかったり、動作がまちまちであったりするからです。Linuxなどの場合はsignal関数も使われていますが、POSIX標準でより細かく制御できるsigaction関数を使う場合もあります。Windowsでのエラー処理はシグナルではなく構造化例外処理(SEH)を使うのが一般的で、通常の通知はイベントループで処理します。
実際にはシグナルが使える場面は少ないかもしれませんが、知識として必要ですし、選択肢として持っておくと良いので、紹介をしました。
環境(stdlib.h)
次に、stdlib.hで提供されている実行環境などに関係する機能について説明します。
- atexit
プログラム終了時に実行させたい処理、例えば確保したリソースを解放するといった処理があったとします。こういった処理はそれなりに複雑になるので関数化し、プログラムが終了する時にこの関数を呼び出すようにコーディングします。プログラムが終了する処理はソースコードの複数の場所に記述できるので、そこに関数を呼び出す処理を書くこともできますが、atexit関数を使うともっと簡単に実現できます。
atexit関数を使うと、プログラムの実行が終了する時に呼び出される関数を登録できます。atexitには引数と戻り値を持たない関数のポインターを渡します。複数の関数を登録する(同じ関数を複数回登録してもよい)ときはatexitを必要なだけ呼び出すことができ、登録された関数は、登録と逆の順序で呼び出されます。
#include <stdlib.h> #include <stdio.h> void func1 ( void ) { printf("func1\n"); } void func2 ( void ) { printf("func2\n"); } int main ( void ) { atexit(func1); atexit(func2); printf("main\n"); return EXIT_SUCCESS; }
このプログラムは次の結果を出力します。
main func2 func1
main関数を抜けたあとにatexitで登録した関数が呼び出されていることが確認できます。
- exitとabort
ここまでのプログラムではmain関数が終了するときにプログラムが終了していましたが、それ以外の関数からでもプログラムを終了させたいことがあります。そんなときはexitやabortという関数を使います。exit関数を呼び出すとプログラムはその場で正常終了します。
#include <stdlib.h> #include <stdio.h> void func (void ) { // exit を呼び出すとプログラムは終了する exit(EXIT_SUCCESS); // ここへは戻ってこない } void func2 ( void ) { printf("func2\n"); } int main ( void ) { // 終了時に呼び出される関数を登録 atexit(func2); func(); // ここへは戻ってこない。 // func2 と表示される。 return EXIT_SUCCESS; }
exit関数では終了コードを指定します。指定する値の意味はmain関数の戻り値と同じです。exit関数を呼び出すと、atexitで登録された関数の呼び出しや、バッファの書き出し、ストリームのクローズ、一時ファイルの削除などの処理が行われたあとにプログラムは終了します。
終了するための関数には、exitの他にabortがあると説明しました。exit関数は正常に終了するときに使いますが、abort関数は異常終了するときに使います。abortではexitと違って引数を取らず、atexitで登録された関数を呼び出しません。また、バッファの書き出し、ストリームのクローズ、一時ファイルの削除が行われるかどうかは、処理系次第となります。
コラム quick_exit
C11規格(ISO/IEC 9899:2011)では、at_quick_exit関数及びquick_exit関数が追加されています。
exitでもquick_exitでもプログラムの実行が終了することは同じですが、exitの呼び出しではatexitで登録された関数が呼び出されるのに対し、quick_exitの呼び出しではat_quick_exitで登録された関数が呼び出されます。またquick_exitを呼び出した場合は、signal関数で登録された関数は呼び出されない、バッファの書き出しやストリームのクローズ、一時ファイルの削除は行われないかもしれない(処理系ごとに決まる)、という違いがあります。
プログラムの実行中に、何らかの原因でデータが壊れてしまうことがあります。単なるプログラムのバグかもしれませんが、悪意を持ったユーザーが意図的に不正なデータを送り込んできたのかもしれません。そのような状態で終了処理を行うと、その終了処理がセキュリティ上のリスクになることがあります。もしプログラム終了後に環境(オペレーティングシステムなど)によって適切にリソースが解放されるのであれば、プログラム側ではそれらを行わずに終了してしまう方が良いとも考えられます。現代のオペレーティングシステムでは、小規模な組み込み向けのものを除いて、おおよそ適切にリソースを解放してくれることが期待できます。
実はこのような用途のために、あまり使われていませんが_Exitという関数も用意されていました。しかしこれは、abortにexitの引数である終了コードを渡せるようにしたようなもので、終了処理を行う機会がほとんどありません。この中間の要求に応えるためにquick_exitが用意されました。
関数名 | 終了コード | 呼び出される関数の登録 | バッファの書き出しなど |
---|---|---|---|
exit | 指定可 | at_exit、signal | 行われる |
quick_exit | 指定可 | at_quick_exit | 行われないかもしれない |
_Exit | 指定可 | なし | 行われないかもしれない |
abort | 指定不可 | なし | 行われないかもしれない |
quick_exitが使える環境では良い選択肢となりえます。特にセキュリティに気をつけるべきプログラムでは、quick_exitの利用を検討してみて下さい。
- getenv
getenvは環境変数へアクセスするための関数です。環境変数は実行環境によって提供される変数で、名前と値がペアになったリストです。このリストの中から指定の名前の変数の値を取得します。もっともよく知られている環境変数はPATHでしょう。次のプログラムでは環境変数PATHの値を表示します。
#include <stdlib.h> #include <stdio.h> int main ( void ) { char *name = "PATH"; char *value = getenv(name); printf("%s=%s\n", name, value); return EXIT_SUCCESS; }
Cではgetenv関数を使って環境変数の値を取得できますが、値を設定する関数は用意されていません。環境変数の扱い、例えばどのような環境変数が存在するかや、環境変数の設定の仕方、大文字小文字を区別するか、などは環境によって異なります。そのような情報が必要な場合は各々の環境について調べてみて下さい。
- system
既存のコマンドを自作プログラムから利用したいことがあります。system関数を使うと、プログラムから別のプログラムを実行できます。
int system ( const char * command );
引数にはコマンド文字列を渡します。このコマンド文字列がどう処理されるのか、あるいは戻り値としてどのような値が返るのかは環境によって異なります。基本的にWindowsではCMD.EXEに、Linuxでは/bin/shに渡され、処理されます。
アルゴリズム(stdlib.h)
アルゴリズムとは問題を解く手順を定式化したものです。整列(ソート)や探索といった問題をコンピューターで解くために、様々なアルゴリズムが考案されています。ある問題を解くためのアルゴリズムは複数あり、高速であるもののメモリが大量に必要であったり、必要なメモリは少ないものの低速であったり、それぞれに特徴があります。どのアルゴリズムが適切なのかは、対処しようとしている問題の特性により変わってきますので、一概には言えません。
Cの標準ライブラリでは、ソートや探索に対して、よく知られた一般的なアルゴリズムを提供しています。
- qsort
ソートのためにはqsortという関数が用意されています。
void qsort ( void * base, size_t nmemb, size_t size, int ( * compar ) ( const void * , const void * ) );
ポインターbaseでソート対象のデータを渡します。そのデータは、1つの要素のサイズがsizeで、要素の数はnmembです。最後の引数int (*compar)(const void *,const void *)の型は「const void * の引数を2つとり、int型を返す、関数へのポインター」です。次のように呼び出します。
#include <stdlib.h> #include <stdio.h> int compare ( const void * a , const void * b ) { // Visual C++ ではキャストが必要。 const int * pa = a; const int * pb = b; // printf("%d - %d = %d\n", *pa, *pb, *pa - *pb); return *pa - *pb; } int main ( void ) { // 整列前のデータ int data[] = { 4, 3, 5, 1, 2 }; qsort(data, sizeof(data) / sizeof(data[0]), sizeof(data[0]), compare); // 整列後のデータ ( 1 2 3 4 5 ) for (int i = 0; i < sizeof(data) / sizeof(data[0]); i++) { printf("%d ", data[i]); } printf("\n"); return EXIT_SUCCESS; }
仮引数comparに渡す関数は、1つ目の引数が2つ目の引数より小さかったら負の値を、等しければ0を、大きければ正の値を返すようにします。
qsort関数がどのようなアルゴリズムでソートするかは決められていませんが、クイックソート(quick sort)のアルゴリズムで実装されていると考えて良いでしょう(そうでなければ混乱しますね)。ソート対象のデータによってはクイックソートが適切とは限りませんので、速度を求めるような場合にはqsort関数を使わずに、別のソートアルゴリズムの実装を利用したり独自実装したりすることも検討してみて下さい。またqsort関数は安定ソートではありませんので、安定ソートが必要な場合も何か工夫が必要です。
ソートの詳細は他の記事、「コーディングに役立つ!アルゴリズムの基本」などを参照して下さい。その際、compare関数がどのように呼び出されているかをprintfで表示すると理解しやすくなるでしょう。
- bsearch
探索のためにはbsearchという関数が用意されています。
void * bsearch ( const void * key, const void * base, size_t nmemb, size_t size, int ( * compar ) ( const void * , const void * ) );
ポインターbaseが指す、1つの要素の大きさがsizeで、要素の数がnmemb個の配列から、ポインターkeyが指す値を探します。最後の引数にある比較関数は、qsortと同じ物が使えます。ただし、探索対象のデータは整列されている必要があります。もし整列されていないデータからbsearchで探索するには、あらかじめqsortなどでソートしておかなければなりません。
#include <stdlib.h> #include <stdio.h> // compare 関数は qsort の例と同じ。 int main ( void ) { int data[] = { 1, 3, 4, 5, 7 }; // ソート済みであること。 int key = 5; // Visual C++ では戻り値のキャストが必要。 int *p = bsearch(&key, data, sizeof(data) / sizeof(data[0]), sizeof(data[0]), compare); if (p == NULL) { printf("'%d' not found.\n", key); } else { // Visual C++ では書式文字列の %td を %Id とする。 // position = 3, value = 5 printf("position=%td, value=%d\n", p - data, *p); } return EXIT_SUCCESS; }
見つからなかった場合にはNULLが返りますので、戻り値の確認が必要です。見つかった場合には、見つかった位置のポインターが返ります。配列dataにおいて5の位置は data[3] (= data + 3)ですから、位置を確認すると結果は3になります。この演算はポインター同士の引き算ですので結果がptrdiff_tになり、printfで表示するときには%tdを使います。
コラム qsort_sとbsearch_s
C11からはqsortとbsearchのセキュリティ強化版qsort_sとbsearch_sが用意されていますので紹介します。
errno_t qsort_s ( void * base, rsize_t nmemb, rsize_t size, int ( * compar )( const void * x, const void * y, void * context), void * context );
qsortと比較すると、「戻り値がvoidからerrno_tになっている」「2つ目と3つ目の引数の型がsize_tからrsize_tになっている」「比較関数とqsort_s自体の引数にcontextが追加されている」という点が異なります。
errno_tはエラーコードを表します。従来はintを使ってエラーコードを返していましたが、それがエラーコードなのかどうかはプロトタイプだけ見ると区別がつきませんでした。これを明確にエラーコードであると表すために使われます。正常終了時には0、異常終了ならば0以外の値になります。
rsize_tはsize_tとほぼ同じと思っていただいて構いません。size_tは現実的にありえないほど大きなオブジェクトでも表すことができましたが、あまり大きな値は異常である可能性が高いので、それらを識別するために用意されました。size_tとrsize_tの型の大きさはまったく同じであることが保証されています(ただし最大値は異なります)。
最後のcontext仮引数は、qsort_sの呼び出し側から比較関数に値を渡すために用意されました。qsort_sの5つ目の引数contextが、比較関数の3つ目の引数contextにそのまま渡されます。qsortで同じ事をやろうとするとグローバル変数を用意する必要がありましたが、これは問題が生じる可能性がありますので引数で渡すことができるようになりました。
void * bsearch_s ( const void * key, const void * base, rsize_t nmemb, rsize_t size, int ( * compar )( const void * k, const void * y, void * context), void * context );
bsearch_sについても、qsort_sと同様の対応となっていますから、説明については省略します。
ただし、Visual C++は違う形式で同じ名前の関数を提供していますので注意が必要です。Visual C++のqsort_sは「戻り値がvoid」、「比較関数のcontextは1つ目の引数で渡される」という違いがあります。詳しくはMSDNのqsort_sの説明を参照して下さい。
今回学んだこと
- stddef.hでは、ポインターとポインターの差を表すptrdiff_t型や、構造体メンバーの位置を表すoffsetofマクロが提供されています。
- stdarg.hで提供されている機能を使って、可変個数の引数を持つ関数を作ることができます。
- sginal.hで提供されている機能を使って、シグナルを処理したり、発生させたりすることができます。
- stdlib.hで提供されているexit (quick_exit)やabortでプログラムを終了することができます。
- 同じくstdlib.hで提供されている機能を使って、環境変数へのアクセスや別のプログラムの起動ができます。
- 同じくstdlib.hで提供されている機能を使って、整列(ソート)や探索ができます。
Copyright © ITmedia, Inc. All Rights Reserved.