先ほどのサンプルプログラムは、コマンドライン引数からファイル名を受け取って、内容を出力しています。これは便利なようにも思えますが、ファイル名を受け取ることにしたがゆえに、標準入力の内容を出力するという目的には使えません。
プログラムの内容次第ですが、ファイルと標準入出力とを区別する必要がないのであれば、あえてファイルだけをターゲットにしたプログラムにするのはもったいないかもしれません。プログラム自体は標準入出力を相手にするように作っておくと、リダイレクト (redirect)
という方法を使うことで、ファイルの入出力に切り替え可能になります。
このように標準入力からデータを受け取るように書かれたプログラムは、リダイレクトを行うことによって、ソースコードを修正することなく、ファイルからデータを受け取るように変更できます。
リダイレクトとは、標準ストリームと結びついている先を任意に切り替える機能です。標準入力ストリームの実際の入力元を
test.txt
に切り替えて実行すれば、前の項のサンプルプログラムと同じ結果を得られます。
リダイレクトはC言語の機能ではなく、コマンドプロセッサ(Windows
のコマンドプロンプトなど)が持っているものです。ほとんどのコマンドプロセッサで、この機能が使えるはずです。
プログラム側は標準入力と標準出力を対象として書かれていますから、このように実行した場合には、標準入力から入力を受け取り、標準出力へ結果を出力するという動作になります。
文字列を数値に変換する 🔗
コマンドライン引数は文字列として渡されてくるため、数値を扱う場合に困ります。たとえば、1つ目のコマンドライン引数にファイル名を、2つ目の引数に行数を渡すと、その行の内容を出力するプログラムを作るとします。その場合、次のように実行します。
test test.txt 7
これは、test.txt の
7行目を出力するという意味です。このとき「7」の部分は、argv[2]
に入っている訳ですが、これは “7”
という文字列になってしまっているので、整数値として扱えるようにする方法を知っておく必要があります。
基本的には、文字列を整数に変換する標準ライブラリ関数を使うだけです。たとえば、atoi関数 を使えます。atoi関数は、<stdlib.h>
に、以下のように宣言されています。
int atoi( const char * str);
名前の由来は、“Ascii TO Integer” です。つまり、ASCIIコードの文字列を
int型の値に変換します。
long int型の値に変換して返す atol関数 、double型で返す
atof関数 、long long
int型で返す atoll関数 があります。
atoi関数は、引数に数字として解釈できるような文字で構成された文字列を渡すと、それを整数値に変換して返します。文字列を先頭から見ていって、数字として解釈できない文字が現れたら、その手前までを変換します。たとえば、“123abc”
を指定すると、123 が返されます。
なお、文字列の先頭部分につく符号「+」「-」は、符号として認識されます 。また、先頭部分に空白文字がある場合、それは無視されます 。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[])
{
if ( argc < 3 ) {
fputs( "コマンドライン引数が不足しています。 \n " , stderr);
exit( EXIT_FAILURE);
}
int target_line = atoi( argv[ 2 ]);
FILE * fp = fopen( argv[ 1 ], "r" );
if ( fp == NULL) {
fprintf( stderr, " %s のオープンに失敗しました。 \n " , argv[ 1 ]);
exit( EXIT_FAILURE);
}
for ( int line = 1 ; ; ++ line) {
char buf[ 80 ];
if ( fgets( buf, sizeof ( buf), fp) == NULL) {
if ( feof( fp)) {
break ;
}
else {
fputs( "ファイルの読み取りに失敗しました。 \n " , stderr);
exit( EXIT_FAILURE);
}
}
if ( line != target_line) {
continue ;
}
// 末尾の改行文字を取り除く
char * p = strchr( buf, ' \n ' );
if ( p != NULL) {
* p = ' \0 ' ;
}
puts( buf);
}
if ( fclose( fp) == EOF) {
fputs( "ファイルクローズに失敗しました。 \n " , stderr);
exit( EXIT_FAILURE);
}
}
入力ファイル (test.txt)
aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
実行結果:
ggg
ところが
atoi関数には問題があります。 たとえば、“abc”
のように明らかに数字として解釈できない文字列を渡したときに、それと分かるようなエラー値を返してくれません。つまり、エラーの検出が不可能 なのです。
お手軽な関数ではありますが、エラーになる可能性がある場面では使うべきではありません。コマンドライン引数は、どんな入力がなされるか分からないので、「エラーになる可能性がある場面」に該当します。
そこで代わりに、strtol関数 を使います。strtol関数は、<stdlib.h>
に、以下のように宣言されています。
long int strtol( const char * restrict str, char ** restrict end_ptr, int radix);
restrict については、第57章 で取り上げます。動作に影響はないので、今は無視して問題ありません。
第1引数に、変換元の文字列を渡します。
第2引数には、最初の変換不能な部分のメモリアドレス を受け取るポインタ変数を渡します。
第3引数には、基数を指定します。つまり、文字列に含まれている数字が、何進法で表記されているとみなすかを指示します。
戻り値は、変換結果が返されます。変換がまったく行えなかった場合には 0
が返されます。また、変換結果が long
int型で表現できる範囲を超えてしまう場合には、 上限値よりも大きいなら、LONG_MAX
が、下限値よりも小さいなら、LONG_MIN
が返されます。この場合、errno に ERANGE
がセットされます。errno については後述します。
第2引数が分かりづらいですが、これはたとえば、“123abc”
を変換しようとした場合には、“123” は変換可能で、“abc”
は変換不能ですから、“abc”
の先頭のメモリアドレスを受け取るということです。
atoi関数と比べると途端に複雑になった感がありますが、エラーが検出できるという面から、strtol関数の方が推奨されます。
strtol関数と同種の関数として、unsigned long型に変換する strtoul関数 、double型に変換する
strtod関数 、float型に変換する
strtof関数 、long
double型に変換する strtold関数 、long long型に変換する
strtoll関数 、unsigned
long long型に変換する strtoull関数 があります。
strtol関数を使って、先ほどのサンプルプログラムを書き換えてみます。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[])
{
if ( argc < 3 ) {
fputs( "コマンドライン引数が不足しています。 \n " , stderr);
exit( EXIT_FAILURE);
}
char * end_ptr;
errno = 0 ;
int target_line = strtol( argv[ 2 ], & end_ptr, 10 );
if ( errno == ERANGE || argv[ 2 ] == end_ptr) {
fprintf( stderr, "行数の指定が無効です。( %s ) \n " , argv[ 2 ]);
exit( EXIT_FAILURE);
}
FILE * fp = fopen( argv[ 1 ], "r" );
if ( fp == NULL) {
fprintf( stderr, " %s のオープンに失敗しました。 \n " , argv[ 1 ]);
exit( EXIT_FAILURE);
}
for ( int line = 1 ; ; ++ line) {
char buf[ 80 ];
if ( fgets( buf, sizeof ( buf), fp) == NULL) {
if ( feof( fp)) {
break ;
}
else {
fputs( "ファイルの読み取りに失敗しました。 \n " , stderr);
exit( EXIT_FAILURE);
}
}
if ( line != target_line) {
continue ;
}
// 末尾の改行文字を取り除く
char * p = strchr( buf, ' \n ' );
if ( p != NULL) {
* p = ' \0 ' ;
}
puts( buf);
}
if ( fclose( fp) == EOF) {
fputs( "ファイルクローズに失敗しました。 \n " , stderr);
exit( EXIT_FAILURE);
}
}
入力ファイル (test.txt)
aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
実行結果:
ggg
strtol関数のエラーをチェックするとき、もっとも注意が必要なのは、戻り値が 0 かどうかでは判断できない という点です。
なぜなら、“0”
という文字列を変換した場合は、正しい変換の結果として「0」が返されるからです。
まったく変換できなかったことを検出するには、第2引数で受け取ったメモリアドレスを、第1引数に指定したメモリアドレスと比較します。
たとえば、“abc”
のような、まったく変換不能な文字列を変換しようとした場合、第2引数には
“abc”
の先頭のメモリアドレスが返されることになるので、第1引数のメモリアドレスと一致します。
ちなみに、「途中までは変換できたが、途中から変換できなかった」場合をエラーとみなすのかどうかは、検討が必要です。大抵のサンプルプログラムは、これを成功とみなして書かれていますが、場合によっては検出する必要性があるかもしれません。必要ならば、やはり第2引数で受け取ったメモリアドレスを見て、今度は、第1引数の末尾のメモリアドレスが入っていることを調べます。
また、文字列に含まれている数字が巨大すぎて、long
int型に収まらないケースも検出しなければなりません。これは、変換自体は成功していても、正しく結果を返せないという状況です。
この場合、strtol関数は、LONG_MAX や LONG_MIN を返す訳ですが、0
を返す場合と同様に、変換結果がたまたま LONG_MAX や
LONG_MIN
と一致している可能性があるため、エラーチェックには使えません。正しい検出方法は、まず
errno に ERANGE が格納されたことを調べることです。
errno は、errno.h
に定義されており、一部の標準ライブラリ関数が、内部で起こしたエラーを伝えるために使うもので、グローバル変数であると考えればよいです。また、ERANGE
はエラーの種類を表現するオブジェクト形式マクロで、他にもいくつか種類があります。
厳密には、errno
はグローバル変数ではないこともありますが、意識する必要はありません。
エラーを errno
で報告する標準ライブラリ関数を呼び出す際には、その呼び出しの直前で 0
を代入しておき、呼び出しの直後で値を調べるようにします。 0
は、エラーが起きていないことを表す値です。
errno に ERANGE
が格納されていることを検出した後、上限値を超えているのか、下限値を超えているのかに興味があるのであれば、続けて、戻り値が
LONG_MAX なのか LONG_MIN なのかを調べます。
errno = 0 ;
int target_line = strtol( argv[ 2 ], & end_ptr, 10 );
if ( errno == ERANGE) {
if ( target_line == LONG_MAX) {
fprintf( stderr, "行数の指定が上限値を超えています。( %s ) \n " , argv[ 2 ]);
}
if ( target_line == LONG_MIN) {
fprintf( stderr, "行数の指定が加減値を超えています。( %s ) \n " , argv[ 2 ]);
}
exit( EXIT_FAILURE);
}
else if ( argv[ 2 ] == end_ptr) {
fprintf( stderr, "行数の指定が無効です。( %s ) \n " , argv[ 2 ]);
exit( EXIT_FAILURE);
}
こうなると非常に面倒です。これを見てしまうと、atoi関数に戻りたくなる気持ちは理解できますが、やはりエラーの可能性を見過ごせないので、strtol関数をラップしたものを用意しておくなどすると良いかもしれません。
/*
strtol関数をラップしたもの
引数:
str: 変換元の文字列。strtol関数の第1引数と同じ。
radix: 基数。strtol関数の第3引数と同じ。
result: 変換結果を受け取るポインタ変数。変換に失敗した場合には、何も格納されない。
戻り値:
変換が成功したら 0以外、失敗したら 0 が返される。
*/
int my_strtol( const char * str, int radix, long int * result)
{
assert( str != NULL);
assert( result != NULL);
char * end;
errno = 0 ;
long int num = strtol( str, & end, radix);
if ( errno == ERANGE) {
if ( num == LONG_MAX) {
fputs( "変換結果が上限値を超えた。 \n , stderr);
}
if ( num == LONG_MIN) {
fputs( "変換結果が下限値を超えた。 \n , stderr);
}
return 0 ;
}
else if ( str == end) {
fputs( "1文字も変換できなかった。 \n " , stderr);
return 0 ;
}
* result = num;
return 1 ;
}
こうした関数があれば、次のように簡潔に書けます。
long int target_line = 0 ;
if ( my_strtol( argv[ 2 ], 10 , & target_line) == 0 ) {
exit( EXIT_FAILURE);
}
練習問題 🔗
問題① コマンドライン引数から、0個以上の整数を受け取り、その合計を標準出力に出力するプログラムを作成してください。
問題② コマンドライン引数に、
のように、「整数」「演算子」「整数」を渡したとき、全体を計算式とみなして計算結果を標準出力へ出力するプログラムを作成してください。(環境によっては、*
の部分がうまく解釈されないかもしれません。その場合は、
のように、’ ’ で囲むことを試してみてください。
問題③ C言語のソースファイルやヘッダファイルを読み込んで、コメント部分を除去した結果を出力するプログラムを作成してください。読み込むファイルの名前は、コマンドライン引数から受け取るようにしてください。
問題④ コマンドライン引数から、ファイルパスを2つ指定し、1つ目のファイルの内容を、2つ目のファイルへ追記するプログラムを作成してください。
たとえば、
としたとき、in.txt の内容を、out.txt の末尾へ追記します。
解答ページはこちら 。
参考リンク 🔗
更新履歴 🔗
’2023/2/15
コーディング規約を統一(変数や関数の名前をスネークケースにする)
’2023/2/12
’2023/2/5
コーディング規約を統一(for、if などの ()
の前後の空白の空け方)
’2023/2/4
コーディング規約を統一(実引数がある関数呼び出しの (
の直後、)
の直前に空白を入れない)
’2022/10/13
コマンドプロンプトで、コマンドライン引数を指定した実行を行う方法は、Windows編で説明するようにした
エクスプローラーで、コマンドライン引数を指定した実行を行う方法は、Windows編で説明するようにした
≪さらに古い更新履歴≫
’2021/12/11
main関数から return 0;
を削除(C言語編全体でのコードの統一)
’2019/8/20
fgets関数を呼び出したあと、改行文字を取り除いて
puts関数へ渡すようにサンプルコードを修正
’2019/8/6
「コンパイラ」よりも「処理系」の方が適切ならば、「処理系」と書くように統一
’2019/8/1
解説のベースを C99 に上げる対応
標準ライブラリ関数の宣言に restrict を付加
’2019/7/23
解説のベースを C99 に上げる対応
ローカル変数の宣言を、ブロックの先頭以外の位置でも行う
’2019/7/15
解説のベースを C99 に上げる対応
ループ制御変数を for文の初期設定式で宣言するように修正
’2018/8/21
Visual Studio
でコマンドライン引数を指定して実行する方法については、開発ツールの情報のページでサポートするようにした。
’2018/5/25
第48章 の練習問題⑨⑩を移動してきて、練習問題③④とした。
’2018/4/20
「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。
’2018/4/2
「VisualC++」という表現を「VisualStudio」に統一。
’2018/3/20
全面的に文章を見直し、修正を行った。
「環境変数」の項を削除。
’2018/1/5
Xcode 8.3.3 を clang 5.0.0 に置き換え。
’2017/7/20
’2017/4/19
’2015/8/29
’2014/1/19
’2010/7/24
前の章へ (第44章 ファイルに対する操作)
次の章へ (第46章 マルチバイト文字)
C言語編のトップページへ
Programming Place Plus
のトップページへ
先頭へ戻る