高度な入出力とコマンドライン
コマンドライン引数はレストランでの注文のようなものです——プログラムに欲しいものを伝え(argv)、プログラムは注文の数を数え(argc)、そして注文を用意します。
コマンドライン引数
argcとargv
main関数は2つのパラメータを受け取ることができます:
int main(int argc, char *argv[])
argc:引数の数、最低でも1(プログラム名自体を含む)argv:引数ベクトル(文字列配列)、argv[0]はプログラム名
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("引数の数: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
実行:
./program hello world 123
引数の数: 4
argv[0] = ./program
argv[1] = hello
argv[2] = world
argv[3] = 123
atoi、atol、strtolで変換する必要があります。
実践的な引数解析
実際のプロジェクトでは、コマンドライン引数に-プレフィックスを使ったオプションを指定するのが一般的です:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
int verbose = 0;
int count = 1;
const char *filename = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
count = atoi(argv[++i]);
} else if (argv[i][0] != '-') {
filename = argv[i];
} else {
fprintf(stderr, "不明なオプション: %s\n", argv[i]);
return 1;
}
}
if (filename == NULL) {
fprintf(stderr, "使い方: %s [-v] [-n count] filename\n", argv[0]);
return 1;
}
printf("ファイル: %s, 回数: %d, 詳細: %s\n",
filename, count, verbose ? "オン" : "オフ");
return 0;
}
getopt関数を使うと、短いオプションと長いオプションの組み合わせを扱えます。
環境変数
getenv
char *getenv(const char *name);
環境変数の値を返します。存在しない場合はNULLを返します。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *path = getenv("PATH");
if (path != NULL) {
printf("PATH = %s\n", path);
}
const char *home = getenv("HOME");
if (home != NULL) {
printf("HOME = %s\n", home);
}
return 0;
}
extern変数environ
C言語には、すべての環境変数文字列の配列を指すグローバル変数environが定義されています:
#include <stdio.h>
extern char **environ;
int main(void) {
char **env = environ;
while (*env) {
printf("%s\n", *env);
env++;
}
return 0;
}
errnoエラー処理
errnoの仕組み
C標準ライブラリ関数がエラーに遭遇した場合、通常は直接クラッシュせず、グローバル変数errnoを設定しエラー値を返します:
#include <errno.h>
errnoはライブラリ関数呼び出しが成功した後でも自動的にゼロにクリアされません!関数呼び出しが失敗した後にのみ確認する必要があります。
perror / strerror
void perror(const char *s);
char *strerror(int errnum);
perrorはstderrにs: エラーメッセージを出力します。strerrorはエラーコードに対応するテキスト説明を返します。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
FILE *fp = fopen("nonexistent_file.txt", "r");
if (fp == NULL) {
perror("fopen");
printf("エラーコード: %d, エラーメッセージ: %s\n", errno, strerror(errno));
return 1;
}
fclose(fp);
return 0;
}
fopen: No such file or directory
エラーコード: 2, エラーメッセージ: No such file or directory
例
完全なエラー処理パターン:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "使い方: %s ファイル名\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
fprintf(stderr, "'%s'を開けません: %s\n", argv[1], strerror(errno));
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
if (ferror(fp)) {
fprintf(stderr, "読み取りエラーが発生しました: %s\n", strerror(errno));
fclose(fp);
return 1;
}
fclose(fp);
return 0;
}
ferrorはストリームで読み書きエラーが発生したかを確認するものであり、feof(ファイル終端の確認)とは異なります。
標準ストリーム
C言語はプログラム起動時に3つのストリームを自動的に開きます:
| ストリーム | 名称 | デフォルトデバイス | ファイル記述子 |
|---|---|---|---|
stdin |
標準入力 | キーボード | 0 |
stdout |
標準出力 | 画面 | 1 |
stderr |
標準エラー | 画面 | 2 |
stdoutとstderrの違い:stdoutはバッファリングされ、stderrはバッファリングされません。リダイレクト時には互いに影響しません。
#include <stdio.h>
int main(void) {
fprintf(stdout, "これは通常出力です\n");
fprintf(stderr, "これはエラー出力です\n");
return 0;
}
./program > output.txt
このように実行すると、「通常出力」はファイルに書き込まれ、「エラー出力」は画面に表示されたままになります。
リダイレクトの原理
コマンドラインでは:
>はstdoutをファイルにリダイレクト2>はstderrをファイルにリダイレクト<はstdinをファイルに置き換え|はあるプログラムのstdoutを別のプログラムのstdinに接続
./program 2>&1 all.log
このコマンドはstderrをstdoutに統合し、両方をall.logに書き込みます。
一時ファイル
tmpfile
FILE *tmpfile(void);
"wb+"モードで開かれた一時ファイルを作成します。ファイルは閉じるときまたはプログラム終了時に自動的に削除されます。
#include <stdio.h>
int main(void) {
FILE *tmp = tmpfile();
if (tmp == NULL) {
perror("tmpfile");
return 1;
}
fprintf(tmp, "一時データ %d\n", 42);
rewind(tmp);
char buf[64];
while (fgets(buf, sizeof(buf), tmp) != NULL) {
printf("%s", buf);
}
fclose(tmp);
return 0;
}
一時データ 42
tmpnam
char *tmpnam(char *s);
既存のファイルと衝突しない一時ファイル名を生成します。ただし、名前の生成とファイルの作成の間に別のプログラムが同じ名前のファイルを作成する競合状態があります。
tmpfileを優先してください——原子的にファイルを作成して開くため、より安全です。tmpnamはマルチスレッド環境では安全ではありません。
例
一時ファイルを使って中間データを処理します:
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *tmp = tmpfile();
if (tmp == NULL) {
perror("tmpfile");
return 1;
}
char *lines[] = {"banana", "apple", "orange", "grape"};
int n = 4;
for (int i = 0; i < n; i++) {
fprintf(tmp, "%s\n", lines[i]);
}
rewind(tmp);
printf("ソート前:\n");
char buf[64];
while (fgets(buf, sizeof(buf), tmp) != NULL) {
buf[strcspn(buf, "\n")] = '\0';
printf(" %s\n", buf);
}
rewind(tmp);
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
char a[64], b[64];
long pos = ftell(tmp);
fgets(a, sizeof(a), tmp);
fgets(b, sizeof(b), tmp);
a[strcspn(a, "\n")] = '\0';
b[strcspn(b, "\n")] = '\0';
if (strcmp(a, b) > 0) {
fseek(tmp, pos, SEEK_SET);
fprintf(tmp, "%s\n%s\n", b, a);
} else {
fseek(tmp, pos, SEEK_SET);
fprintf(tmp, "%s\n%s\n", a, b);
}
fseek(tmp, pos + strlen(a) + strlen(b) + 2, SEEK_SET);
}
rewind(tmp);
}
rewind(tmp);
printf("ソート後:\n");
while (fgets(buf, sizeof(buf), tmp) != NULL) {
buf[strcspn(buf, "\n")] = '\0';
printf(" %s\n", buf);
}
fclose(tmp);
return 0;
}
ソート前:
banana
apple
orange
grape
ソート後:
apple
grape
orange
banana
高度なフォーマット出力
snprintf
int snprintf(char *str, size_t size, const char *format, ...);
sprintfと比較して、snprintfはsizeパラメータが追加され、書き込み長を制限してバッファオーバーフローを防ぎます。
#include <stdio.h>
int main(void) {
char buf[10];
int n = snprintf(buf, sizeof(buf), "Hello, %s!", "World");
printf("buf = \"%s\", 必要な長さ = %d\n", buf, n);
return 0;
}
buf = "Hello, Wo", 必要な長さ = 13
snprintfの戻り値は、フォーマット出力が持つべき長さであり、実際に書き込まれたバイト数ではありません。戻り値がsize-1より大きい場合、出力は切り詰められています。
❓ よくある質問
📖 まとめ
argc/argvでコマンドライン引数を受け取り、argv[0]はプログラム名getenvで環境変数を読み取り、environですべての環境変数を走査errno+perror/strerrorが標準ライブラリのエラー処理の標準的手法stdoutはバッファリングされ、stderrはバッファリングされず、リダイレクト時には独立tmpfileは自動削除される一時ファイルを作成し、tmpnamより安全
📝 練習問題
-cで文字数、-lで行数、-wで単語数をカウントするコマンドラインプログラムを作成してくださいtmpfileを使ってファイルの内容を逆順にするプログラムを作成してください(すべて一時ファイルに書き込み、逆順で読み戻す)>と2>を使ってstdoutとstderrのリダイレクトの違いを示すプログラムを作成してください



