予備知識
最初に, fgets
scanf
printf
などに共通する テキストモード を確認しておきましょう。これは,OSごとに違う改行コード
OS | 俗称 | エスケープ表記 |
---|---|---|
Windows | CRLF | \r\n |
Linux macOS FreeBSD など |
LF | \n |
Macintosh | CR | \r |
**これらすべてをC言語側からは \n
として扱えるようにしてくれる特性を持ちます。**便利ですね。
指定サイズ内で1行を受け取る
格納領域が次のように256文字ぶん用意されていると仮定します。
char buffer[256];
scanf
とfgets
の主要な違いを簡単にまとめると以下のようになります。NULL文字\0
が終端に自動付与されることを考慮すると,ユーザが実質入力できるのは255文字までであることに注意します。
観点 | scanf |
fgets |
---|---|---|
基本的な使用法 | scanf("%255s", buffer) |
fgets(buffer, 256, stdin) |
改行の扱い | 改行の直前まで読み込む | 改行も一緒に読み込む |
成功時の返り値 | 読み込んだパラメータ数 | buffer |
失敗時の返り値 | 0 または EOF (-1) |
NULL |
入力文字数の取得 | できる | できない |
scanf
は"%s"
のようにサイズ指定せずに使うこともできますが,バッファオーバーランの危険性があるので出来るだけちゃんと書きましょう。
scanf
の利用
scanf
は多機能なので動作のカスタマイズが可能です。そのままの状態ではfgets
の劣化版のように見えてしまいますが,フォーマットをちゃんと書くことで実用性が向上します。
-
%s
はすべての空白文字を無視します。これには半角スペースも含まれています。そこで,%s
の代わりに%[^\n]
と書くことで,無視する対象を改行だけに限定することができます。%255[^\n]
は,改行以外を1〜255文字読み込むことを意味します。 -
%
の代わりに%*
を使うと,その部分を読み飛ばすことができます。1行に256文字以上入力されたとき,最初の255文字までを受け取って残りを捨てたい場合は,**%255[^\n]%*[^\n]
**と書きます。 - さらに次に
scanf
を使う場合に備えて,残った改行は**%*c
で読み飛ばします。これで保持されていた入力データはすべて処理されたことになります。但し,次に読み取るものが%d
など数値**である場合には,これをしなくても特に影響はありません。
また,scanf
は正常に読み取って変数に格納できたパラメータの数を返すので,これを成功したかどうかの判定に使うことができます。 %255[^\n]%*[^\n]
で実行した場合の返り値は以下のようになります。
- 1文字以上読み取れたときは 1
- 改行だけが入力されたときは 0
- 最初から Ctrl+D (Windowsの場合は Ctrl+Z) でEOFが入力されたときは
EOF
(この定数の値は -1)
失敗したときにはそのまま return 1;
してプログラムを異常終了扱いにさせておくとよいでしょう。
#include <stdio.h>
int main(void)
{
char buffer[256];
printf("Input: ");
if (scanf("%255[^\n]%*[^\n]", buffer) != 1) {
return 1;
}
scanf("%*c");
printf("Output: %s\n", buffer);
return 0;
}
(但しあらかじめ空文字列を格納しておく必要がある)
#include <stdio.h>
int main(void)
{
char buffer[256] = "";
printf("Input: ");
if (scanf("%255[^\n]%*[^\n]", buffer) == EOF) {
return 1;
}
scanf("%*c");
printf("Output: %s\n", buffer);
return 0;
}
(
%255[^\n]
の後ろに,unsigned/intの場合は%n
,size_t/ssize_tの場合は%zn
を書きます)#include <stdio.h>
int main(void)
{
char buffer[256];
size_t length;
printf("Input: ");
if (scanf("%255[^\n]%zn%*[^\n]", buffer, &length) != 1) {
return 1;
}
scanf("%*c");
printf("Output: %s\n", buffer);
printf("Length: %zu\n", length);
return 0;
}
(%n
系は符号付き整数で受け取るように定義されていますが,負の値が出現する可能性がゼロなので,符号無し整数でも特に問題はありません)
fgets
の利用
fgets
は改行も1行に含めて一緒に取り込みます。削除したい場合は自分でコードを書かなければなりません。strlen
関数を使うなどして文字列長を求めることも必要です。
また, 返り値は以下のようになります。
- 1文字以上読み取れたときは
buffer
(改行だけでも1文字以上という扱いになる) - 最初から Ctrl+D (Windowsの場合は Ctrl+Z) でEOFが入力されたときは
NULL
失敗したときにはそのまま return 1;
してプログラムを異常終了扱いにさせておくとよいでしょう。
#include <stdio.h>
#include <string.h>
int main(void)
{
char buffer[256];
size_t length;
printf("Input: ");
if (fgets(buffer, 256, stdin) == NULL || buffer[0] == '\n') {
return 1;
}
length = strlen(buffer);
if (buffer[length - 1] == '\n') {
buffer[--length] = '\0';
}
printf("Output: %s\n", buffer);
printf("Length: %zu\n", length);
return 0;
}
#include <stdio.h>
#include <string.h>
int main(void)
{
char buffer[256];
size_t length;
printf("Input: ");
if (fgets(buffer, 256, stdin) == NULL) {
return 1;
}
length = strlen(buffer);
if (length > 0 && buffer[length - 1] == '\n') {
buffer[--length] = '\0';
}
printf("Output: %s\n", buffer);
printf("Length: %zu\n", length);
return 0;
}
#include <stdio.h>
int main(void)
{
char buffer[256];
printf("Input: ");
if (fgets(buffer, 256, stdin) == NULL) {
return 1;
}
printf("Output: %s\n", buffer);
return 0;
}
整数を受け取る
scanf
の利用
一般的にはscanf
関数の %d
を使うことが多いと思います。ここでは,複数個の数値が入力された際,余分なものは無視するように書いてみます。
#include <stdio.h>
int main(void)
{
int num;
printf("Input: ");
if (scanf("%d%*[^\n]", &num) != 1) {
return 1;
}
scanf("%*c");
printf("Output: %d\n", buffer);
return 0;
}
しかし,これには
- 最初にいきなり空白文字が入力された場合に読み取りが終了しない
- 範囲外の値が入力された場合に対応できない
などの欠点があります。
scanf
とstrtol
の併用
scanf
単独でやろうとすると上述のような欠点があるので,まず文字列として受け取って,あとから変換するアプローチを採ってみます。strtol
関数を利用するので,整数の型は long
(正確にはlong int
) になります。なお,atoi
やatol
にはエラー検出機能は無いので注意してください。
(簡易版)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void)
{
char buffer[32], *endptr;
long num;
printf("Input: ");
if (scanf("%31[^\n]%*[^\n]", buffer) != 1) {
return 1;
}
scanf("%*c");
num = strtol(buffer, &endptr, 10);
if (*endptr != '\0' || errno == ERANGE) {
return 1;
}
printf("Output: %ld\n", num);
return 0;
}
(エラーメッセージも表示する完全版)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int main(void)
{
char buffer[32], *endptr;
long num;
printf("Input: ");
if (scanf("%31[^\n]%*[^\n]", buffer) != 1) {
fprintf(stderr, "Error: No input specified\n");
return 1;
}
scanf("%*c");
num = strtol(buffer, &endptr, 10);
if (*endptr != '\0') {
fprintf(stderr, "Error: Invalid charcter found: %c\n", *endptr);
return 1;
}
if (errno == ERANGE) {
fprintf(stderr, "Error: Out of range (%s)\n",
num == LONG_MAX ? "Overflow" : "Underflow");
return 1;
}
printf("Output: %ld\n", num);
return 0;
}
指定サイズ内で複数行を受け取る
fread
を使えば行という概念にとらわれず,格納領域がすべて埋まるか入力がEOFに到達するまで読み取ることができます。しかし,バイナリモードで読み取るため,最初に説明した改行コードの自動変換の機能を持ちません。テキストを処理する際には少し不便です。
そこでscanf
の出番です。**%4095[\x01-\xff]
**のように書くと,NULL文字以外の文字をすべて読み取ることができます。つまり,改行も読み取れるのです。
#include <stdio.h>
#include <string.h>
int main(void)
{
char buffer[4096], *p;
size_t i = 0;
printf("Input: \n");
if (scanf("%4095[\x01-\xff]", buffer) != 1) {
return 1;
}
printf("Output: \n");
for (i = 0, p = strtok(buffer, "\n"); p; ++i, p = strtok(NULL, "\n")) {
printf("[%zu] %s\n", i, p);
}
return 0;
}
任意サイズの1行を受け取る
基本的にC言語で可変長は「どうしても」という場合以外は避けるべきですが,メモリが許す限りどんな長さの1行でも受け取ってくれるgetline
という関数があります。使用後に自分でメモリをfree
する必要があるので注意してください。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *buffer = NULL;
ssize_t length;
printf("Input: ");
if ((length = getline(&buffer, &(size_t){4096}, stdin)) == EOF) {
return 1;
}
if (buffer[length - 1] == '\n') {
buffer[--length] = '\0';
}
printf("Output: %s\n", buffer);
printf("Length: %zd\n", length);
free(buffer);
return 0;
}
ここでは最初に確保される領域の大きさを 4096 文字にしました。オーバーした場合は自動的に拡張されますが,大量に入力されることが事前にわかっていれば大きめに設定しておくほうが望ましいです。
日本語文字列を文字単位で処理する
基本事項
文字コードについて
さて,ここまではchar
を文字と称してきましたが,厳密にはこれは誤りです。なぜならば,日本語などはchar
1個ぶんのデータでは表せないからです。例えば最近最も使われるUTF-8という文字コードだと,日本語で使われる1文字には**char
3つ分の領域が必要です。そのためchar
1つをバイトという単位で数え,日本語で使われる文字はそれらが複数なのでマルチバイト文字**と呼ばれます。
マルチバイト文字を扱う際,連続したバイト列としてとして扱うぶんには何も工夫しなくても困らないことが多いのですが,1バイトではなく1文字ずつ取り出したいあるいはバイト長ではなく文字列長を求めたいというときには特殊な処理を書く必要があります。
ロケール設定
マルチバイト文字を正しく扱う場合,以下の設定を行います。
#include <locale.h>
setlocale(LC_ALL, "");
使用する型など
- マルチバイト文字を1文字ずつ区別できるように格納する場合,
char
の代わりに**wchar_t
という型を使います。wchar.h
**のインクルードが必要です。 -
char
の代わりにwchar_t
のリテラルを書きたい場合,''
""
はそれぞれL''
L""
と書きます。 -
char
の代わりにwchar_t
用のフォーマット指定子を使いたい場合,%c
%s
はそれぞれ%lc
%ls
と書きます。
例
(
%n
で求められるのはバイト長であって文字列長ではありません,そのためループ条件変数としては使えません)#include <stdio.h>
#include <wchar.h>
#include <locale.h>
int main(void)
{
wchar_t buffer[256];
setlocale(LC_ALL, "");
printf("入力: ");
if (scanf("%255l[^\n]%*[^\n]", buffer) != 1) {
return 1;
}
scanf("%*c");
printf("出力: ");
for (wchar_t *c = buffer; *c; ++c) {
printf("「%lc」", *c);
}
putchar('\n');
return 0;
}
macOS における scanf
関数および wscanf
関数のバグ
以下に述べることはmacOS固有です。Linuxでは確認できませんでした。WindowsやFreeBSDでは未確認です,誰か教えてください。